228 lines
6.9 KiB
Rust
228 lines
6.9 KiB
Rust
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<IgnoredPolygon>,
|
|
},
|
|
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<StageLineID> {
|
|
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<usize> {
|
|
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));
|
|
}
|
|
}
|