1use repame_sim::Sim;
25use repame_sim::bevy_ecs::prelude::{Res, World};
26use repame_sim::state::State;
27
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub enum Flow {
31 Menu,
33 Playing,
35 Paused,
37 Modal,
40}
41
42impl Flow {
43 pub fn name(self) -> &'static str {
45 match self {
46 Flow::Menu => "menu",
47 Flow::Playing => "playing",
48 Flow::Paused => "paused",
49 Flow::Modal => "modal",
50 }
51 }
52
53 pub fn keeps_run(self) -> bool {
57 !matches!(self, Flow::Menu)
58 }
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
63pub enum Transition {
64 Idle,
65 Running,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
74pub enum Suspension {
75 Live,
76 Suspended,
77}
78
79pub fn install(sim: &mut Sim, start: Flow) -> &mut Sim {
87 sim.add_state(start);
88 sim.add_state(Transition::Idle);
89 sim.add_state(Suspension::Live);
90 sim
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum InputOwner {
96 Game,
98 Panel,
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
104pub struct FlowState {
105 flow: Flow,
106 transition: Transition,
107 suspension: Suspension,
108}
109
110impl FlowState {
111 pub const CLOSED: FlowState =
115 FlowState::new(Flow::Menu, Transition::Idle, Suspension::Live);
116
117 pub const fn new(flow: Flow, transition: Transition, suspension: Suspension) -> Self {
118 Self {
119 flow,
120 transition,
121 suspension,
122 }
123 }
124
125 pub fn from_world(world: &World) -> Option<Self> {
130 Some(Self {
131 flow: *world.get_resource::<State<Flow>>()?.get(),
132 transition: *world.get_resource::<State<Transition>>()?.get(),
133 suspension: *world.get_resource::<State<Suspension>>()?.get(),
134 })
135 }
136
137 pub const fn flow(&self) -> Flow {
138 self.flow
139 }
140
141 pub const fn transition(&self) -> Transition {
142 self.transition
143 }
144
145 pub const fn suspension(&self) -> Suspension {
146 self.suspension
147 }
148
149 pub const fn menu(&self) -> bool {
151 matches!(self.flow, Flow::Menu)
152 }
153
154 pub const fn in_run(&self) -> bool {
156 matches!(self.flow, Flow::Playing)
157 }
158
159 pub const fn paused(&self) -> bool {
161 matches!(self.flow, Flow::Paused)
162 }
163
164 pub const fn modal(&self) -> bool {
169 matches!(self.flow, Flow::Paused | Flow::Modal)
170 }
171
172 pub const fn transitional(&self) -> bool {
174 matches!(self.transition, Transition::Running)
175 }
176
177 pub const fn suspended(&self) -> bool {
179 matches!(self.suspension, Suspension::Suspended)
180 }
181
182 pub const fn run_live(&self) -> bool {
185 self.in_run() && !self.transitional() && !self.suspended()
186 }
187
188 pub const fn panel_owns_input(&self) -> bool {
190 self.menu() || self.modal()
191 }
192
193 pub const fn game_has_input(&self) -> bool {
198 self.run_live()
199 }
200
201 pub const fn input_owner(&self) -> InputOwner {
202 if self.panel_owns_input() {
203 InputOwner::Panel
204 } else {
205 InputOwner::Game
206 }
207 }
208}
209
210pub fn run_live(
216 flow: Res<State<Flow>>,
217 transition: Res<State<Transition>>,
218 suspension: Res<State<Suspension>>,
219) -> bool {
220 FlowState::new(*flow.get(), *transition.get(), *suspension.get()).run_live()
221}
222
223pub fn panel_owns_input(
225 flow: Res<State<Flow>>,
226 transition: Res<State<Transition>>,
227 suspension: Res<State<Suspension>>,
228) -> bool {
229 FlowState::new(*flow.get(), *transition.get(), *suspension.get()).panel_owns_input()
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use repame_sim::bevy_ecs::schedule::IntoScheduleConfigs;
236 use repame_sim::in_state;
237
238 const FLOWS: [Flow; 4] = [Flow::Menu, Flow::Playing, Flow::Paused, Flow::Modal];
239 const TRANSITIONS: [Transition; 2] = [Transition::Idle, Transition::Running];
240 const SUSPENSIONS: [Suspension; 2] = [Suspension::Live, Suspension::Suspended];
241
242 fn expected(
245 flow: Flow,
246 transition: Transition,
247 suspension: Suspension,
248 ) -> (bool, bool, bool, bool, bool, bool, bool, bool, InputOwner) {
249 let transitional = transition == Transition::Running;
250 let suspended = suspension == Suspension::Suspended;
251 (
252 flow == Flow::Menu,
253 flow == Flow::Playing,
254 flow == Flow::Paused,
255 matches!(flow, Flow::Paused | Flow::Modal),
256 transitional,
257 suspended,
258 flow == Flow::Playing && !transitional && !suspended,
259 matches!(flow, Flow::Menu | Flow::Paused | Flow::Modal),
260 if matches!(flow, Flow::Menu | Flow::Paused | Flow::Modal) {
261 InputOwner::Panel
262 } else {
263 InputOwner::Game
264 },
265 )
266 }
267
268 #[test]
269 fn the_predicates_agree_with_the_truth_table() {
270 for flow in FLOWS {
271 for transition in TRANSITIONS {
272 for suspension in SUSPENSIONS {
273 let state = FlowState::new(flow, transition, suspension);
274 let row = expected(flow, transition, suspension);
275 let case = format!("({flow:?}, {transition:?}, {suspension:?})");
276 assert_eq!(state.menu(), row.0, "menu {case}");
277 assert_eq!(state.in_run(), row.1, "in_run {case}");
278 assert_eq!(state.paused(), row.2, "paused {case}");
279 assert_eq!(state.modal(), row.3, "modal {case}");
280 assert_eq!(state.transitional(), row.4, "transitional {case}");
281 assert_eq!(state.suspended(), row.5, "suspended {case}");
282 assert_eq!(state.run_live(), row.6, "run_live {case}");
283 assert_eq!(state.panel_owns_input(), row.7, "panel_owns_input {case}");
284 assert_eq!(state.input_owner(), row.8, "owner {case}");
285 assert_eq!(state.game_has_input(), row.6, "game_has_input {case}");
286 }
287 }
288 }
289 }
290
291 #[test]
292 fn a_suspend_does_not_become_a_pause() {
293 let suspended = FlowState::new(Flow::Playing, Transition::Idle, Suspension::Suspended);
294 assert!(suspended.suspended());
295 assert!(!suspended.paused());
296 assert!(!suspended.modal());
297 assert!(!suspended.run_live());
298 assert_eq!(suspended.input_owner(), InputOwner::Game);
299 }
300
301 #[test]
302 fn menu_is_the_only_flow_without_a_run_behind_it() {
303 assert!(!Flow::Menu.keeps_run());
304 assert!(Flow::Playing.keeps_run());
305 assert!(Flow::Paused.keeps_run());
306 assert!(Flow::Modal.keeps_run());
307 }
308
309 #[test]
310 fn state_schedules_fire_in_enter_exit_order() {
311 use std::sync::{Arc, Mutex};
312
313 let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
314 let mut sim = Sim::with_default_step();
315 install(&mut sim, Flow::Menu);
316
317 let (a, b, c) = (log.clone(), log.clone(), log.clone());
318 sim.on_exit(Flow::Menu, move || a.lock().unwrap().push("exit:menu"));
319 sim.on_enter(Flow::Playing, move || b.lock().unwrap().push("enter:playing"));
320 sim.on_transition(Flow::Menu, Flow::Playing, move || {
321 c.lock().unwrap().push("menu->playing")
322 });
323
324 sim.set_state(Flow::Playing);
325 let state = FlowState::from_world(&sim.world).expect("install registered the flow");
326 assert!(state.menu(), "the transition applies at the step head");
327 sim.tick();
328 assert_eq!(
329 *log.lock().unwrap(),
330 ["exit:menu", "menu->playing", "enter:playing"]
331 );
332 let state = FlowState::from_world(&sim.world).expect("install registered the flow");
333 assert!(state.run_live());
334 }
335
336 #[test]
337 fn run_live_gates_a_system_through_the_sim() {
338 use std::sync::atomic::{AtomicU32, Ordering};
339 static TICKS: AtomicU32 = AtomicU32::new(0);
340
341 let mut sim = Sim::with_default_step();
342 install(&mut sim, Flow::Menu);
343 sim.add_chained_systems(
344 (|| {
345 TICKS.fetch_add(1, Ordering::SeqCst);
346 })
347 .run_if(run_live),
348 );
349
350 sim.tick();
351 assert_eq!(TICKS.load(Ordering::SeqCst), 0, "menu does not advance the world");
352
353 sim.set_state(Flow::Playing);
354 sim.tick();
355 assert_eq!(TICKS.load(Ordering::SeqCst), 1);
356
357 sim.set_state(Transition::Running);
358 sim.tick();
359 assert_eq!(TICKS.load(Ordering::SeqCst), 1, "a wipe freezes the world");
360
361 sim.set_state(Transition::Idle);
362 sim.set_state(Suspension::Suspended);
363 sim.tick();
364 assert_eq!(TICKS.load(Ordering::SeqCst), 1, "a suspend freezes the world");
365
366 sim.set_state(Suspension::Live);
367 sim.tick();
368 assert_eq!(TICKS.load(Ordering::SeqCst), 2);
369 }
370
371 #[test]
372 fn states_work_through_in_state_too() {
373 use std::sync::atomic::{AtomicU32, Ordering};
374 static HITS: AtomicU32 = AtomicU32::new(0);
375
376 let mut sim = Sim::with_default_step();
377 install(&mut sim, Flow::Menu);
378 sim.add_chained_systems(
379 (|| {
380 HITS.fetch_add(1, Ordering::SeqCst);
381 })
382 .run_if(in_state(Flow::Playing)),
383 );
384 sim.set_state(Flow::Playing);
385 sim.tick();
386 assert_eq!(HITS.load(Ordering::SeqCst), 1);
387 sim.set_state(Flow::Paused);
388 sim.tick();
389 assert_eq!(HITS.load(Ordering::SeqCst), 1);
390 }
391
392 #[test]
393 fn a_missing_install_reads_as_none() {
394 let sim = Sim::with_default_step();
395 assert!(FlowState::from_world(&sim.world).is_none());
396 }
397}