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}
29
30impl<A: ActionLike> ActionState<A> {
31 pub fn new(map: ActionMap<A>) -> Self {
32 Self {
33 map,
34 pressed: HashSet::new(),
35 just_pressed: HashSet::new(),
36 just_released: HashSet::new(),
37 consumed: HashSet::new(),
38 axes: HashMap::new(),
39 active_contexts: HashSet::new(),
40 }
41 }
42
43 pub fn map(&self) -> &ActionMap<A> {
44 &self.map
45 }
46
47 pub fn end_tick(&mut self) {
50 self.just_pressed.clear();
51 self.just_released.clear();
52 }
53
54 pub fn clear_edges(&mut self) {
56 self.end_tick();
57 }
58
59 pub fn set_contexts(&mut self, contexts: &[&str]) {
65 let next: HashSet<String> = contexts.iter().map(|s| s.to_string()).collect();
66 if next != self.active_contexts {
67 self.active_contexts = next;
68 self.just_pressed.clear();
69 self.just_released.clear();
70 }
71 }
72
73 fn live(&self, action: &A) -> bool {
74 self.map.live_in(action, &self.active_contexts) && !self.consumed.contains(action)
75 }
76
77 fn press(&mut self, action: &A) {
78 if self.pressed.insert(action.clone()) {
79 self.just_pressed.insert(action.clone());
80 }
81 }
82
83 fn release(&mut self, action: &A) {
84 if self.pressed.remove(action) {
85 self.just_released.insert(action.clone());
86 }
87 }
88
89 fn fire_binding(&mut self, binding: &Binding, down: bool) {
90 let actions: Vec<A> = self
91 .map
92 .actions()
93 .filter(|a| self.map.bindings_for(a).contains(binding))
94 .cloned()
95 .collect();
96 for action in actions {
97 if down {
98 self.press(&action);
99 } else {
100 self.release(&action);
101 }
102 }
103 }
104
105 pub fn key(&mut self, chord: &KeyChord, down: bool) {
107 self.fire_binding(&Binding::Key(chord.clone()), down);
108 }
109
110 pub fn mouse(&mut self, button: PointerButton, down: bool) {
112 self.fire_binding(&Binding::Mouse(button), down);
113 }
114
115 pub fn pad(&mut self, button: GamepadButton, down: bool) {
117 self.fire_binding(&Binding::Pad(button), down);
118 }
119
120 pub fn axis(&mut self, axis: GamepadAxis, value: f32) {
122 self.axes.insert(axis, value);
123 let flips: Vec<(A, bool)> = self
124 .map
125 .actions()
126 .flat_map(|a| {
127 self.map
128 .bindings_for(a)
129 .iter()
130 .filter_map(move |b| match b {
131 Binding::Axis {
132 axis: ba,
133 threshold,
134 } if *ba == axis => {
135 Some((a.clone(), Binding::axis_active(*threshold, value)))
136 }
137 _ => None,
138 })
139 })
140 .collect();
141 for (action, active) in flips {
142 if active {
143 self.press(&action);
144 } else {
145 self.release(&action);
146 }
147 }
148 }
149
150 pub fn axis_value(&self, axis: GamepadAxis) -> f32 {
152 self.axes.get(&axis).copied().unwrap_or(0.0)
153 }
154
155 pub fn pressed(&self, action: &A) -> bool {
157 self.pressed.contains(action) && self.live(action)
158 }
159
160 pub fn just_pressed(&self, action: &A) -> bool {
162 self.just_pressed.contains(action) && self.live(action)
163 }
164
165 pub fn just_released(&self, action: &A) -> bool {
167 self.just_released.contains(action)
168 }
169
170 pub fn consume(&mut self, action: &A) -> bool {
173 let held = self.pressed.contains(action);
174 self.consumed.insert(action.clone());
175 held
176 }
177
178 pub fn consume_all(&mut self) {
179 self.consumed.extend(self.pressed.iter().cloned());
180 }
181
182 pub fn mock(&mut self, action: &A, down: bool) {
184 if down {
185 self.press(action);
186 } else {
187 self.release(action);
188 }
189 }
190
191 pub fn clear_consumed(&mut self) {
193 self.consumed.clear();
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use repose_core::input::{Key, Modifiers};
201
202 fn jump_map() -> ActionMap<&'static str> {
203 let mut map = ActionMap::new();
204 map.bind(
205 "jump",
206 Binding::Key(KeyChord::new(Key::Space, Modifiers::default())),
207 );
208 map.bind("jump", Binding::Pad(GamepadButton::South));
209 map
210 }
211
212 fn space() -> KeyChord {
213 KeyChord::new(Key::Space, Modifiers::default())
214 }
215
216 #[test]
217 fn edges_clear_at_tick_end() {
218 let mut st = ActionState::new(jump_map());
219 st.key(&space(), true);
220 assert!(st.just_pressed(&"jump"));
221 assert!(st.pressed(&"jump"));
222 assert!(st.just_pressed(&"jump"));
224 st.end_tick();
225 assert!(!st.just_pressed(&"jump"));
226 assert!(st.pressed(&"jump"));
227 st.key(&space(), false);
228 assert!(st.just_released(&"jump"));
229 assert!(!st.pressed(&"jump"));
230 st.end_tick();
231 assert!(!st.just_released(&"jump"));
232 }
233
234 #[test]
235 fn pad_and_key_drive_same_action() {
236 let mut st = ActionState::new(jump_map());
237 st.pad(GamepadButton::South, true);
238 assert!(st.just_pressed(&"jump"));
239 st.end_tick();
240 st.pad(GamepadButton::South, false);
241 st.end_tick();
242 st.key(&space(), true);
243 assert!(st.just_pressed(&"jump"));
244 }
245
246 #[test]
247 fn axis_threshold_flips() {
248 let mut map = ActionMap::new();
249 map.bind(
250 "left",
251 Binding::Axis {
252 axis: GamepadAxis::LeftStickX,
253 threshold: -0.5,
254 },
255 );
256 let mut st = ActionState::new(map);
257 st.axis(GamepadAxis::LeftStickX, -0.2);
258 assert!(!st.pressed(&"left"));
259 st.axis(GamepadAxis::LeftStickX, -0.8);
260 assert!(st.just_pressed(&"left"));
261 assert_eq!(st.axis_value(GamepadAxis::LeftStickX), -0.8);
262 st.axis(GamepadAxis::LeftStickX, 0.0);
263 assert!(st.just_released(&"left"));
264 }
265
266 #[test]
267 fn consume_hides_from_late_readers() {
268 let mut st = ActionState::new(jump_map());
269 st.key(&space(), true);
270 assert!(st.consume(&"jump"));
271 assert!(!st.pressed(&"jump"));
272 assert!(!st.just_pressed(&"jump"));
273 st.clear_consumed();
274 assert!(st.pressed(&"jump"));
275 }
276
277 #[test]
278 fn context_gates_sim_action() {
279 let mut map = jump_map();
280 map.in_context("gameplay", "jump");
281 let mut st = ActionState::new(map);
282 st.set_contexts(&["menu"]);
283 st.key(&space(), true);
284 assert!(!st.pressed(&"jump"));
285 st.set_contexts(&["gameplay"]);
286 st.key(&space(), true);
287 assert!(st.pressed(&"jump"));
288 }
289
290 #[test]
291 fn context_switch_drops_pending_edges() {
292 let mut map = jump_map();
293 map.in_context("gameplay", "jump");
294 let mut st = ActionState::new(map);
295 st.set_contexts(&["gameplay"]);
296 st.key(&space(), true);
297 assert!(st.just_pressed(&"jump"));
298 st.set_contexts(&["menu"]);
299 assert!(!st.just_pressed(&"jump"));
300 assert!(!st.pressed(&"jump"));
301 st.key(&space(), false);
302 st.set_contexts(&["gameplay"]);
303 assert!(!st.just_released(&"jump"));
304 }
305
306 #[test]
307 fn mock_drives_without_hardware() {
308 let mut st = ActionState::new(jump_map());
309 st.mock(&"jump", true);
310 assert!(st.just_pressed(&"jump"));
311 st.mock(&"jump", false);
312 assert!(st.just_released(&"jump"));
313 }
314}