Add authoritative fighter surface state
CI / build-and-test (pull_request) Successful in 1m42s
CI / production-build (pull_request) Successful in 10m8s

This commit is contained in:
2026-09-25 10:45:17 +02:00
parent 9a3a1383ff
commit a54d3a9c49
18 changed files with 411 additions and 301 deletions
+114 -30
View File
@@ -3,7 +3,8 @@
//! bit-for-bit deterministic for rollback.
use crate::fighter::state::{
CollisionContext, FighterState, InterruptContext, air, dash, fall, ground_attack, turn, wait, walk,
CollisionContext, FighterState, InterruptContext, air, dash, fall, ground_attack, turn, wait,
walk,
};
use crate::fighter::{
FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity,
@@ -13,13 +14,6 @@ use crate::math::int::{FGWide, FGi32};
use crate::math::vec::FGVec2;
use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType};
use bevy::prelude::*;
#[derive(Reflect, Hash, Clone, Debug)]
pub struct GroundedStateCommon {
pub current_line_id: StageLineID,
}
pub fn apply_grounded_friction(friction: FGi32, ground_vel: FGi32) -> FGi32 {
if friction.abs() > ground_vel.abs() {
-ground_vel
@@ -181,22 +175,23 @@ pub fn update_friction_motion(ctx: &mut crate::fighter::state::UpdateContext) {
pub fn collide_with_stage_grounded(
state_context: &mut CollisionContext,
ground_common: &mut GroundedStateCommon,
can_walk_off: bool,
) -> GroundedMotionResult {
let translation = &mut state_context.translation;
let ecb = state_context.ecb;
let stage_collision = state_context.stage_collision;
let stage_poly = stage_collision
.stage_polys
.get(ground_common.current_line_id.polygon);
let Some(current_line_id) = state_context.surface.line_id() else {
return GroundedMotionResult::InAir;
};
let stage_poly = stage_collision.stage_polys.get(current_line_id.polygon);
if let Some(stage_poly) = stage_poly {
let ecb_bottom_pos = translation.0 + ecb.get_bottom_point();
match resolve_grounded_position(
stage_poly,
ground_common.current_line_id.segment,
current_line_id.segment,
ecb_bottom_pos,
can_walk_off,
) {
@@ -216,30 +211,37 @@ pub fn collide_with_stage_grounded(
.0
.project_onto_normalized(current_segment.normal)
};
ground_common.current_line_id.segment = segment_idx;
state_context.surface.land(StageLineID {
polygon: current_line_id.polygon,
segment: segment_idx,
});
GroundedMotionResult::InGround
}
GroundedPositionResult::InAir => GroundedMotionResult::InAir,
GroundedPositionResult::InAir => {
state_context.surface.leave_ground();
GroundedMotionResult::InAir
}
}
} else {
state_context.surface.leave_ground();
GroundedMotionResult::InAir
}
}
pub fn collision_interrupt(
ctx: &mut CollisionContext,
common: &mut GroundedStateCommon,
can_walk_off: bool,
) -> Option<FighterState> {
match collide_with_stage_grounded(ctx, common, can_walk_off) {
pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Option<FighterState> {
match collide_with_stage_grounded(ctx, can_walk_off) {
GroundedMotionResult::InGround => None,
GroundedMotionResult::InAir => Some(FighterState::Fall(fall::FallState::new(air::AirborneStateCommon::default()))),
GroundedMotionResult::InAir => Some(FighterState::Fall(fall::FallState::new(
air::AirborneStateCommon::default(),
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fighter::ecb::FighterPreviousECB;
use crate::fighter::{FighterECB, FighterSurfaceState};
use crate::stage::line::StagePolyType;
fn two_segment_floor() -> StagePoly {
@@ -346,28 +348,110 @@ mod tests {
GroundedPositionResult::InAir
));
}
#[test]
fn grounded_collision_updates_surface_segment() {
let stage_collision = crate::stage::line::StageCollision {
stage_polys: vec![two_segment_floor()],
};
let mut surface = FighterSurfaceState::Grounded {
line_id: StageLineID {
polygon: 0,
segment: 0,
},
};
let mut translation = FighterTranslation(FGVec2::lit("11", "0"));
let previous_translation = FighterPreviousTranslation(translation.0);
let mut velocity = FighterVelocity::default();
let ecb = FighterECB {
vertical_half: FGi32::ONE,
horizontal_half: FGi32::ONE,
};
let previous_ecb = FighterPreviousECB::from(ecb);
let mut context = CollisionContext {
translation: &mut translation,
prev_translation: &previous_translation,
velocity: &mut velocity,
surface: &mut surface,
ecb: &ecb,
prev_ecb: &previous_ecb,
stage_collision: &stage_collision,
};
assert!(matches!(
collide_with_stage_grounded(&mut context, true),
GroundedMotionResult::InGround
));
assert_eq!(
surface.line_id(),
Some(StageLineID {
polygon: 0,
segment: 1,
})
);
}
#[test]
fn walking_off_edge_clears_surface_contact() {
let stage_collision = crate::stage::line::StageCollision {
stage_polys: vec![StagePoly::build(
StagePolyType::Platform,
&[
(FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor),
(FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor),
],
)],
};
let mut surface = FighterSurfaceState::Grounded {
line_id: StageLineID {
polygon: 0,
segment: 0,
},
};
let mut translation = FighterTranslation(FGVec2::lit("11", "0"));
let previous_translation = FighterPreviousTranslation(translation.0);
let mut velocity = FighterVelocity::default();
let ecb = FighterECB {
vertical_half: FGi32::ONE,
horizontal_half: FGi32::ONE,
};
let previous_ecb = FighterPreviousECB::from(ecb);
let mut context = CollisionContext {
translation: &mut translation,
prev_translation: &previous_translation,
velocity: &mut velocity,
surface: &mut surface,
ecb: &ecb,
prev_ecb: &previous_ecb,
stage_collision: &stage_collision,
};
assert!(matches!(
collide_with_stage_grounded(&mut context, true),
GroundedMotionResult::InAir
));
assert_eq!(surface, FighterSurfaceState::Airborne);
}
}
pub fn grounded_movement_common_interrupts(
state_context: &InterruptContext,
ground_common: &GroundedStateCommon,
) -> Option<FighterState> {
super::jump::check_input(state_context, ground_common)
super::jump::check_input(state_context)
}
pub fn grounded_movement_standstill_common_interrupts(
state_context: &InterruptContext,
ground_common: &GroundedStateCommon,
) -> Option<FighterState> {
if let Some(state) = ground_attack::check_interrupt(state_context, &ground_common) {
if let Some(state) = ground_attack::check_interrupt(state_context) {
Some(state)
} else if let Some(state) = dash::check_input(state_context, &ground_common) {
} else if let Some(state) = dash::check_input(state_context) {
Some(state)
} else if let Some(state) = turn::check_input(state_context, &ground_common) {
} else if let Some(state) = turn::check_input(state_context) {
Some(state)
} else if let Some(state) = walk::check_input(state_context, &ground_common) {
} else if let Some(state) = walk::check_input(state_context) {
Some(state)
} else if let Some(state) = wait::check_input(state_context, &ground_common) {
} else if let Some(state) = wait::check_input(state_context) {
Some(state)
} else {
None