use core::time::Duration;
use bevy::prelude::*;
use log::warn;
use crate::prelude::*;
#[derive(Component, Default, Debug, Clone)]
#[cfg_attr(
feature = "reflect",
derive(Reflect),
reflect(Clone, Component, Debug, Default)
)]
pub struct Combo {
pub steps: Vec<ComboStep>,
pub cancel_actions: Vec<CancelAction>,
pub time_kind: TimeKind,
step_index: usize,
timer: Timer,
}
impl Combo {
pub fn with_step(mut self, step: impl Into<ComboStep>) -> Self {
self.steps.push(step.into());
self
}
pub fn with_cancel(mut self, action: impl Into<CancelAction>) -> Self {
self.cancel_actions.push(action.into());
self
}
#[must_use]
pub fn timer(&self) -> &Timer {
&self.timer
}
fn reset(&mut self) {
self.step_index = 0;
self.timer.reset();
let duration = self.steps.first().map(|s| s.timeout).unwrap_or_default();
self.timer.set_duration(Duration::from_secs_f32(duration));
}
fn is_cancelled(&self, actions: &ActionsQuery) -> bool {
let current_step = &self.steps[self.step_index];
for condition in &self.cancel_actions {
if condition.action == current_step.action {
continue;
}
let Ok((.., events, _)) = actions.get(condition.action) else {
warn!(
"cancel condition references an invalid action `{}`",
condition.action
);
continue;
};
if events.intersects(condition.events) {
return true;
}
}
for step in &self.steps {
if step.action == current_step.action {
continue;
}
let Ok((.., events, _)) = actions.get(step.action) else {
continue;
};
if events.intersects(step.events) {
return true;
}
}
false
}
}
impl InputCondition for Combo {
fn evaluate(
&mut self,
actions: &ActionsQuery,
time: &ContextTime,
_value: ActionValue,
) -> TriggerState {
if self.steps.is_empty() {
warn!("combo has no steps");
return TriggerState::None;
}
if self.is_cancelled(actions) {
self.reset();
}
if self.step_index > 0 {
self.timer.tick(time.delta_kind(self.time_kind));
if self.timer.is_finished() {
self.reset();
}
}
let current_step = &self.steps[self.step_index];
let Ok((_, &state, events, _)) = actions.get(current_step.action) else {
warn!(
"step {} references an invalid action `{}`",
self.step_index, current_step.action
);
self.reset();
return TriggerState::None;
};
if events.contains(current_step.events) {
self.step_index += 1;
if self.step_index >= self.steps.len() {
self.reset();
return TriggerState::Fired;
} else {
let next_step = &self.steps[self.step_index];
self.timer.reset();
self.timer
.set_duration(Duration::from_secs_f32(next_step.timeout));
}
}
if self.step_index > 0 || state > TriggerState::None {
return TriggerState::Ongoing;
}
TriggerState::None
}
fn kind(&self) -> ConditionKind {
ConditionKind::Implicit
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "reflect", derive(Reflect), reflect(Clone, Debug))]
pub struct ComboStep {
pub action: Entity,
pub events: ActionEvents,
pub timeout: f32,
}
impl ComboStep {
#[must_use]
pub fn new(action: Entity) -> Self {
Self {
action,
events: ActionEvents::COMPLETE,
timeout: 0.5,
}
}
#[must_use]
pub fn with_events(mut self, events: ActionEvents) -> Self {
self.events = events;
self
}
#[must_use]
pub fn with_timeout(mut self, timeout: f32) -> Self {
self.timeout = timeout;
self
}
}
impl From<Entity> for ComboStep {
fn from(action: Entity) -> Self {
Self::new(action)
}
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "reflect", derive(Reflect), reflect(Clone, Debug))]
pub struct CancelAction {
pub action: Entity,
pub events: ActionEvents,
}
impl CancelAction {
#[must_use]
fn new(action: Entity) -> Self {
Self {
action,
events: ActionEvents::ONGOING | ActionEvents::FIRE,
}
}
}
impl From<Entity> for CancelAction {
fn from(action: Entity) -> Self {
Self::new(action)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context;
#[test]
fn empty() {
let (world, mut state) = context::init_world();
let (time, actions) = state.get(&world);
let mut condition = Combo::default();
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
}
#[test]
fn invalid_step() {
let (world, mut state) = context::init_world();
let (time, actions) = state.get(&world);
let mut condition = Combo::default().with_step(Entity::PLACEHOLDER);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn timeout() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), ActionEvents::COMPLETE))
.id();
let action_b = world.spawn(Action::<B>::new()).id();
world
.resource_mut::<Time<Real>>()
.advance_by(Duration::from_secs(1));
let (time, actions) = state.get(&world);
let mut condition = Combo::default().with_step(action_a).with_step(action_b);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing,
"first step shouldn't be affected by time"
);
assert_eq!(condition.step_index, 1);
world
.resource_mut::<Time<Real>>()
.advance_by(Duration::from_secs(1));
world.entity_mut(action_a).insert(ActionEvents::empty()); world.entity_mut(action_b).insert(ActionEvents::COMPLETE);
let (time, actions) = state.get(&world);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn first_step_ongoing() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), TriggerState::Ongoing))
.id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default().with_step(action_a);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing
);
}
#[test]
fn steps() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), ActionEvents::ONGOING))
.id();
let action_b = world.spawn(Action::<B>::new()).id();
let action_c = world.spawn(Action::<C>::new()).id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default()
.with_step(ComboStep::new(action_a).with_events(ActionEvents::ONGOING))
.with_step(ComboStep::new(action_b).with_timeout(0.6))
.with_step(ComboStep::new(action_c).with_timeout(0.3));
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing
);
assert_eq!(condition.step_index, 1);
world
.resource_mut::<Time<Real>>()
.advance_by(Duration::from_secs_f32(0.5));
world.entity_mut(action_a).insert(ActionEvents::empty());
world.entity_mut(action_b).insert(ActionEvents::COMPLETE);
let (time, actions) = state.get(&world);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing
);
assert_eq!(condition.step_index, 2);
world
.resource_mut::<Time<Real>>()
.advance_by(Duration::from_secs_f32(0.2));
world.entity_mut(action_b).insert(ActionEvents::empty());
world.entity_mut(action_c).insert(ActionEvents::COMPLETE);
let (time, actions) = state.get(&world);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Fired
);
assert_eq!(condition.step_index, 0);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn same_action() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), ActionEvents::COMPLETE))
.id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default().with_step(action_a).with_step(action_a);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing
);
assert_eq!(condition.step_index, 1);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Fired
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn out_of_order() {
let (mut world, mut state) = context::init_world();
let action_a = world.spawn(Action::<A>::new()).id();
let action_b = world
.spawn((Action::<B>::new(), ActionEvents::COMPLETE))
.id();
let action_c = world.spawn(Action::<C>::new()).id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default()
.with_step(action_a)
.with_step(action_b)
.with_step(action_c);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
assert_eq!(condition.step_index, 0);
world.entity_mut(action_b).insert(ActionEvents::empty());
world.entity_mut(action_a).insert(ActionEvents::COMPLETE);
let (time, actions) = state.get(&world);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing
);
assert_eq!(condition.step_index, 1);
world.entity_mut(action_a).insert(ActionEvents::empty());
world.entity_mut(action_c).insert(ActionEvents::COMPLETE);
let (time, actions) = state.get(&world);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn ignore_same_cancel_action() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), ActionEvents::COMPLETE))
.id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default().with_step(action_a).with_cancel(action_a);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Fired
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn missing_cancel_action() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), ActionEvents::COMPLETE))
.id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default()
.with_step(action_a)
.with_cancel(Entity::PLACEHOLDER);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Fired
);
assert_eq!(condition.step_index, 0);
}
#[test]
fn cancel() {
let (mut world, mut state) = context::init_world();
let action_a = world
.spawn((Action::<A>::new(), ActionEvents::COMPLETE))
.id();
let action_b = world.spawn(Action::<B>::new()).id();
let action_c = world.spawn(Action::<C>::new()).id();
let (time, actions) = state.get(&world);
let mut condition = Combo::default()
.with_step(action_a)
.with_step(action_b)
.with_cancel(action_c);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::Ongoing
);
assert_eq!(condition.step_index, 1);
world.entity_mut(action_a).insert(ActionEvents::empty());
world.entity_mut(action_b).insert(ActionEvents::COMPLETE);
world.entity_mut(action_c).insert(ActionEvents::FIRE);
let (time, actions) = state.get(&world);
assert_eq!(
condition.evaluate(&actions, &time, 0.0.into()),
TriggerState::None
);
assert_eq!(condition.step_index, 0);
}
#[derive(Debug, InputAction)]
#[action_output(bool)]
struct A;
#[derive(Debug, InputAction)]
#[action_output(bool)]
struct B;
#[derive(Debug, InputAction)]
#[action_output(bool)]
struct C;
}