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.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 pub fn end_tick(&mut self) {
63 self.just_pressed.clear();
64 self.just_released.clear();
65 self.consumed.clear();
66 }
67
68 pub fn clear_edges(&mut self) {
70 self.end_tick();
71 }
72
73 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 pub fn key(&mut self, chord: &KeyChord, down: bool) {
146 self.fire_binding(&Binding::Key(chord.clone()), down);
147 }
148
149 pub fn physical(&mut self, key: PhysicalKey, down: bool) {
151 self.fire_binding(&Binding::Physical(key), down);
152 }
153
154 pub fn mouse(&mut self, button: PointerButton, down: bool) {
156 self.fire_binding(&Binding::Mouse(button), down);
157 }
158
159 pub fn pad(&mut self, button: GamepadButton, down: bool) {
161 self.fire_binding(&Binding::Pad(button), down);
162 }
163
164 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 pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
191 self.axes.get(&axis).copied().unwrap_or(0.0)
192 }
193
194 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 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 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 pub fn pressed(&self, action: &A) -> bool {
247 self.pressed.contains(action) && self.live(action)
248 }
249
250 pub fn just_pressed(&self, action: &A) -> bool {
252 self.just_pressed.contains(action) && self.live(action)
253 }
254
255 pub fn just_released(&self, action: &A) -> bool {
257 self.just_released.contains(action) && self.live(action)
258 }
259
260 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 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 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 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 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 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 assert_eq!(
477 st.vector(&"left", &"right", &"jump", &"jump", 0.6),
478 (0.0, 0.0)
479 );
480 st.axis(GamepadAxis::LeftStickX, 0.1);
482 assert_eq!(st.strength(&"right"), 0.0);
483 st.key(&space(), true);
485 assert_eq!(st.strength(&"jump"), 1.0);
486 }
487}