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(Clone, Debug, Hash, PartialEq, Eq)] pub struct IgnoredPolygon { pub polygon: usize, pub frames_remaining: u32, } #[derive(Component, Clone, Debug, Hash, PartialEq, Eq)] pub enum FighterSurfaceState { Airborne { ecb_lock_frames_remaining: u32, ignored_polys: Vec, }, Grounded { line_id: StageLineID, }, } impl Default for FighterSurfaceState { fn default() -> Self { Self::Airborne { ecb_lock_frames_remaining: 0, ignored_polys: vec![], } } } 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 fn clear_polygon_ignore(&mut self, polygon: usize) { let Self::Airborne { ignored_polys, .. } = self else { return; }; ignored_polys.retain(|element| element.polygon != polygon); } pub fn ignore_polygon(&mut self, polygon: usize, duration_frames: u32) { let Self::Airborne { ignored_polys, .. } = self else { return; }; if duration_frames == 0 { return; } if let Some(ignored) = ignored_polys.iter_mut().find(|ignored| ignored.polygon == polygon) { ignored.frames_remaining = duration_frames; } else { ignored_polys.push(IgnoredPolygon { polygon, frames_remaining: duration_frames, }); } } pub fn is_polygon_ignored(&self, polygon: usize) -> bool { matches!(self, Self::Airborne { ignored_polys, .. } if ignored_polys.iter().any(|ignored| ignored.polygon == polygon)) } pub(crate) fn ignored_polygon_ids(&self) -> Vec { match self { Self::Airborne { ignored_polys, .. } => { ignored_polys.iter().map(|ignored| ignored.polygon).collect() } Self::Grounded { .. } => vec![], } } 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, }; let ignored_polys = match self { Self::Airborne { ignored_polys, .. } => ignored_polys.clone(), Self::Grounded { .. } => vec![], }; *self = Self::Airborne { ecb_lock_frames_remaining, ignored_polys }; } 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); } } pub(crate) fn tick_ignored_polygons(&mut self) { if let Self::Airborne { ignored_polys, .. } = self { for ignored in ignored_polys.iter_mut() { ignored.frames_remaining = ignored.frames_remaining.saturating_sub(1); } ignored_polys.retain(|ignored| ignored.frames_remaining > 0); } } } #[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, ignored_polys: vec![], } ); } #[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, ignored_polys: vec![], } ); } #[test] fn ignored_polygon_expires_after_requested_frames() { let mut surface = FighterSurfaceState::default(); surface.ignore_polygon(3, 2); assert!(surface.is_polygon_ignored(3)); surface.tick_ignored_polygons(); assert!(surface.is_polygon_ignored(3)); surface.tick_ignored_polygons(); assert!(!surface.is_polygon_ignored(3)); } #[test] fn ignoring_polygon_refreshes_existing_entry() { let mut surface = FighterSurfaceState::default(); surface.ignore_polygon(3, 2); surface.ignore_polygon(3, 5); surface.tick_ignored_polygons(); surface.tick_ignored_polygons(); assert!(surface.is_polygon_ignored(3)); } #[test] fn airborne_policy_preserves_ignored_polygons_and_landing_clears_them() { let mut surface = FighterSurfaceState::default(); surface.ignore_polygon(3, 5); surface.apply_airborne_policy(EcbLockPolicy::Restart, 2); assert!(surface.is_polygon_ignored(3)); surface.land(StageLineID { polygon: 0, segment: 0 }); assert!(!surface.is_polygon_ignored(3)); } }