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: HashSet<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: HashSet::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 self.down_buttons.insert(binding.clone());
98 } else {
99 self.down_buttons.remove(binding);
100 }
101 }
102 Binding::Axis { .. } => {}
103 }
104 let actions: Vec<A> = self
105 .map
106 .actions()
107 .filter(|a| self.map.bindings_for(a).contains(binding))
108 .cloned()
109 .collect();
110 for action in actions {
111 if self.action_down(&action) {
112 self.press(&action);
113 } else {
114 self.release(&action);
115 }
116 }
117 }
118
119 fn binding_down(&self, binding: &Binding) -> bool {
120 match binding {
121 Binding::Key(_) | Binding::Mouse(_) | Binding::Pad(_) => {
122 self.down_buttons.contains(binding)
123 }
124 Binding::Axis { axis, threshold } => {
125 Binding::axis_active(*threshold, self.axis_value(*axis))
126 }
127 }
128 }
129
130 fn action_down(&self, action: &A) -> bool {
131 self.map
132 .bindings_for(action)
133 .iter()
134 .any(|b| self.binding_down(b))
135 }
136
137 pub fn key(&mut self, chord: &KeyChord, down: bool) {
139 self.fire_binding(&Binding::Key(chord.clone()), down);
140 }
141
142 pub fn mouse(&mut self, button: PointerButton, down: bool) {
144 self.fire_binding(&Binding::Mouse(button), down);
145 }
146
147 pub fn pad(&mut self, button: GamepadButton, down: bool) {
149 self.fire_binding(&Binding::Pad(button), down);
150 }
151
152 pub fn axis(&mut self, axis: GamepadAxis, value: f32) {
156 self.axes.insert(axis, value);
157 let actions: Vec<A> = self
158 .map
159 .actions()
160 .filter(|a| {
161 self.map.bindings_for(a).iter().any(|b| match b {
162 Binding::Axis { axis: ba, .. } => *ba == axis,
163 _ => false,
164 })
165 })
166 .cloned()
167 .collect();
168 for action in actions {
169 if self.action_down(&action) {
170 self.press(&action);
171 } else {
172 self.release(&action);
173 }
174 }
175 }
176
177 pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
183 self.axes.get(&axis).copied().unwrap_or(0.0)
184 }
185
186 pub fn strength(&self, action: &A) -> f32 {
203 if !self.live(action) {
204 return 0.0;
205 }
206 let mut best = 0.0f32;
207 for b in self.map.bindings_for(action) {
208 let s = match b {
209 Binding::Key(_) | Binding::Mouse(_) | Binding::Pad(_) => {
210 if self.binding_down(b) {
211 1.0
212 } else {
213 0.0
214 }
215 }
216 Binding::Axis { axis, threshold } => {
217 let v = self.axis_value(*axis);
218 if !Binding::axis_active(*threshold, v) {
219 0.0
220 } else {
221 let t = threshold.abs().clamp(0.0, 0.95);
223 let a = v.abs();
224 if a <= t {
225 0.0
226 } else {
227 ((a - t) / (1.0 - t)).clamp(0.0, 1.0).max(1e-6)
228 }
229 }
230 }
231 };
232 best = best.max(s);
233 }
234 best.clamp(0.0, 1.0)
235 }
236
237 pub fn vector(&self, neg_x: &A, pos_x: &A, neg_y: &A, pos_y: &A, deadzone: f32) -> (f32, f32) {
260 let mut x = self.strength(pos_x) - self.strength(neg_x);
261 let mut y = self.strength(pos_y) - self.strength(neg_y);
262 let len = (x * x + y * y).sqrt();
263 if len < deadzone.max(0.0) {
264 return (0.0, 0.0);
265 }
266 if len > 1.0 {
267 x /= len;
268 y /= len;
269 }
270 (x, y)
271 }
272
273 pub fn pressed(&self, action: &A) -> bool {
283 self.pressed.contains(action) && self.live(action)
284 }
285
286 pub fn just_pressed(&self, action: &A) -> bool {
294 self.just_pressed.contains(action) && self.live(action)
295 }
296
297 pub fn just_released(&self, action: &A) -> bool {
305 self.just_released.contains(action) && self.live(action)
306 }
307
308 pub fn consume(&mut self, action: &A) -> bool {
311 let held = self.pressed.contains(action);
312 self.consumed.insert(action.clone());
313 held
314 }
315
316 pub fn consume_all(&mut self) {
317 self.consumed.extend(self.pressed.iter().cloned());
318 }
319
320 pub fn mock(&mut self, action: &A, down: bool) {
322 if down {
323 self.press(action);
324 } else {
325 self.release(action);
326 }
327 }
328
329 pub fn clear_consumed(&mut self) {
331 self.consumed.clear();
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use repose_core::input::{Key, Modifiers};
339
340 fn jump_map() -> ActionMap<&'static str> {
341 let mut map = ActionMap::new();
342 map.bind(
343 "jump",
344 Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
345 );
346 map.bind("jump", Binding::Pad(GamepadButton::South));
347 map
348 }
349
350 fn space() -> KeyChord {
351 KeyChord::new(Key::Space, Modifiers::default())
352 }
353
354 #[test]
355 fn edges_clear_at_tick_end() {
356 let mut st = ActionState::new(jump_map());
357 st.key(&space(), true);
358 assert!(st.just_pressed(&"jump"));
359 assert!(st.pressed(&"jump"));
360 assert!(st.just_pressed(&"jump"));
362 st.end_tick();
363 assert!(!st.just_pressed(&"jump"));
364 assert!(st.pressed(&"jump"));
365 st.key(&space(), false);
366 assert!(st.just_released(&"jump"));
367 assert!(!st.pressed(&"jump"));
368 st.end_tick();
369 assert!(!st.just_released(&"jump"));
370 }
371
372 #[test]
373 fn pad_and_key_drive_same_action() {
374 let mut st = ActionState::new(jump_map());
375 st.pad(GamepadButton::South, true);
376 assert!(st.just_pressed(&"jump"));
377 st.end_tick();
378 st.pad(GamepadButton::South, false);
379 st.end_tick();
380 st.key(&space(), true);
381 assert!(st.just_pressed(&"jump"));
382 }
383
384 #[test]
385 fn axis_threshold_flips() {
386 let mut map = ActionMap::new();
387 map.bind(
388 "left",
389 Binding::Axis {
390 axis: GamepadAxis::LeftStickX,
391 threshold: -0.5,
392 },
393 );
394 let mut st = ActionState::new(map);
395 st.axis(GamepadAxis::LeftStickX, -0.2);
396 assert!(!st.pressed(&"left"));
397 st.axis(GamepadAxis::LeftStickX, -0.8);
398 assert!(st.just_pressed(&"left"));
399 assert_eq!(st.axis_value(GamepadAxis::LeftStickX), -0.8);
400 st.axis(GamepadAxis::LeftStickX, 0.0);
401 assert!(st.just_released(&"left"));
402 }
403
404 #[test]
405 fn consume_hides_from_late_readers() {
406 let mut st = ActionState::new(jump_map());
407 st.key(&space(), true);
408 assert!(st.consume(&"jump"));
409 assert!(!st.pressed(&"jump"));
410 assert!(!st.just_pressed(&"jump"));
411 st.clear_consumed();
412 assert!(st.pressed(&"jump"));
413 }
414
415 #[test]
416 fn context_gates_sim_action() {
417 let mut map = jump_map();
418 map.in_context("gameplay", "jump");
419 let mut st = ActionState::new(map);
420 st.set_contexts(&["menu"]);
421 st.key(&space(), true);
422 assert!(!st.pressed(&"jump"));
423 st.set_contexts(&["gameplay"]);
424 st.key(&space(), true);
425 assert!(st.pressed(&"jump"));
426 }
427
428 #[test]
429 fn context_switch_drops_pending_edges() {
430 let mut map = jump_map();
431 map.in_context("gameplay", "jump");
432 let mut st = ActionState::new(map);
433 st.set_contexts(&["gameplay"]);
434 st.key(&space(), true);
435 assert!(st.just_pressed(&"jump"));
436 st.set_contexts(&["menu"]);
437 assert!(!st.just_pressed(&"jump"));
438 assert!(!st.pressed(&"jump"));
439 st.key(&space(), false);
440 st.set_contexts(&["gameplay"]);
441 assert!(!st.just_released(&"jump"));
442 }
443
444 #[test]
445 fn mock_drives_without_hardware() {
446 let mut st = ActionState::new(jump_map());
447 st.mock(&"jump", true);
448 assert!(st.just_pressed(&"jump"));
449 st.mock(&"jump", false);
450 assert!(st.just_released(&"jump"));
451 }
452
453 #[test]
454 fn multi_binding_release_is_or() {
455 let mut st = ActionState::new(jump_map());
456 st.key(&space(), true);
457 st.pad(GamepadButton::South, true);
458 assert!(st.pressed(&"jump"));
459 st.end_tick();
460 st.key(&space(), false);
461 assert!(st.pressed(&"jump"), "South still held");
462 assert!(!st.just_released(&"jump"), "no release while OR-held");
463 st.pad(GamepadButton::South, false);
464 assert!(!st.pressed(&"jump"));
465 assert!(st.just_released(&"jump"));
466 }
467
468 #[test]
469 fn consume_hides_release_too() {
470 let mut st = ActionState::new(jump_map());
471 st.key(&space(), true);
472 st.end_tick();
473 st.key(&space(), false);
474 assert!(st.just_released(&"jump"));
475 st.consume(&"jump");
476 assert!(!st.just_released(&"jump"));
477 }
478
479 #[test]
480 fn strength_and_vector_behaviour() {
481 use repose_core::input::Key;
482 let mut map = ActionMap::new();
483 map.bind(
484 "right",
485 Binding::Axis {
486 axis: GamepadAxis::LeftStickX,
487 threshold: 0.2,
488 },
489 );
490 map.bind(
491 "left",
492 Binding::Axis {
493 axis: GamepadAxis::LeftStickX,
494 threshold: -0.2,
495 },
496 );
497 map.bind(
498 "jump",
499 Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
500 );
501 let mut st = ActionState::new(map);
502 assert_eq!(st.strength(&"right"), 0.0);
504 assert_eq!(st.vector(&"left", &"right", &"jump", &"jump", 0.2), (0.0, 0.0));
505 st.axis(GamepadAxis::LeftStickX, 0.5);
507 assert!((st.strength(&"right") - 0.375).abs() < 1e-6);
508 assert_eq!(st.strength(&"left"), 0.0);
509 let (x, y) = st.vector(&"left", &"right", &"jump", &"jump", 0.2);
510 assert!((x - 0.375).abs() < 1e-6 && y.abs() < 1e-6);
511 assert_eq!(st.vector(&"left", &"right", &"jump", &"jump", 0.6), (0.0, 0.0));
513 st.axis(GamepadAxis::LeftStickX, 0.1);
515 assert_eq!(st.strength(&"right"), 0.0);
516 st.key(&space(), true);
518 assert_eq!(st.strength(&"jump"), 1.0);
519 }
520}