131 lines
3.6 KiB
Rust
131 lines
3.6 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(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<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(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,
|
|
}
|
|
);
|
|
}
|
|
}
|