generated from 2ndbeam/bevy-template
feat: Implemented ActionGraph
This commit is contained in:
parent
18931ac157
commit
62199a1817
4 changed files with 262 additions and 3 deletions
|
|
@ -1,3 +1,190 @@
|
||||||
//! Action graph is a directed [GraphMap] defined with physical actions (vertices) and logical actions (edges)
|
//! [ActionGraph] is a specialized directed [GraphMap],
|
||||||
//! Logical actions represent action types sent to character
|
//! defined with physical actions (vertices) and logical actions (edges),
|
||||||
//! Physical actions are actions that are performed with the character
|
//! named phys and logs respectively
|
||||||
|
//!
|
||||||
|
//! # Physical actions (Phys)
|
||||||
|
//! Phys are actions that are performed with the character
|
||||||
|
//! Graph wraps phys with [ActionNode] that also contains node level in the graph
|
||||||
|
//! Phys "level" is the amount of logs taken to reach this state
|
||||||
|
//! and allows several copies of each phys to be placed in the graph
|
||||||
|
//!
|
||||||
|
//! # Logical actions (Log)
|
||||||
|
//! Logs represent action types sent to character as input
|
||||||
|
//! They define which phys should be performed next
|
||||||
|
//!
|
||||||
|
//! # Example
|
||||||
|
//! ```
|
||||||
|
//! use bevy_combat_proto::graph::action::{ActionGraph, ActionNode, PhysicalAction};
|
||||||
|
//!
|
||||||
|
//! // This is the logical action
|
||||||
|
//! ##[derive(Debug, PartialEq, Eq)]
|
||||||
|
//! enum AttackType {
|
||||||
|
//! Light,
|
||||||
|
//! Heavy,
|
||||||
|
//! }
|
||||||
|
//! // This is the physical action
|
||||||
|
//! ##[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
//! enum AttackAction {
|
||||||
|
//! Stab,
|
||||||
|
//! VerticalSlash,
|
||||||
|
//! HorizontalSlash,
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // While not necessary, it's useful to implement PhysicalAction,
|
||||||
|
//! // because it provides Default for ActionGraph
|
||||||
|
//! impl PhysicalAction<AttackType> for AttackAction {
|
||||||
|
//! fn paths() -> Vec<Vec<(AttackType, Self)>> {
|
||||||
|
//! use AttackType as L;
|
||||||
|
//! use AttackAction as P;
|
||||||
|
//! vec![
|
||||||
|
//! vec![(L::Heavy, P::VerticalSlash), (L::Light, P::HorizontalSlash), (L::Heavy, P::Stab)],
|
||||||
|
//! vec![(L::Heavy, P::VerticalSlash), (L::Heavy, P::Stab), (L::Light, P::Stab)],
|
||||||
|
//! vec![(L::Light, P::HorizontalSlash), (L::Heavy, P::Stab), (L::Light, P::Stab)],
|
||||||
|
//! vec![(L::Light, P::HorizontalSlash), (L::Light, P::VerticalSlash), (L::Heavy, P::HorizontalSlash)],
|
||||||
|
//! vec![(L::Light, P::HorizontalSlash), (L::Light, P::VerticalSlash), (L::Light, P::VerticalSlash)],
|
||||||
|
//! ]
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! // Attack combos in this case may be implemented with such graph:
|
||||||
|
//! // -(Light)------------------> None
|
||||||
|
//! // /
|
||||||
|
//! // -(Light)-> Some(HorizontalSlash)-(Heavy)------------> Some(Stab)
|
||||||
|
//! // / /
|
||||||
|
//! // -(Heavy)---> Some(VerticalSlash) -(Light)--
|
||||||
|
//! // / \ /
|
||||||
|
//! // None -(Heavy)------------> Some(Stab)-(Heavy)------------------> None
|
||||||
|
//! // \ /
|
||||||
|
//! // -(Light)-> Some(HorizontalSlash) -(Heavy)-> Some(HorizontalSlash)
|
||||||
|
//! // \ /
|
||||||
|
//! // -(Light)---> Some(VerticalSlash)-(Light)---> Some(VerticalSlash)
|
||||||
|
//! // [0 logs] [1 log] [2 logs] [3 logs]
|
||||||
|
//!
|
||||||
|
//! // Given that AttackType::Light is represented with button A,
|
||||||
|
//! // and AttackType::Heavy is represented with button B, these are the combos:
|
||||||
|
//! // B->A->B == VerticalSlash->HorizontalSlash->Stab
|
||||||
|
//! // B->B->A == VerticalSlash->Stab->Stab
|
||||||
|
//! // A->B->A == HorizontalSlash->Stab->Stab]
|
||||||
|
//! // A->A->B == HorizontalSlash->VerticalSlash->HorizontalSlash]
|
||||||
|
//! // A->A->A == HorizontalSlash->VerticalSlash->VerticalSlash]
|
||||||
|
//!
|
||||||
|
//! fn main() {
|
||||||
|
//! let mut graph = ActionGraph::<AttackAction, AttackType>::default();
|
||||||
|
//!
|
||||||
|
//! // Perform some combo
|
||||||
|
//!
|
||||||
|
//! // Gives VerticalSlash with level 1
|
||||||
|
//! let first_action = graph.next(AttackType::Heavy);
|
||||||
|
//! // Gives Stab with level 2
|
||||||
|
//! let second_action = graph.next(AttackType::Heavy);
|
||||||
|
//! // Gives Stab with level 3
|
||||||
|
//! let third_action = graph.next(AttackType::Light);
|
||||||
|
//! // Gives None, which can be translated to starting point of the graph using
|
||||||
|
//! // unwrap_or_default
|
||||||
|
//! let fourth_action = graph.next(AttackType::Light);
|
||||||
|
//!
|
||||||
|
//! assert_eq!(first_action, Some(ActionNode::new(Some(AttackAction::VerticalSlash), 1)));
|
||||||
|
//! assert_eq!(second_action, Some(ActionNode::new(Some(AttackAction::Stab), 2)));
|
||||||
|
//! assert_eq!(third_action, Some(ActionNode::new(Some(AttackAction::Stab), 3)));
|
||||||
|
//! assert_eq!(fourth_action, None);
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::{fmt::Debug, hash::Hash};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use petgraph::{graphmap::NodeTrait, prelude::*};
|
||||||
|
|
||||||
|
/// This trait is used to implement [Default] on specialized [ActionGraph]
|
||||||
|
pub trait PhysicalAction<Log: Eq>: NodeTrait {
|
||||||
|
/// Default paths to use with [ActionGraph::from_paths]
|
||||||
|
fn paths() -> Vec<Vec<(Log, Self)>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Graph that defines relations between physical actions
|
||||||
|
/// See module docs for usage example
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct ActionGraph<Phys, Log>
|
||||||
|
where Phys: NodeTrait, Log: Eq {
|
||||||
|
graph: DiGraphMap<ActionNode<Phys>, Log>,
|
||||||
|
state: ActionNode<Phys>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Phys, Log> ActionGraph<Phys, Log>
|
||||||
|
where Phys: NodeTrait, Log: Eq {
|
||||||
|
|
||||||
|
/// Constructs new ActionGraph from paths (Log, Phys)
|
||||||
|
pub fn from_paths(paths: Vec<Vec<(Log, Phys)>>) -> Self {
|
||||||
|
let mut g = DiGraphMap::new();
|
||||||
|
|
||||||
|
let start_node = ActionNode::default();
|
||||||
|
g.add_node(start_node);
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
let mut current_node = start_node;
|
||||||
|
for (i, (log, phys)) in path.into_iter().enumerate() {
|
||||||
|
let next_node = ActionNode::new(Some(phys), 1 + i as u8);
|
||||||
|
if g.nodes().find(|n| *n == next_node).is_none() {
|
||||||
|
g.add_node(next_node);
|
||||||
|
}
|
||||||
|
g.add_edge(current_node, next_node, log);
|
||||||
|
current_node = next_node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Self { graph: g, state: start_node }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes log and returns next phys, returns [None] if graph returned to starting point
|
||||||
|
/// Use [Option::unwrap_or_default] on result of this method
|
||||||
|
pub fn next(&mut self, log: Log) -> Option<ActionNode<Phys>> {
|
||||||
|
let state = self.state;
|
||||||
|
let out_state = self.graph.edges(state)
|
||||||
|
.find_map(|(_, next_phys, checked_log)| {
|
||||||
|
if checked_log == &log && next_phys.action.is_some() { Some(next_phys) } else { None }
|
||||||
|
});
|
||||||
|
let next_state = out_state.unwrap_or_default();
|
||||||
|
|
||||||
|
self.state = next_state;
|
||||||
|
|
||||||
|
out_state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets state of the graph
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.state = ActionNode::default();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Phys, Log> Default for ActionGraph<Phys, Log>
|
||||||
|
where Phys: PhysicalAction<Log>, Log: Eq {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from_paths(Phys::paths())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Node type used in [ActionGraph], wraps physical action with it's level in the graph
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Reflect)]
|
||||||
|
#[reflect(Clone, PartialEq, Hash)]
|
||||||
|
pub struct ActionNode<Phys>
|
||||||
|
where Phys: NodeTrait {
|
||||||
|
/// Action type
|
||||||
|
pub action: Option<Phys>,
|
||||||
|
/// Action level
|
||||||
|
pub level: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Phys> Default for ActionNode<Phys>
|
||||||
|
where Phys: NodeTrait {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { action: None, level: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Phys> ActionNode<Phys>
|
||||||
|
where Phys: NodeTrait {
|
||||||
|
/// Constructs new node
|
||||||
|
pub fn new(action: Option<Phys>, level: u8) -> Self {
|
||||||
|
Self { action, level }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,7 @@
|
||||||
|
|
||||||
//! Combat prototype made with bevy
|
//! Combat prototype made with bevy
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
pub mod graph;
|
pub mod graph;
|
||||||
|
|
|
||||||
68
src/tests/graph.rs
Normal file
68
src/tests/graph.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::graph::action::{ActionGraph, ActionNode, PhysicalAction};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum AttackType {
|
||||||
|
Light,
|
||||||
|
Heavy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
enum AttackAction {
|
||||||
|
Stab,
|
||||||
|
VerticalSlash,
|
||||||
|
HorizontalSlash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PhysicalAction<AttackType> for AttackAction {
|
||||||
|
fn paths() -> Vec<Vec<(AttackType, Self)>> {
|
||||||
|
use AttackType as L;
|
||||||
|
use AttackAction as P;
|
||||||
|
vec![
|
||||||
|
vec![(L::Heavy, P::VerticalSlash), (L::Light, P::HorizontalSlash), (L::Heavy, P::Stab)],
|
||||||
|
vec![(L::Heavy, P::VerticalSlash), (L::Heavy, P::Stab), (L::Light, P::Stab)],
|
||||||
|
vec![(L::Light, P::HorizontalSlash), (L::Heavy, P::Stab), (L::Light, P::Stab)],
|
||||||
|
vec![(L::Light, P::HorizontalSlash), (L::Light, P::VerticalSlash), (L::Heavy, P::HorizontalSlash)],
|
||||||
|
vec![(L::Light, P::HorizontalSlash), (L::Light, P::VerticalSlash), (L::Light, P::VerticalSlash)],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_wrong_combos() -> Vec<Vec<AttackType>> {
|
||||||
|
use AttackType as L;
|
||||||
|
vec![
|
||||||
|
vec![L::Heavy, L::Light, L::Light],
|
||||||
|
vec![L::Heavy, L::Heavy, L::Heavy],
|
||||||
|
vec![L::Light, L::Heavy, L::Heavy],
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn action_graph_combos() {
|
||||||
|
let mut graph = ActionGraph::<AttackAction, AttackType>::default();
|
||||||
|
|
||||||
|
for combo in AttackAction::paths() {
|
||||||
|
for (i, (log, phys)) in combo.into_iter().enumerate() {
|
||||||
|
let expected_node = ActionNode::new(Some(phys), 1 + i as u8);
|
||||||
|
let checked_node = graph.next(log).unwrap();
|
||||||
|
assert_eq!(checked_node, expected_node);
|
||||||
|
}
|
||||||
|
graph.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_wrong_combos() {
|
||||||
|
let wrong_combos = get_wrong_combos();
|
||||||
|
|
||||||
|
let mut graph = ActionGraph::<AttackAction, AttackType>::default();
|
||||||
|
|
||||||
|
for combo in wrong_combos {
|
||||||
|
for (i, log) in combo.iter().enumerate() {
|
||||||
|
if i < combo.len() - 1 {
|
||||||
|
assert!(graph.next(*log).is_some());
|
||||||
|
} else {
|
||||||
|
assert!(graph.next(*log).is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/tests/mod.rs
Normal file
1
src/tests/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mod graph;
|
||||||
Loading…
Add table
Add a link
Reference in a new issue