1use std::collections::{HashMap, HashSet};
4
5use bevy_ecs::prelude::*;
6use repose_core::input::{GamepadAxis, GamepadButton, PointerButton};
7use repose_core::shortcuts::KeyChord;
8
9use super::ActionLike;
10use super::binding::Binding;
11use super::map::ActionMap;
12
13#[derive(Resource, Debug)]
20pub struct ActionState<A: ActionLike> {
21 map: ActionMap<A>,
22 pressed: HashSet<A>,
23 just_pressed: HashSet<A>,
24 just_released: HashSet<A>,
25 consumed: HashSet<A>,
26 axes: HashMap<GamepadAxis, f32>,
27 active_contexts: HashSet<String>,
28 down_buttons: Vec<Binding>,
31}
32
33impl<A: ActionLike> ActionState<A> {
34 pub fn new(map: ActionMap<A>) -> Self {
35 Self {
36 map,
37 pressed: HashSet::new(),
38 just_pressed: HashSet::new(),
39 just_released: HashSet::new(),
40 consumed: HashSet::new(),
41 axes: HashMap::new(),
42 active_contexts: HashSet::new(),
43 down_buttons: Vec::new(),
44 }
45 }
46
47 pub fn map(&self) -> &ActionMap<A> {
48 &self.map
49 }
50
51 pub fn end_tick(&mut self) {
54 self.just_pressed.clear();
55 self.just_released.clear();
56 }
57
58 pub fn clear_edges(&mut self) {
60 self.end_tick();
61 }
62
63 pub fn set_contexts(&mut self, contexts: &[&str]) {
69 let next: HashSet<String> = contexts.iter().map(|s| s.to_string()).collect();
70 if next != self.active_contexts {
71 self.active_contexts = next;
72 self.just_pressed.clear();
73 self.just_released.clear();
74 }
75 }
76
77 fn live(&self, action: &A) -> bool {
78 self.map.live_in(action, &self.active_contexts) && !self.consumed.contains(action)
79 }
80
81 fn press(&mut self, action: &A) {
82 if self.pressed.insert(action.clone()) {
83 self.just_pressed.insert(action.clone());
84 }
85 }
86
87 fn release(&mut self, action: &A) {
88 if self.pressed.remove(action) {
89 self.just_released.insert(action.clone());
90 }
91 }
92
93 fn fire_binding(&mut self, binding: &Binding, down: bool) {
94 match binding {
95 Binding::Key(_) | Binding::Mouse(_) | Binding::Pad(_) => {
96 if down {
97 if !self.down_buttons.iter().any(|b| b == binding) {
98 self.down_buttons.push(binding.clone());
99 }
100 } else {
101 self.down_buttons.retain(|b| b != binding);
102 }
103 }
104 Binding::Axis { .. } => {}
105 }
106 let actions: Vec<A> = self
107 .map
108 .actions()
109 .filter(|a| self.map.bindings_for(a).contains(binding))
110 .cloned()
111 .collect();
112 for action in actions {
113 if self.action_down(&action) {
114 self.press(&action);
115 } else {
116 self.release(&action);
117 }
118 }
119 }
120
121 fn binding_down(&self, binding: &Binding) -> bool {
122 match binding {
123 Binding::Key(_) | Binding::Mouse(_) | Binding::Pad(_) => {
124 self.down_buttons.iter().any(|b| b == binding)
125 }
126 Binding::Axis { axis, threshold } => {
127 Binding::axis_active(*threshold, self.axis_value(*axis))
128 }
129 }
130 }
131
132 fn action_down(&self, action: &A) -> bool {
133 self.map
134 .bindings_for(action)
135 .iter()
136 .any(|b| self.binding_down(b))
137 }
138
139 pub fn key(&mut self, chord: &KeyChord, down: bool) {
141 self.fire_binding(&Binding::Key(chord.clone()), down);
142 }
143
144 pub fn mouse(&mut self, button: PointerButton, down: bool) {
146 self.fire_binding(&Binding::Mouse(button), down);
147 }
148
149 pub fn pad(&mut self, button: GamepadButton, down: bool) {
151 self.fire_binding(&Binding::Pad(button), down);
152 }
153
154 pub fn axis(&mut self, axis: GamepadAxis, value: f32) {
158 self.axes.insert(axis, value);
159 let actions: Vec<A> = self
160 .map
161 .actions()
162 .filter(|a| {
163 self.map.bindings_for(a).iter().any(|b| match b {
164 Binding::Axis { axis: ba, .. } => *ba == axis,
165 _ => false,
166 })
167 })
168 .cloned()
169 .collect();
170 for action in actions {
171 if self.action_down(&action) {
172 self.press(&action);
173 } else {
174 self.release(&action);
175 }
176 }
177 }
178
179 pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
185 self.axes.get(&axis).copied().unwrap_or(0.0)
186 }
187
188 pub fn strength(&self, action: &A) -> f32 {
204 if !self.live(action) {
205 return 0.0;
206 }
207 let mut best = 0.0f32;
208 for b in self.map.bindings_for(action) {
209 let s = match b {
210 Binding::Key(_) | Binding::Mouse(_) | Binding::Pad(_) => {
211 if self.binding_down(b) {
212 1.0
213 } else {
214 0.0
215 }
216 }
217 Binding::Axis { axis, threshold } => {
218 let v = self.axis_value(*axis);
219 if Binding::axis_active(*threshold, v) {
220 v.abs().clamp(0.0, 1.0).max(1e-6)
221 } else {
222 0.0
223 }
224 }
225 };
226 best = best.max(s);
227 }
228 best.clamp(0.0, 1.0)
229 }
230
231 pub fn vector(&self, neg_x: &A, pos_x: &A, neg_y: &A, pos_y: &A, deadzone: f32) -> (f32, f32) {
254 let mut x = self.strength(pos_x) - self.strength(neg_x);
255 let mut y = self.strength(pos_y) - self.strength(neg_y);
256 let len = (x * x + y * y).sqrt();
257 if len < deadzone.max(0.0) {
258 return (0.0, 0.0);
259 }
260 if len > 1.0 {
261 x /= len;
262 y /= len;
263 }
264 (x, y)
265 }
266
267 pub fn pressed(&self, action: &A) -> bool {
277 self.pressed.contains(action) && self.live(action)
278 }
279
280 pub fn just_pressed(&self, action: &A) -> bool {
288 self.just_pressed.contains(action) && self.live(action)
289 }
290
291 pub fn just_released(&self, action: &A) -> bool {
299 self.just_released.contains(action) && self.live(action)
300 }
301
302 pub fn consume(&mut self, action: &A) -> bool {
305 let held = self.pressed.contains(action);
306 self.consumed.insert(action.clone());
307 held
308 }
309
310 pub fn consume_all(&mut self) {
311 self.consumed.extend(self.pressed.iter().cloned());
312 }
313
314 pub fn mock(&mut self, action: &A, down: bool) {
316 if down {
317 self.press(action);
318 } else {
319 self.release(action);
320 }
321 }
322
323 pub fn clear_consumed(&mut self) {
325 self.consumed.clear();
326 }
327}
328
329#[cfg(test)]
330mod tests {
331 use super::*;
332 use repose_core::input::{Key, Modifiers};
333
334 fn jump_map() -> ActionMap<&'static str> {
335 let mut map = ActionMap::new();
336 map.bind(
337 "jump",
338 Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
339 );
340 map.bind("jump", Binding::Pad(GamepadButton::South));
341 map
342 }
343
344 fn space() -> KeyChord {
345 KeyChord::new(Key::Space, Modifiers::default())
346 }
347
348 #[test]
349 fn edges_clear_at_tick_end() {
350 let mut st = ActionState::new(jump_map());
351 st.key(&space(), true);
352 assert!(st.just_pressed(&"jump"));
353 assert!(st.pressed(&"jump"));
354 assert!(st.just_pressed(&"jump"));
356 st.end_tick();
357 assert!(!st.just_pressed(&"jump"));
358 assert!(st.pressed(&"jump"));
359 st.key(&space(), false);
360 assert!(st.just_released(&"jump"));
361 assert!(!st.pressed(&"jump"));
362 st.end_tick();
363 assert!(!st.just_released(&"jump"));
364 }
365
366 #[test]
367 fn pad_and_key_drive_same_action() {
368 let mut st = ActionState::new(jump_map());
369 st.pad(GamepadButton::South, true);
370 assert!(st.just_pressed(&"jump"));
371 st.end_tick();
372 st.pad(GamepadButton::South, false);
373 st.end_tick();
374 st.key(&space(), true);
375 assert!(st.just_pressed(&"jump"));
376 }
377
378 #[test]
379 fn axis_threshold_flips() {
380 let mut map = ActionMap::new();
381 map.bind(
382 "left",
383 Binding::Axis {
384 axis: GamepadAxis::LeftStickX,
385 threshold: -0.5,
386 },
387 );
388 let mut st = ActionState::new(map);
389 st.axis(GamepadAxis::LeftStickX, -0.2);
390 assert!(!st.pressed(&"left"));
391 st.axis(GamepadAxis::LeftStickX, -0.8);
392 assert!(st.just_pressed(&"left"));
393 assert_eq!(st.axis_value(GamepadAxis::LeftStickX), -0.8);
394 st.axis(GamepadAxis::LeftStickX, 0.0);
395 assert!(st.just_released(&"left"));
396 }
397
398 #[test]
399 fn consume_hides_from_late_readers() {
400 let mut st = ActionState::new(jump_map());
401 st.key(&space(), true);
402 assert!(st.consume(&"jump"));
403 assert!(!st.pressed(&"jump"));
404 assert!(!st.just_pressed(&"jump"));
405 st.clear_consumed();
406 assert!(st.pressed(&"jump"));
407 }
408
409 #[test]
410 fn context_gates_sim_action() {
411 let mut map = jump_map();
412 map.in_context("gameplay", "jump");
413 let mut st = ActionState::new(map);
414 st.set_contexts(&["menu"]);
415 st.key(&space(), true);
416 assert!(!st.pressed(&"jump"));
417 st.set_contexts(&["gameplay"]);
418 st.key(&space(), true);
419 assert!(st.pressed(&"jump"));
420 }
421
422 #[test]
423 fn context_switch_drops_pending_edges() {
424 let mut map = jump_map();
425 map.in_context("gameplay", "jump");
426 let mut st = ActionState::new(map);
427 st.set_contexts(&["gameplay"]);
428 st.key(&space(), true);
429 assert!(st.just_pressed(&"jump"));
430 st.set_contexts(&["menu"]);
431 assert!(!st.just_pressed(&"jump"));
432 assert!(!st.pressed(&"jump"));
433 st.key(&space(), false);
434 st.set_contexts(&["gameplay"]);
435 assert!(!st.just_released(&"jump"));
436 }
437
438 #[test]
439 fn mock_drives_without_hardware() {
440 let mut st = ActionState::new(jump_map());
441 st.mock(&"jump", true);
442 assert!(st.just_pressed(&"jump"));
443 st.mock(&"jump", false);
444 assert!(st.just_released(&"jump"));
445 }
446
447 #[test]
448 fn multi_binding_release_is_or() {
449 let mut st = ActionState::new(jump_map());
450 st.key(&space(), true);
451 st.pad(GamepadButton::South, true);
452 assert!(st.pressed(&"jump"));
453 st.end_tick();
454 st.key(&space(), false);
455 assert!(st.pressed(&"jump"), "South still held");
456 assert!(!st.just_released(&"jump"), "no release while OR-held");
457 st.pad(GamepadButton::South, false);
458 assert!(!st.pressed(&"jump"));
459 assert!(st.just_released(&"jump"));
460 }
461
462 #[test]
463 fn consume_hides_release_too() {
464 let mut st = ActionState::new(jump_map());
465 st.key(&space(), true);
466 st.end_tick();
467 st.key(&space(), false);
468 assert!(st.just_released(&"jump"));
469 st.consume(&"jump");
470 assert!(!st.just_released(&"jump"));
471 }
472
473 #[test]
474 fn strength_and_vector_behaviour() {
475 use repose_core::input::Key;
476 let mut map = ActionMap::new();
477 map.bind(
478 "right",
479 Binding::Axis {
480 axis: GamepadAxis::LeftStickX,
481 threshold: 0.2,
482 },
483 );
484 map.bind(
485 "left",
486 Binding::Axis {
487 axis: GamepadAxis::LeftStickX,
488 threshold: -0.2,
489 },
490 );
491 map.bind(
492 "jump",
493 Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
494 );
495 let mut st = ActionState::new(map);
496 assert_eq!(st.strength(&"right"), 0.0);
498 assert_eq!(st.vector(&"left", &"right", &"jump", &"jump", 0.2), (0.0, 0.0));
499 st.axis(GamepadAxis::LeftStickX, 0.5);
501 assert!((st.strength(&"right") - 0.5).abs() < 1e-6);
502 assert_eq!(st.strength(&"left"), 0.0);
503 let (x, y) = st.vector(&"left", &"right", &"jump", &"jump", 0.2);
504 assert!((x - 0.5).abs() < 1e-6 && y.abs() < 1e-6);
505 assert_eq!(st.vector(&"left", &"right", &"jump", &"jump", 0.6), (0.0, 0.0));
507 st.axis(GamepadAxis::LeftStickX, 0.1);
509 assert_eq!(st.strength(&"right"), 0.0);
510 st.key(&space(), true);
512 assert_eq!(st.strength(&"jump"), 1.0);
513 }
514}