use bevy::{
ecs::template::{EntityTemplate, Template, TemplateContext},
log::warn_once,
prelude::*,
};
use smallvec::{SmallVec, smallvec};
use crate::prelude::*;
#[derive(Component, Debug, Clone)]
#[cfg_attr(feature = "reflect", derive(Reflect), reflect(Clone, Component, Debug))]
pub struct Chord {
pub actions: SmallVec<[Entity; 2]>,
pub ongoing: bool,
}
impl Chord {
#[must_use]
pub fn single(action: Entity) -> Self {
Self::new(smallvec![action])
}
#[must_use]
pub fn new(actions: impl Into<SmallVec<[Entity; 2]>>) -> Self {
Self {
actions: actions.into(),
ongoing: true,
}
}
#[must_use]
pub fn with_ongoing(mut self, enable: bool) -> Self {
self.ongoing = enable;
self
}
}
impl FromTemplate for Chord {
type Template = ChordTemplate;
}
#[derive(Debug)]
pub struct ChordTemplate {
pub actions: SmallVecTemplate<EntityTemplate, 2>,
pub ongoing: bool,
}
impl Default for ChordTemplate {
fn default() -> Self {
Self {
actions: SmallVecTemplate::default(),
ongoing: true,
}
}
}
impl ChordTemplate {
#[must_use]
pub fn single(action: impl Into<EntityTemplate>) -> Self {
let action = action.into();
Self::new([action])
}
#[must_use]
pub fn new(actions: impl Into<SmallVecTemplate<EntityTemplate, 2>>) -> Self {
Self {
actions: actions.into(),
ongoing: true,
}
}
#[must_use]
pub fn with_ongoing(mut self, enable: bool) -> Self {
self.ongoing = enable;
self
}
}
impl Template for ChordTemplate {
type Output = Chord;
fn build_template(&self, context: &mut TemplateContext) -> Result<Chord> {
Ok(Chord::new(self.actions.build_template(context)?).with_ongoing(self.ongoing))
}
fn clone_template(&self) -> Self {
Self {
actions: self.actions.clone_template(),
ongoing: self.ongoing,
}
}
}
impl InputCondition for Chord {
fn evaluate(
&mut self,
actions: &ActionsQuery,
_time: &ContextTime,
_value: ActionValue,
) -> TriggerState {
let mut has_active = false;
let mut all_fired = true;
for &action in &self.actions {
let Ok((_, &state, ..)) = actions.get(action) else {
warn_once!("`{action}` is not a valid action");
continue;
};
if state != TriggerState::None {
has_active = true;
}
if state != TriggerState::Fired {
all_fired = false;
}
}
if has_active && all_fired {
TriggerState::Fired
} else if has_active && self.ongoing {
TriggerState::Ongoing
} else {
TriggerState::None
}
}
fn kind(&self) -> ConditionKind {
ConditionKind::Implicit
}
}
#[cfg(test)]
mod tests {
use bevy_enhanced_input_macros::InputAction;
use super::*;
use crate::context;
#[test]
fn fired() {
let (mut world, mut state) = context::init_world();
let action = world
.spawn((Action::<Test>::new(), TriggerState::Fired))
.id();
let (time, actions) = state.get(&world).unwrap();
let mut condition = Chord::single(action);
assert_eq!(
condition.evaluate(&actions, &time, true.into()),
TriggerState::Fired,
);
}
#[test]
fn with_ongoing() {
let (mut world, mut state) = context::init_world();
let action1 = world
.spawn((Action::<Test>::new(), TriggerState::Fired))
.id();
let action2 = world
.spawn((Action::<Test>::new(), TriggerState::None))
.id();
let (time, actions) = state.get(&world).unwrap();
let mut condition = Chord::new([action1, action2]).with_ongoing(true);
assert_eq!(
condition.evaluate(&actions, &time, true.into()),
TriggerState::Ongoing,
);
}
#[test]
fn without_ongoing() {
let (mut world, mut state) = context::init_world();
let action1 = world
.spawn((Action::<Test>::new(), TriggerState::Fired))
.id();
let action2 = world
.spawn((Action::<Test>::new(), TriggerState::None))
.id();
let (time, actions) = state.get(&world).unwrap();
let mut condition = Chord::new([action1, action2]).with_ongoing(false);
assert_eq!(
condition.evaluate(&actions, &time, true.into()),
TriggerState::None,
);
}
#[test]
fn none() {
let (mut world, mut state) = context::init_world();
let action1 = world
.spawn((Action::<Test>::new(), TriggerState::None))
.id();
let action2 = world
.spawn((Action::<Test>::new(), TriggerState::None))
.id();
let (time, actions) = state.get(&world).unwrap();
let mut condition = Chord::new([action1, action2]);
assert_eq!(
condition.evaluate(&actions, &time, true.into()),
TriggerState::None,
);
}
#[test]
fn missing_action() {
let (world, mut state) = context::init_world();
let (time, actions) = state.get(&world).unwrap();
let mut condition = Chord::single(Entity::PLACEHOLDER);
assert_eq!(
condition.evaluate(&actions, &time, true.into()),
TriggerState::None,
);
}
#[derive(InputAction)]
#[action_output(bool)]
struct Test;
}