Compare commits

..

No commits in common. "62199a1817ce9c530023ee303558b92e28d305e0" and "d38dec9ea5025af1d98dc6b4c07f7f13b8759fcc" have entirely different histories.

7 changed files with 407 additions and 1028 deletions

1170
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,13 +1,12 @@
cargo-features = ["codegen-backend"]
[package]
name = "bevy_combat_proto"
name = "bevy_template"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = { version = "0.18.0" }
petgraph = { version = "0.8.3" }
[profile.dev]
opt-level = 1

View file

@ -1,190 +0,0 @@
//! [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 }
}
}

View file

@ -1,3 +0,0 @@
//! Specialized graph types
pub mod action;

View file

@ -1,8 +0,0 @@
#![warn(missing_docs)]
//! Combat prototype made with bevy
#[cfg(test)]
mod tests;
pub mod graph;

View file

@ -1,68 +0,0 @@
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());
}
}
}
}

View file

@ -1 +0,0 @@
mod graph;