1use 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#[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 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 pub fn end_tick(&mut self) {
64 self.just_pressed.clear();
65 self.just_released.clear();
66 self.consumed.clear();
67 }
68
69 pub fn clear_edges(&mut self) {
71 self.end_tick();
72 }
73
74 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 pub fn key(&mut self, chord: &KeyChord, down: bool) {
147 self.fire_binding(&Binding::Key(chord.clone()), down);
148 }
149
150 pub fn physical(&mut self, key: PhysicalKey, down: bool) {
152 self.fire_binding(&Binding::Physical(key), down);
153 }
154
155 pub fn mouse(&mut self, button: PointerButton, down: bool) {
157 self.fire_binding(&Binding::Mouse(button), down);
158 }
159
160 pub fn pad(&mut self, button: GamepadButton, down: bool) {
162 self.fire_binding(&Binding::Pad(button), down);
163 }
164
165 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 pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
192 self.axes.get(&axis).copied().unwrap_or(0.0)
193 }
194
195 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 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 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 pub fn pressed(&self, action: &A) -> bool {
251 self.pressed.contains(action) && self.live(action)
252 }
253
254 pub fn just_pressed(&self, action: &A) -> bool {
256 self.just_pressed.contains(action) && self.live(action)
257 }
258
259 pub fn just_released(&self, action: &A) -> bool {
261 self.just_released.contains(action) && self.live(action)
262 }
263
264 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 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 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 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 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 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 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 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 assert_eq!(
534 st.vector(&"left", &"right", &"jump", &"jump", 0.6),
535 (0.0, 0.0)
536 );
537 st.axis(GamepadAxis::LeftStickX, 0.1);
539 assert_eq!(st.strength(&"right"), 0.0);
540 st.key(&space(), true);
542 assert_eq!(st.strength(&"jump"), 1.0);
543 }
544}