use bevy::prelude::*; use crate::{stage::line::StageLineID}; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum EcbLockPolicy { StartOnDetach, Restart, Preserve, Clear, } /// The stage surface currently supporting a fighter. /// /// This is physics state rather than fighter-state-machine state. Keeping it /// separate means grounded, landing, and hitstun states all share one source /// of truth for stage contact. #[derive(Component, Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum FighterSurfaceState { Airborne { ecb_lock_frames_remaining: u32 }, Grounded { line_id: StageLineID, }, } impl Default for FighterSurfaceState { fn default() -> Self { Self::Airborne { ecb_lock_frames_remaining: 0 } } } impl FighterSurfaceState { pub fn is_grounded(self) -> bool { matches!(self, Self::Grounded { .. }) } pub fn line_id(self) -> Option { match self { Self::Airborne { .. } => None, Self::Grounded { line_id } => Some(line_id), } } pub(crate) fn land(&mut self, line_id: StageLineID) { *self = Self::Grounded { line_id }; } pub(crate) fn apply_airborne_policy( &mut self, policy: EcbLockPolicy, ecb_lock_duration: u32, ) { let current_lock = match *self { Self::Airborne { ecb_lock_frames_remaining } => ecb_lock_frames_remaining, Self::Grounded { .. } => 0, }; let ecb_lock_frames_remaining = match policy { EcbLockPolicy::StartOnDetach if self.is_grounded() => ecb_lock_duration, EcbLockPolicy::StartOnDetach => current_lock, EcbLockPolicy::Restart => ecb_lock_duration, EcbLockPolicy::Preserve => current_lock, EcbLockPolicy::Clear => 0, }; *self = Self::Airborne { ecb_lock_frames_remaining }; } pub(crate) fn tick_ecb_lock(&mut self) { if let Self::Airborne { ecb_lock_frames_remaining } = self { *ecb_lock_frames_remaining = ecb_lock_frames_remaining.saturating_sub(1); } } } #[cfg(test)] mod tests { use super::*; #[test] fn contact_lifecycle_has_one_source_of_truth() { let mut surface = FighterSurfaceState::default(); assert!(!surface.is_grounded()); assert_eq!(surface.line_id(), None); let line_id = StageLineID { polygon: 2, segment: 3, }; surface.land(line_id); assert!(surface.is_grounded()); assert_eq!(surface.line_id(), Some(line_id)); surface.apply_airborne_policy(EcbLockPolicy::Clear, 0); assert!(!surface.is_grounded()); assert_eq!(surface.line_id(), None); } #[test] fn start_on_detach_does_not_reset_an_existing_lock() { let mut surface = FighterSurfaceState::default(); surface.apply_airborne_policy(EcbLockPolicy::Restart, 5); surface.tick_ecb_lock(); surface.apply_airborne_policy(EcbLockPolicy::StartOnDetach, 5); assert_eq!( surface, FighterSurfaceState::Airborne { ecb_lock_frames_remaining: 4, } ); } #[test] fn restart_retriggers_an_existing_lock() { let mut surface = FighterSurfaceState::default(); surface.apply_airborne_policy(EcbLockPolicy::Restart, 5); surface.tick_ecb_lock(); surface.apply_airborne_policy(EcbLockPolicy::Restart, 5); assert_eq!( surface, FighterSurfaceState::Airborne { ecb_lock_frames_remaining: 5, } ); } }