use std::collections::{HashMap, HashSet};
use bevy_ecs::prelude::*;
use repose_core::input::{GamepadAxis, GamepadButton, PointerButton};
use repose_core::shortcuts::KeyChord;
use super::ActionLike;
use super::binding::Binding;
use super::map::ActionMap;
#[derive(Resource, Debug)]
pub struct ActionState<A: ActionLike> {
map: ActionMap<A>,
pressed: HashSet<A>,
just_pressed: HashSet<A>,
just_released: HashSet<A>,
consumed: HashSet<A>,
axes: HashMap<GamepadAxis, f32>,
active_contexts: HashSet<String>,
}
impl<A: ActionLike> ActionState<A> {
pub fn new(map: ActionMap<A>) -> Self {
Self {
map,
pressed: HashSet::new(),
just_pressed: HashSet::new(),
just_released: HashSet::new(),
consumed: HashSet::new(),
axes: HashMap::new(),
active_contexts: HashSet::new(),
}
}
pub fn map(&self) -> &ActionMap<A> {
&self.map
}
pub fn end_tick(&mut self) {
self.just_pressed.clear();
self.just_released.clear();
}
pub fn clear_edges(&mut self) {
self.end_tick();
}
pub fn set_contexts(&mut self, contexts: &[&str]) {
let next: HashSet<String> = contexts.iter().map(|s| s.to_string()).collect();
if next != self.active_contexts {
self.active_contexts = next;
self.just_pressed.clear();
self.just_released.clear();
}
}
fn live(&self, action: &A) -> bool {
self.map.live_in(action, &self.active_contexts) && !self.consumed.contains(action)
}
fn press(&mut self, action: &A) {
if self.pressed.insert(action.clone()) {
self.just_pressed.insert(action.clone());
}
}
fn release(&mut self, action: &A) {
if self.pressed.remove(action) {
self.just_released.insert(action.clone());
}
}
fn fire_binding(&mut self, binding: &Binding, down: bool) {
let actions: Vec<A> = self
.map
.actions()
.filter(|a| self.map.bindings_for(a).contains(binding))
.cloned()
.collect();
for action in actions {
if down {
self.press(&action);
} else {
self.release(&action);
}
}
}
pub fn key(&mut self, chord: &KeyChord, down: bool) {
self.fire_binding(&Binding::Key(chord.clone()), down);
}
pub fn mouse(&mut self, button: PointerButton, down: bool) {
self.fire_binding(&Binding::Mouse(button), down);
}
pub fn pad(&mut self, button: GamepadButton, down: bool) {
self.fire_binding(&Binding::Pad(button), down);
}
pub fn axis(&mut self, axis: GamepadAxis, value: f32) {
self.axes.insert(axis, value);
let flips: Vec<(A, bool)> = self
.map
.actions()
.flat_map(|a| {
self.map
.bindings_for(a)
.iter()
.filter_map(move |b| match b {
Binding::Axis {
axis: ba,
threshold,
} if *ba == axis => {
Some((a.clone(), Binding::axis_active(*threshold, value)))
}
_ => None,
})
})
.collect();
for (action, active) in flips {
if active {
self.press(&action);
} else {
self.release(&action);
}
}
}
pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
self.axes.get(&axis).copied().unwrap_or(0.0)
}
pub fn pressed(&self, action: &A) -> bool {
self.pressed.contains(action) && self.live(action)
}
pub fn just_pressed(&self, action: &A) -> bool {
self.just_pressed.contains(action) && self.live(action)
}
pub fn just_released(&self, action: &A) -> bool {
self.just_released.contains(action)
}
pub fn consume(&mut self, action: &A) -> bool {
let held = self.pressed.contains(action);
self.consumed.insert(action.clone());
held
}
pub fn consume_all(&mut self) {
self.consumed.extend(self.pressed.iter().cloned());
}
pub fn mock(&mut self, action: &A, down: bool) {
if down {
self.press(action);
} else {
self.release(action);
}
}
pub fn clear_consumed(&mut self) {
self.consumed.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
use repose_core::input::{Key, Modifiers};
fn jump_map() -> ActionMap<&'static str> {
let mut map = ActionMap::new();
map.bind(
"jump",
Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
);
map.bind("jump", Binding::Pad(GamepadButton::South));
map
}
fn space() -> KeyChord {
KeyChord::new(Key::Space, Modifiers::default())
}
#[test]
fn edges_clear_at_tick_end() {
let mut st = ActionState::new(jump_map());
st.key(&space(), true);
assert!(st.just_pressed(&"jump"));
assert!(st.pressed(&"jump"));
assert!(st.just_pressed(&"jump"));
st.end_tick();
assert!(!st.just_pressed(&"jump"));
assert!(st.pressed(&"jump"));
st.key(&space(), false);
assert!(st.just_released(&"jump"));
assert!(!st.pressed(&"jump"));
st.end_tick();
assert!(!st.just_released(&"jump"));
}
#[test]
fn pad_and_key_drive_same_action() {
let mut st = ActionState::new(jump_map());
st.pad(GamepadButton::South, true);
assert!(st.just_pressed(&"jump"));
st.end_tick();
st.pad(GamepadButton::South, false);
st.end_tick();
st.key(&space(), true);
assert!(st.just_pressed(&"jump"));
}
#[test]
fn axis_threshold_flips() {
let mut map = ActionMap::new();
map.bind(
"left",
Binding::Axis {
axis: GamepadAxis::LeftStickX,
threshold: -0.5,
},
);
let mut st = ActionState::new(map);
st.axis(GamepadAxis::LeftStickX, -0.2);
assert!(!st.pressed(&"left"));
st.axis(GamepadAxis::LeftStickX, -0.8);
assert!(st.just_pressed(&"left"));
assert_eq!(st.axis_value(GamepadAxis::LeftStickX), -0.8);
st.axis(GamepadAxis::LeftStickX, 0.0);
assert!(st.just_released(&"left"));
}
#[test]
fn consume_hides_from_late_readers() {
let mut st = ActionState::new(jump_map());
st.key(&space(), true);
assert!(st.consume(&"jump"));
assert!(!st.pressed(&"jump"));
assert!(!st.just_pressed(&"jump"));
st.clear_consumed();
assert!(st.pressed(&"jump"));
}
#[test]
fn context_gates_sim_action() {
let mut map = jump_map();
map.in_context("gameplay", "jump");
let mut st = ActionState::new(map);
st.set_contexts(&["menu"]);
st.key(&space(), true);
assert!(!st.pressed(&"jump"));
st.set_contexts(&["gameplay"]);
st.key(&space(), true);
assert!(st.pressed(&"jump"));
}
#[test]
fn context_switch_drops_pending_edges() {
let mut map = jump_map();
map.in_context("gameplay", "jump");
let mut st = ActionState::new(map);
st.set_contexts(&["gameplay"]);
st.key(&space(), true);
assert!(st.just_pressed(&"jump"));
st.set_contexts(&["menu"]);
assert!(!st.just_pressed(&"jump"));
assert!(!st.pressed(&"jump"));
st.key(&space(), false);
st.set_contexts(&["gameplay"]);
assert!(!st.just_released(&"jump"));
}
#[test]
fn mock_drives_without_hardware() {
let mut st = ActionState::new(jump_map());
st.mock(&"jump", true);
assert!(st.just_pressed(&"jump"));
st.mock(&"jump", false);
assert!(st.just_released(&"jump"));
}
}