Add authoritative fighter surface state (#60)
CI / build-and-test (push) Successful in 1m28s
CI / production-build (push) Successful in 11m59s

## Summary
- add a rollback-tracked `FighterSurfaceState` as the single source of truth for stage contact
- move active stage line ownership out of individual grounded fighter states
- update landing, airborne motion, ledge departure, hit reactions, and debug save states to maintain surface contact
- add tests for contact lifecycle, connected-segment traversal, and ledge departure

Reviewed-on: #60
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #60.
This commit is contained in:
2026-09-25 10:45:27 +00:00
committed by EIREXE
parent 3daf8f2e21
commit d521461625
18 changed files with 411 additions and 301 deletions
+12 -17
View File
@@ -7,7 +7,7 @@ use crate::{
InterruptContext, UpdateContext,
air::{self, AirborneStateCommon},
fall::FallState,
ground::{GroundedStateCommon, collision_interrupt},
ground::collision_interrupt,
land::LandingType,
wait::WaitState,
},
@@ -17,7 +17,7 @@ use crate::{
#[derive(Clone, Debug, Hash)]
pub enum HitstopStateKind {
Grounded(GroundedStateCommon),
Grounded,
Airborne,
}
@@ -43,7 +43,7 @@ impl FighterStateImpl for HitstunState {
fn name(&self) -> String {
let hitstop_kind = match self.hitstop_kind {
HitstopStateKind::Grounded(_) => "Grounded",
HitstopStateKind::Grounded => "Grounded",
HitstopStateKind::Airborne => "Airborne",
};
@@ -58,14 +58,10 @@ impl FighterStateImpl for HitstunState {
fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option<FighterState> {
if state_context.get_current_animation_frame() > self.hit_reaction.hitstun_frames {
match &self.hitstop_kind {
HitstopStateKind::Grounded(grounded_state_common) => {
Some(FighterState::Wait(WaitState {
grounded_common: grounded_state_common.clone(),
}))
}
HitstopStateKind::Airborne => {
Some(FighterState::Fall(FallState::new(AirborneStateCommon::default())))
}
HitstopStateKind::Grounded => Some(FighterState::Wait(WaitState)),
HitstopStateKind::Airborne => Some(FighterState::Fall(FallState::new(
AirborneStateCommon::default(),
))),
}
} else {
None
@@ -85,8 +81,9 @@ impl FighterStateImpl for HitstunState {
}
match self.hitstop_kind {
HitstopStateKind::Grounded(_) => {}
HitstopStateKind::Grounded => {}
HitstopStateKind::Airborne => {
ctx.surface.leave_ground();
air::integrate_gravity(ctx.velocity, ctx.fighter_attributes, false);
}
}
@@ -103,7 +100,7 @@ impl FighterStateImpl for HitstunState {
fn on_enter(&mut self, state_context: &mut EnterContext) {
let animation = match self.hitstop_kind {
HitstopStateKind::Grounded(_) => AnimKind::FlinchGround,
HitstopStateKind::Grounded => AnimKind::FlinchGround,
HitstopStateKind::Airborne => AnimKind::FlinchAir,
};
state_context.play_animation(animation, false);
@@ -116,9 +113,7 @@ impl FighterStateImpl for HitstunState {
state_context: &mut CollisionContext,
) -> Option<FighterState> {
match &mut self.hitstop_kind {
HitstopStateKind::Grounded(grounded_state_common) => {
collision_interrupt(state_context, grounded_state_common, true)
}
HitstopStateKind::Grounded => collision_interrupt(state_context, true),
HitstopStateKind::Airborne => {
air::collision_interrupt(state_context, LandingType::Normal)
}
@@ -127,7 +122,7 @@ impl FighterStateImpl for HitstunState {
fn is_visually_grounded<'a>(&'a self) -> bool {
match self.hitstop_kind {
HitstopStateKind::Grounded(_) => true,
HitstopStateKind::Grounded => true,
HitstopStateKind::Airborne => false,
}
}