Skip to main content

repame_input/
state.rs

1//! Per-tick action state: levels, edges, consumption, mocking.
2
3use std::collections::{HashMap, HashSet};
4
5use bevy_ecs::prelude::*;
6use repose_core::input::{GamepadAxis, GamepadButton, PhysicalKey, PointerButton};
7use repose_core::shortcuts::KeyChord;
8
9use super::ActionLike;
10use super::binding::Binding;
11use super::map::ActionMap;
12
13/// Sim-side gameplay input fed from repose events, read per tick.
14/// Edges clear at tick end via `end_tick` (registered last).
15/// Frame-scope readers call `clear_edges` after consuming.
16#[derive(Resource, Debug)]
17pub struct ActionState<A: ActionLike> {
18    map: ActionMap<A>,
19    pressed: HashSet<A>,
20    just_pressed: HashSet<A>,
21    just_released: HashSet<A>,
22    consumed: HashSet<A>,
23    axes: HashMap<GamepadAxis, f32>,
24    active_contexts: HashSet<String>,
25    /// Held button bindings. Releases drop the action when no binding stays down.
26    down_buttons: HashSet<Binding>,
27}
28
29impl<A: ActionLike> ActionState<A> {
30    pub fn new(map: ActionMap<A>) -> Self {
31        Self {
32            map,
33            pressed: HashSet::new(),
34            just_pressed: HashSet::new(),
35            just_released: HashSet::new(),
36            consumed: HashSet::new(),
37            axes: HashMap::new(),
38            active_contexts: HashSet::new(),
39            down_buttons: HashSet::new(),
40        }
41    }
42
43    pub fn map(&self) -> &ActionMap<A> {
44        &self.map
45    }
46
47    pub fn replace_map(&mut self, map: ActionMap<A>) {
48        self.map = map;
49        let live: Vec<Binding> = self
50            .map
51            .actions()
52            .flat_map(|a| self.map.bindings_for(a).iter().cloned())
53            .collect();
54        self.down_buttons.retain(|b| live.contains(b));
55        self.pressed.retain(|a| !self.map.bindings_for(a).is_empty());
56        self.just_pressed
57            .retain(|a| !self.map.bindings_for(a).is_empty());
58        self.just_released.clear();
59    }
60
61    /// Edge rollover for tick end: edges clear, levels persist.
62    pub fn end_tick(&mut self) {
63        self.just_pressed.clear();
64        self.just_released.clear();
65        self.consumed.clear();
66    }
67
68    /// Clear edges outside the schedule.
69    pub fn clear_edges(&mut self) {
70        self.end_tick();
71    }
72
73    /// Switch the active context set. Pending edges drop on change;
74    /// re-setting the same set keeps them. Levels track hardware.
75    pub fn set_contexts(&mut self, contexts: &[&str]) {
76        let next: HashSet<String> = contexts.iter().map(|s| s.to_string()).collect();
77        if next != self.active_contexts {
78            self.active_contexts = next;
79            self.just_pressed.clear();
80            self.just_released.clear();
81        }
82    }
83
84    fn live(&self, action: &A) -> bool {
85        self.map.live_in(action, &self.active_contexts) && !self.consumed.contains(action)
86    }
87
88    fn press(&mut self, action: &A) {
89        if self.pressed.insert(action.clone()) {
90            self.just_pressed.insert(action.clone());
91        }
92    }
93
94    fn release(&mut self, action: &A) {
95        if self.pressed.remove(action) {
96            self.just_released.insert(action.clone());
97        }
98    }
99
100    fn fire_binding(&mut self, binding: &Binding, down: bool) {
101        match binding {
102            Binding::Key(_) | Binding::Physical(_) | Binding::Mouse(_) | Binding::Pad(_) => {
103                if down {
104                    self.down_buttons.insert(binding.clone());
105                } else {
106                    self.down_buttons.remove(binding);
107                }
108            }
109            Binding::Axis { .. } => {}
110        }
111        let actions: Vec<A> = self
112            .map
113            .actions()
114            .filter(|a| self.map.bindings_for(a).contains(binding))
115            .cloned()
116            .collect();
117        for action in actions {
118            if self.action_down(&action) {
119                self.press(&action);
120            } else {
121                self.release(&action);
122            }
123        }
124    }
125
126    fn binding_down(&self, binding: &Binding) -> bool {
127        match binding {
128            Binding::Key(_) | Binding::Physical(_) | Binding::Mouse(_) | Binding::Pad(_) => {
129                self.down_buttons.contains(binding)
130            }
131            Binding::Axis { axis, threshold } => {
132                Binding::axis_active(*threshold, self.axis_value(*axis))
133            }
134        }
135    }
136
137    fn action_down(&self, action: &A) -> bool {
138        self.map
139            .bindings_for(action)
140            .iter()
141            .any(|b| self.binding_down(b))
142    }
143
144    /// Feed a keyboard chord press/release.
145    pub fn key(&mut self, chord: &KeyChord, down: bool) {
146        self.fire_binding(&Binding::Key(chord.clone()), down);
147    }
148
149    /// Feed a physical key position press/release.
150    pub fn physical(&mut self, key: PhysicalKey, down: bool) {
151        self.fire_binding(&Binding::Physical(key), down);
152    }
153
154    /// Feed a mouse button press/release.
155    pub fn mouse(&mut self, button: PointerButton, down: bool) {
156        self.fire_binding(&Binding::Mouse(button), down);
157    }
158
159    /// Feed a gamepad button press/release.
160    pub fn pad(&mut self, button: GamepadButton, down: bool) {
161        self.fire_binding(&Binding::Pad(button), down);
162    }
163
164    /// Feed an axis value; threshold bindings flip on crossing.
165    pub fn axis(&mut self, axis: GamepadAxis, value: f32) {
166        self.axes.insert(axis, value);
167        let actions: Vec<A> = self
168            .map
169            .actions()
170            .filter(|a| {
171                self.map.bindings_for(a).iter().any(|b| match b {
172                    Binding::Axis { axis: ba, .. } => *ba == axis,
173                    _ => false,
174                })
175            })
176            .cloned()
177            .collect();
178        for action in actions {
179            if self.action_down(&action) {
180                self.press(&action);
181            } else {
182                self.release(&action);
183            }
184        }
185    }
186
187    /// Latest raw value fed for an axis (`0.0` if not fed).
188    ///
189    /// Raw deflection in `-1..1` (sticks) or `0..1` (triggers), no threshold applied.
190    pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
191        self.axes.get(&axis).copied().unwrap_or(0.0)
192    }
193
194    /// Intensity from `0.0` (inactive) to `1.0` (held). Buttons read `0`/`1`;
195    /// axes remap `threshold..1` to `0..1`. Strongest binding wins.
196    /// Gated or consumed actions read `0.0`.
197    pub fn strength(&self, action: &A) -> f32 {
198        if !self.live(action) {
199            return 0.0;
200        }
201        let mut best = 0.0f32;
202        for b in self.map.bindings_for(action) {
203            let s =
204                match b {
205                    Binding::Key(_) | Binding::Physical(_) | Binding::Mouse(_) | Binding::Pad(_) => {
206                        if self.binding_down(b) { 1.0 } else { 0.0 }
207                    }
208                    Binding::Axis { axis, threshold } => {
209                        let v = self.axis_value(*axis);
210                        if !Binding::axis_active(*threshold, v) {
211                            0.0
212                        } else {
213                            // Radial deadzone remap t..1 to 0..1.
214                            let t = threshold.abs().clamp(0.0, 0.95);
215                            let a = v.abs();
216                            if a <= t {
217                                0.0
218                            } else {
219                                ((a - t) / (1.0 - t)).clamp(0.0, 1.0).max(1e-6)
220                            }
221                        }
222                    }
223                };
224            best = best.max(s);
225        }
226        best.clamp(0.0, 1.0)
227    }
228
229    /// Movement vector from four actions. Lengths below `deadzone` snap to zero;
230    /// lengths above `1` normalize back. `y` is screen convention (down-positive).
231    pub fn vector(&self, neg_x: &A, pos_x: &A, neg_y: &A, pos_y: &A, deadzone: f32) -> (f32, f32) {
232        let mut x = self.strength(pos_x) - self.strength(neg_x);
233        let mut y = self.strength(pos_y) - self.strength(neg_y);
234        let len = (x * x + y * y).sqrt();
235        if len < deadzone.max(0.0) {
236            return (0.0, 0.0);
237        }
238        if len > 1.0 {
239            x /= len;
240            y /= len;
241        }
242        (x, y)
243    }
244
245    /// Held, live, and not consumed. True each tick a binding is down.
246    pub fn pressed(&self, action: &A) -> bool {
247        self.pressed.contains(action) && self.live(action)
248    }
249
250    /// Started this tick. Edge set on press, cleared at `end_tick`.
251    pub fn just_pressed(&self, action: &A) -> bool {
252        self.just_pressed.contains(action) && self.live(action)
253    }
254
255    /// Released this tick. Clears at `end_tick` like `just_pressed`.
256    pub fn just_released(&self, action: &A) -> bool {
257        self.just_released.contains(action) && self.live(action)
258    }
259
260    /// Mark consumed so later readers skip it. Returns true while held.
261    pub fn consume(&mut self, action: &A) -> bool {
262        let held = self.pressed.contains(action);
263        self.consumed.insert(action.clone());
264        held
265    }
266
267    pub fn consume_all(&mut self) {
268        self.consumed.extend(self.pressed.iter().cloned());
269    }
270
271    /// Drive an action without hardware.
272    pub fn mock(&mut self, action: &A, down: bool) {
273        if down {
274            self.press(action);
275        } else {
276            self.release(action);
277        }
278    }
279
280    /// Clear consumption.
281    pub fn clear_consumed(&mut self) {
282        self.consumed.clear();
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289    use repose_core::input::{Key, Modifiers};
290
291    fn jump_map() -> ActionMap<&'static str> {
292        let mut map = ActionMap::new();
293        map.bind(
294            "jump",
295            Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
296        );
297        map.bind("jump", Binding::Pad(GamepadButton::South));
298        map
299    }
300
301    fn space() -> KeyChord {
302        KeyChord::new(Key::Space, Modifiers::default())
303    }
304
305    #[test]
306    fn edges_clear_at_tick_end() {
307        let mut st = ActionState::new(jump_map());
308        st.key(&space(), true);
309        assert!(st.just_pressed(&"jump"));
310        assert!(st.pressed(&"jump"), "South still held");
311        // Edges persist until tick end; systems read, then clear.
312        assert!(st.just_pressed(&"jump"));
313        st.end_tick();
314        assert!(!st.just_pressed(&"jump"));
315        assert!(st.pressed(&"jump"), "South still held");
316        st.key(&space(), false);
317        assert!(st.just_released(&"jump"));
318        assert!(!st.pressed(&"jump"));
319        st.end_tick();
320        assert!(!st.just_released(&"jump"), "no release while OR-held");
321    }
322
323    #[test]
324    fn pad_and_key_drive_same_action() {
325        let mut st = ActionState::new(jump_map());
326        st.pad(GamepadButton::South, true);
327        assert!(st.just_pressed(&"jump"));
328        st.end_tick();
329        st.pad(GamepadButton::South, false);
330        st.end_tick();
331        st.key(&space(), true);
332        assert!(st.just_pressed(&"jump"));
333    }
334
335    #[test]
336    fn axis_threshold_flips() {
337        let mut map = ActionMap::new();
338        map.bind(
339            "left",
340            Binding::Axis {
341                axis: GamepadAxis::LeftStickX,
342                threshold: -0.5,
343            },
344        );
345        let mut st = ActionState::new(map);
346        st.axis(GamepadAxis::LeftStickX, -0.2);
347        assert!(!st.pressed(&"left"));
348        st.axis(GamepadAxis::LeftStickX, -0.8);
349        assert!(st.just_pressed(&"left"));
350        assert_eq!(st.axis_value(GamepadAxis::LeftStickX), -0.8);
351        st.axis(GamepadAxis::LeftStickX, 0.0);
352        assert!(st.just_released(&"left"));
353    }
354
355    #[test]
356    fn consume_hides_from_late_readers() {
357        let mut st = ActionState::new(jump_map());
358        st.key(&space(), true);
359        assert!(st.consume(&"jump"));
360        assert!(!st.pressed(&"jump"));
361        assert!(!st.just_pressed(&"jump"));
362        st.clear_consumed();
363        assert!(st.pressed(&"jump"), "South still held");
364    }
365
366    #[test]
367    fn tick_end_clears_consumption() {
368        let mut st = ActionState::new(jump_map());
369        st.key(&space(), true);
370        assert!(st.consume(&"jump"));
371        assert!(!st.pressed(&"jump"));
372        st.end_tick();
373        assert!(st.pressed(&"jump"), "consume must not leak past tick end");
374    }
375
376    #[test]
377    fn context_gates_sim_action() {
378        let mut map = jump_map();
379        map.in_context("gameplay", "jump");
380        let mut st = ActionState::new(map);
381        st.set_contexts(&["menu"]);
382        st.key(&space(), true);
383        assert!(!st.pressed(&"jump"));
384        st.set_contexts(&["gameplay"]);
385        st.key(&space(), true);
386        assert!(st.pressed(&"jump"), "South still held");
387    }
388
389    #[test]
390    fn context_switch_drops_pending_edges() {
391        let mut map = jump_map();
392        map.in_context("gameplay", "jump");
393        let mut st = ActionState::new(map);
394        st.set_contexts(&["gameplay"]);
395        st.key(&space(), true);
396        assert!(st.just_pressed(&"jump"));
397        st.set_contexts(&["menu"]);
398        assert!(!st.just_pressed(&"jump"));
399        assert!(!st.pressed(&"jump"));
400        st.key(&space(), false);
401        st.set_contexts(&["gameplay"]);
402        assert!(!st.just_released(&"jump"), "no release while OR-held");
403    }
404
405    #[test]
406    fn mock_drives_without_hardware() {
407        let mut st = ActionState::new(jump_map());
408        st.mock(&"jump", true);
409        assert!(st.just_pressed(&"jump"));
410        st.mock(&"jump", false);
411        assert!(st.just_released(&"jump"));
412    }
413
414    #[test]
415    fn multi_binding_release_is_or() {
416        let mut st = ActionState::new(jump_map());
417        st.key(&space(), true);
418        st.pad(GamepadButton::South, true);
419        assert!(st.pressed(&"jump"), "South still held");
420        st.end_tick();
421        st.key(&space(), false);
422        assert!(st.pressed(&"jump"), "South still held");
423        assert!(!st.just_released(&"jump"), "no release while OR-held");
424        st.pad(GamepadButton::South, false);
425        assert!(!st.pressed(&"jump"));
426        assert!(st.just_released(&"jump"));
427    }
428
429    #[test]
430    fn consume_hides_release_too() {
431        let mut st = ActionState::new(jump_map());
432        st.key(&space(), true);
433        st.end_tick();
434        st.key(&space(), false);
435        assert!(st.just_released(&"jump"));
436        st.consume(&"jump");
437        assert!(!st.just_released(&"jump"), "no release while OR-held");
438    }
439
440    #[test]
441    fn strength_and_vector_behaviour() {
442        use repose_core::input::Key;
443        let mut map = ActionMap::new();
444        map.bind(
445            "right",
446            Binding::Axis {
447                axis: GamepadAxis::LeftStickX,
448                threshold: 0.2,
449            },
450        );
451        map.bind(
452            "left",
453            Binding::Axis {
454                axis: GamepadAxis::LeftStickX,
455                threshold: -0.2,
456            },
457        );
458        map.bind(
459            "jump",
460            Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
461        );
462        let mut st = ActionState::new(map);
463        // Rest reads zero.
464        assert_eq!(st.strength(&"right"), 0.0);
465        assert_eq!(
466            st.vector(&"left", &"right", &"jump", &"jump", 0.2),
467            (0.0, 0.0)
468        );
469        // Half deflection right, deadzone remapped.
470        st.axis(GamepadAxis::LeftStickX, 0.5);
471        assert!((st.strength(&"right") - 0.375).abs() < 1e-6);
472        assert_eq!(st.strength(&"left"), 0.0);
473        let (x, y) = st.vector(&"left", &"right", &"jump", &"jump", 0.2);
474        assert!((x - 0.375).abs() < 1e-6 && y.abs() < 1e-6);
475        // Deadzone snaps small sticks to zero.
476        assert_eq!(
477            st.vector(&"left", &"right", &"jump", &"jump", 0.6),
478            (0.0, 0.0)
479        );
480        // Below threshold the strength reads zero.
481        st.axis(GamepadAxis::LeftStickX, 0.1);
482        assert_eq!(st.strength(&"right"), 0.0);
483        // Buttons read full strength.
484        st.key(&space(), true);
485        assert_eq!(st.strength(&"jump"), 1.0);
486    }
487}