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)
|
||||
//! Logical actions represent action types sent to character
|
||||
//! Physical actions are actions that are performed with the character
|
||||
//! [ActionGraph] is a specialized directed [GraphMap],
|
||||
//! defined with physical actions (vertices) and logical actions (edges),
|
||||
//! 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
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
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