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