use repame_sim::Sim;
use repame_sim::bevy_ecs::prelude::{Res, World};
use repame_sim::state::State;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Flow {
Menu,
Playing,
Paused,
Modal,
}
impl Flow {
pub fn name(self) -> &'static str {
match self {
Flow::Menu => "menu",
Flow::Playing => "playing",
Flow::Paused => "paused",
Flow::Modal => "modal",
}
}
pub fn keeps_run(self) -> bool {
!matches!(self, Flow::Menu)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Transition {
Idle,
Running,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Suspension {
Live,
Suspended,
}
pub fn install(sim: &mut Sim, start: Flow) -> &mut Sim {
sim.add_state(start);
sim.add_state(Transition::Idle);
sim.add_state(Suspension::Live);
sim
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InputOwner {
Game,
Panel,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FlowState {
flow: Flow,
transition: Transition,
suspension: Suspension,
}
impl FlowState {
pub const CLOSED: FlowState =
FlowState::new(Flow::Menu, Transition::Idle, Suspension::Live);
pub const fn new(flow: Flow, transition: Transition, suspension: Suspension) -> Self {
Self {
flow,
transition,
suspension,
}
}
pub fn from_world(world: &World) -> Option<Self> {
Some(Self {
flow: *world.get_resource::<State<Flow>>()?.get(),
transition: *world.get_resource::<State<Transition>>()?.get(),
suspension: *world.get_resource::<State<Suspension>>()?.get(),
})
}
pub const fn flow(&self) -> Flow {
self.flow
}
pub const fn transition(&self) -> Transition {
self.transition
}
pub const fn suspension(&self) -> Suspension {
self.suspension
}
pub const fn menu(&self) -> bool {
matches!(self.flow, Flow::Menu)
}
pub const fn in_run(&self) -> bool {
matches!(self.flow, Flow::Playing)
}
pub const fn paused(&self) -> bool {
matches!(self.flow, Flow::Paused)
}
pub const fn modal(&self) -> bool {
matches!(self.flow, Flow::Paused | Flow::Modal)
}
pub const fn transitional(&self) -> bool {
matches!(self.transition, Transition::Running)
}
pub const fn suspended(&self) -> bool {
matches!(self.suspension, Suspension::Suspended)
}
pub const fn run_live(&self) -> bool {
self.in_run() && !self.transitional() && !self.suspended()
}
pub const fn panel_owns_input(&self) -> bool {
self.menu() || self.modal()
}
pub const fn game_has_input(&self) -> bool {
self.run_live()
}
pub const fn input_owner(&self) -> InputOwner {
if self.panel_owns_input() {
InputOwner::Panel
} else {
InputOwner::Game
}
}
}
pub fn run_live(
flow: Res<State<Flow>>,
transition: Res<State<Transition>>,
suspension: Res<State<Suspension>>,
) -> bool {
FlowState::new(*flow.get(), *transition.get(), *suspension.get()).run_live()
}
pub fn panel_owns_input(
flow: Res<State<Flow>>,
transition: Res<State<Transition>>,
suspension: Res<State<Suspension>>,
) -> bool {
FlowState::new(*flow.get(), *transition.get(), *suspension.get()).panel_owns_input()
}
#[cfg(test)]
mod tests {
use super::*;
use repame_sim::bevy_ecs::schedule::IntoScheduleConfigs;
use repame_sim::in_state;
const FLOWS: [Flow; 4] = [Flow::Menu, Flow::Playing, Flow::Paused, Flow::Modal];
const TRANSITIONS: [Transition; 2] = [Transition::Idle, Transition::Running];
const SUSPENSIONS: [Suspension; 2] = [Suspension::Live, Suspension::Suspended];
fn expected(
flow: Flow,
transition: Transition,
suspension: Suspension,
) -> (bool, bool, bool, bool, bool, bool, bool, bool, InputOwner) {
let transitional = transition == Transition::Running;
let suspended = suspension == Suspension::Suspended;
(
flow == Flow::Menu,
flow == Flow::Playing,
flow == Flow::Paused,
matches!(flow, Flow::Paused | Flow::Modal),
transitional,
suspended,
flow == Flow::Playing && !transitional && !suspended,
matches!(flow, Flow::Menu | Flow::Paused | Flow::Modal),
if matches!(flow, Flow::Menu | Flow::Paused | Flow::Modal) {
InputOwner::Panel
} else {
InputOwner::Game
},
)
}
#[test]
fn the_predicates_agree_with_the_truth_table() {
for flow in FLOWS {
for transition in TRANSITIONS {
for suspension in SUSPENSIONS {
let state = FlowState::new(flow, transition, suspension);
let row = expected(flow, transition, suspension);
let case = format!("({flow:?}, {transition:?}, {suspension:?})");
assert_eq!(state.menu(), row.0, "menu {case}");
assert_eq!(state.in_run(), row.1, "in_run {case}");
assert_eq!(state.paused(), row.2, "paused {case}");
assert_eq!(state.modal(), row.3, "modal {case}");
assert_eq!(state.transitional(), row.4, "transitional {case}");
assert_eq!(state.suspended(), row.5, "suspended {case}");
assert_eq!(state.run_live(), row.6, "run_live {case}");
assert_eq!(state.panel_owns_input(), row.7, "panel_owns_input {case}");
assert_eq!(state.input_owner(), row.8, "owner {case}");
assert_eq!(state.game_has_input(), row.6, "game_has_input {case}");
}
}
}
}
#[test]
fn a_suspend_does_not_become_a_pause() {
let suspended = FlowState::new(Flow::Playing, Transition::Idle, Suspension::Suspended);
assert!(suspended.suspended());
assert!(!suspended.paused());
assert!(!suspended.modal());
assert!(!suspended.run_live());
assert_eq!(suspended.input_owner(), InputOwner::Game);
}
#[test]
fn menu_is_the_only_flow_without_a_run_behind_it() {
assert!(!Flow::Menu.keeps_run());
assert!(Flow::Playing.keeps_run());
assert!(Flow::Paused.keeps_run());
assert!(Flow::Modal.keeps_run());
}
#[test]
fn state_schedules_fire_in_enter_exit_order() {
use std::sync::{Arc, Mutex};
let log: Arc<Mutex<Vec<&'static str>>> = Arc::new(Mutex::new(Vec::new()));
let mut sim = Sim::with_default_step();
install(&mut sim, Flow::Menu);
let (a, b, c) = (log.clone(), log.clone(), log.clone());
sim.on_exit(Flow::Menu, move || a.lock().unwrap().push("exit:menu"));
sim.on_enter(Flow::Playing, move || b.lock().unwrap().push("enter:playing"));
sim.on_transition(Flow::Menu, Flow::Playing, move || {
c.lock().unwrap().push("menu->playing")
});
sim.set_state(Flow::Playing);
let state = FlowState::from_world(&sim.world).expect("install registered the flow");
assert!(state.menu(), "the transition applies at the step head");
sim.tick();
assert_eq!(
*log.lock().unwrap(),
["exit:menu", "menu->playing", "enter:playing"]
);
let state = FlowState::from_world(&sim.world).expect("install registered the flow");
assert!(state.run_live());
}
#[test]
fn run_live_gates_a_system_through_the_sim() {
use std::sync::atomic::{AtomicU32, Ordering};
static TICKS: AtomicU32 = AtomicU32::new(0);
let mut sim = Sim::with_default_step();
install(&mut sim, Flow::Menu);
sim.add_chained_systems(
(|| {
TICKS.fetch_add(1, Ordering::SeqCst);
})
.run_if(run_live),
);
sim.tick();
assert_eq!(TICKS.load(Ordering::SeqCst), 0, "menu does not advance the world");
sim.set_state(Flow::Playing);
sim.tick();
assert_eq!(TICKS.load(Ordering::SeqCst), 1);
sim.set_state(Transition::Running);
sim.tick();
assert_eq!(TICKS.load(Ordering::SeqCst), 1, "a wipe freezes the world");
sim.set_state(Transition::Idle);
sim.set_state(Suspension::Suspended);
sim.tick();
assert_eq!(TICKS.load(Ordering::SeqCst), 1, "a suspend freezes the world");
sim.set_state(Suspension::Live);
sim.tick();
assert_eq!(TICKS.load(Ordering::SeqCst), 2);
}
#[test]
fn states_work_through_in_state_too() {
use std::sync::atomic::{AtomicU32, Ordering};
static HITS: AtomicU32 = AtomicU32::new(0);
let mut sim = Sim::with_default_step();
install(&mut sim, Flow::Menu);
sim.add_chained_systems(
(|| {
HITS.fetch_add(1, Ordering::SeqCst);
})
.run_if(in_state(Flow::Playing)),
);
sim.set_state(Flow::Playing);
sim.tick();
assert_eq!(HITS.load(Ordering::SeqCst), 1);
sim.set_state(Flow::Paused);
sim.tick();
assert_eq!(HITS.load(Ordering::SeqCst), 1);
}
#[test]
fn a_missing_install_reads_as_none() {
let sim = Sim::with_default_step();
assert!(FlowState::from_world(&sim.world).is_none());
}
}