Add mechanism to ignore platforms #63

Merged
EIREXE merged 1 commits from platform_ignore_thingy into master 2026-09-25 21:43:50 +00:00
6 changed files with 161 additions and 16 deletions
+2 -2
View File
@@ -153,7 +153,7 @@ pub(super) fn save_or_restore(world: &mut World) {
entity.insert(saved.hitboxes.clone()); entity.insert(saved.hitboxes.clone());
entity.insert(saved.solved_hurtboxes.clone()); entity.insert(saved.solved_hurtboxes.clone());
entity.insert(saved.bone_matrices.clone()); entity.insert(saved.bone_matrices.clone());
entity.insert(saved.surface); entity.insert(saved.surface.clone());
} }
world.insert_resource(checkpoint.pending_hits.clone()); world.insert_resource(checkpoint.pending_hits.clone());
world.insert_resource(checkpoint.stage_collision); world.insert_resource(checkpoint.stage_collision);
@@ -187,7 +187,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option<FighterCheckpoint> {
hitboxes: fighter.get::<FighterHitboxes>()?.clone(), hitboxes: fighter.get::<FighterHitboxes>()?.clone(),
solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(), solved_hurtboxes: fighter.get::<FighterSolvedHurtboxes>()?.clone(),
bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(), bone_matrices: fighter.get::<FighterBoneMatrices>()?.clone(),
surface: *fighter.get::<FighterSurfaceState>()?, surface: fighter.get::<FighterSurfaceState>()?.clone(),
}) })
} }
+53
View File
@@ -44,6 +44,7 @@ pub fn air_collide_with_stage(
prev_ecb: &FighterPreviousECB, prev_ecb: &FighterPreviousECB,
ecb: &FighterECB, ecb: &FighterECB,
stage_collision: &StageCollision, stage_collision: &StageCollision,
ignored_polys: &[usize],
) -> Option<AirCollisionWithStageResult> { ) -> Option<AirCollisionWithStageResult> {
let ray_segment = FGSegment2d::new( let ray_segment = FGSegment2d::new(
prev_translation.0 + prev_ecb.get_bottom_point(), prev_translation.0 + prev_ecb.get_bottom_point(),
@@ -54,6 +55,7 @@ pub fn air_collide_with_stage(
.stage_polys .stage_polys
.iter() .iter()
.enumerate() .enumerate()
.filter(|(poly_idx, _)| !ignored_polys.contains(poly_idx))
.filter_map(|(poly_idx, poly)| { .filter_map(|(poly_idx, poly)| {
poly.intersect_ray(ray_segment).map(|intersect_result| { poly.intersect_ray(ray_segment).map(|intersect_result| {
( (
@@ -76,3 +78,54 @@ pub fn air_collide_with_stage(
.max_by(|(_, a), (_, b)| a.cmp(b)) .max_by(|(_, a), (_, b)| a.cmp(b))
.map(|(result, _)| result) .map(|(result, _)| result)
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::fighter::ecb::{FighterECB, FighterPreviousECB};
use crate::stage::line::{StagePoly, StagePolyLineSegmentType, StagePolyType};
fn horizontal_platform(y: &str) -> StagePoly {
StagePoly::build(
StagePolyType::Platform,
&[
(FGVec2::lit("-2", y), StagePolyLineSegmentType::Floor),
(FGVec2::lit("2", y), StagePolyLineSegmentType::Floor),
],
)
}
#[test]
fn airborne_collision_skips_ignored_polygons() {
let stage_collision = StageCollision {
stage_polys: vec![horizontal_platform("0"), horizontal_platform("1")],
};
let previous_translation = FighterPreviousTranslation(FGVec2::lit("0", "2"));
let translation = FighterTranslation(FGVec2::lit("0", "-1"));
let previous_ecb = FighterPreviousECB {
bottom: FGVec2::ZERO,
top: FGVec2::ZERO,
left: FGVec2::ZERO,
right: FGVec2::ZERO,
};
let ecb = FighterECB {
bottom: FGVec2::ZERO,
top: FGVec2::ZERO,
left: FGVec2::ZERO,
right: FGVec2::ZERO,
};
let collision = air_collide_with_stage(
&previous_translation,
&translation,
&previous_ecb,
&ecb,
&stage_collision,
&[1],
)
.expect("the non-ignored platform should still be collidable");
assert_eq!(collision.line_id.polygon, 0);
assert_eq!(collision.hit_position, FGVec2::lit("0", "0"));
}
}
+3 -2
View File
@@ -105,8 +105,8 @@ pub fn update_ecb(
let left = FGVec2::new(min_x, midpoint_height); let left = FGVec2::new(min_x, midpoint_height);
let right = FGVec2::new(max_x, midpoint_height); let right = FGVec2::new(max_x, midpoint_height);
let ecb_lock_frames_remaining = if let FighterSurfaceState::Airborne { ecb_lock_frames_remaining } = *surface_state { let ecb_lock_frames_remaining = if let FighterSurfaceState::Airborne { ecb_lock_frames_remaining, .. } = surface_state {
ecb_lock_frames_remaining *ecb_lock_frames_remaining
} else { } else {
0 0
}; };
@@ -129,6 +129,7 @@ pub fn update_ecb(
pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) { pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) {
for mut state in surface_states { for mut state in surface_states {
state.tick_ecb_lock(); state.tick_ecb_lock();
state.tick_ignored_polygons();
} }
} }
+1 -1
View File
@@ -206,7 +206,7 @@ impl Plugin for FighterPlugin {
.checksum_component_with_hash::<FighterPreviousFacingDirection>() .checksum_component_with_hash::<FighterPreviousFacingDirection>()
.rollback_component_with_copy::<animation::FighterAnimationFrame>() .rollback_component_with_copy::<animation::FighterAnimationFrame>()
.checksum_component_with_hash::<animation::FighterAnimationFrame>() .checksum_component_with_hash::<animation::FighterAnimationFrame>()
.rollback_component_with_copy::<FighterSurfaceState>() .rollback_component_with_clone::<FighterSurfaceState>()
.checksum_component_with_hash::<FighterSurfaceState>() .checksum_component_with_hash::<FighterSurfaceState>()
.rollback_component_with_copy::<FighterFacingDirection>() .rollback_component_with_copy::<FighterFacingDirection>()
.rollback_component_with_copy::<FighterDamage>() .rollback_component_with_copy::<FighterDamage>()
+2
View File
@@ -199,12 +199,14 @@ impl CollisionContext<'_> {
pub fn air_collide_with_stage( pub fn air_collide_with_stage(
&self, &self,
) -> Option<crate::fighter::collision::AirCollisionWithStageResult> { ) -> Option<crate::fighter::collision::AirCollisionWithStageResult> {
let ignored_polys = self.surface.ignored_polygon_ids();
crate::fighter::collision::air_collide_with_stage( crate::fighter::collision::air_collide_with_stage(
self.prev_translation, self.prev_translation,
self.translation, self.translation,
self.prev_ecb, self.prev_ecb,
self.ecb, self.ecb,
self.stage_collision, self.stage_collision,
&ignored_polys,
) )
} }
} }
+100 -11
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::{stage::line::StageLineID}; use crate::stage::line::StageLineID;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum EcbLockPolicy { pub enum EcbLockPolicy {
@@ -15,10 +15,17 @@ pub enum EcbLockPolicy {
/// This is physics state rather than fighter-state-machine state. Keeping it /// This is physics state rather than fighter-state-machine state. Keeping it
/// separate means grounded, landing, and hitstun states all share one source /// separate means grounded, landing, and hitstun states all share one source
/// of truth for stage contact. /// of truth for stage contact.
#[derive(Component, Clone, Copy, Debug, Hash, PartialEq, Eq)] #[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 { pub enum FighterSurfaceState {
Airborne { Airborne {
ecb_lock_frames_remaining: u32 ecb_lock_frames_remaining: u32,
ignored_polys: Vec<IgnoredPolygon>,
}, },
Grounded { Grounded {
line_id: StageLineID, line_id: StageLineID,
@@ -28,20 +35,21 @@ pub enum FighterSurfaceState {
impl Default for FighterSurfaceState { impl Default for FighterSurfaceState {
fn default() -> Self { fn default() -> Self {
Self::Airborne { Self::Airborne {
ecb_lock_frames_remaining: 0 ecb_lock_frames_remaining: 0,
ignored_polys: vec![],
} }
} }
} }
impl FighterSurfaceState { impl FighterSurfaceState {
pub fn is_grounded(self) -> bool { pub fn is_grounded(&self) -> bool {
matches!(self, Self::Grounded { .. }) matches!(self, Self::Grounded { .. })
} }
pub fn line_id(self) -> Option<StageLineID> { pub fn line_id(&self) -> Option<StageLineID> {
match self { match self {
Self::Airborne { .. } => None, Self::Airborne { .. } => None,
Self::Grounded { line_id } => Some(line_id), Self::Grounded { line_id } => Some(*line_id),
} }
} }
@@ -49,13 +57,46 @@ impl FighterSurfaceState {
*self = Self::Grounded { line_id }; *self = Self::Grounded { line_id };
} }
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( pub(crate) fn apply_airborne_policy(
&mut self, &mut self,
policy: EcbLockPolicy, policy: EcbLockPolicy,
ecb_lock_duration: u32, ecb_lock_duration: u32,
) { ) {
let current_lock = match *self { let current_lock = match self {
Self::Airborne { ecb_lock_frames_remaining } => ecb_lock_frames_remaining, Self::Airborne { ecb_lock_frames_remaining, .. } => *ecb_lock_frames_remaining,
Self::Grounded { .. } => 0, Self::Grounded { .. } => 0,
}; };
let ecb_lock_frames_remaining = match policy { let ecb_lock_frames_remaining = match policy {
@@ -65,14 +106,27 @@ impl FighterSurfaceState {
EcbLockPolicy::Preserve => current_lock, EcbLockPolicy::Preserve => current_lock,
EcbLockPolicy::Clear => 0, EcbLockPolicy::Clear => 0,
}; };
*self = Self::Airborne { ecb_lock_frames_remaining }; 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) { pub(crate) fn tick_ecb_lock(&mut self) {
if let Self::Airborne { ecb_lock_frames_remaining } = self { if let Self::Airborne { ecb_lock_frames_remaining, .. } = self {
*ecb_lock_frames_remaining = ecb_lock_frames_remaining.saturating_sub(1); *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)] #[cfg(test)]
@@ -109,6 +163,7 @@ mod tests {
surface, surface,
FighterSurfaceState::Airborne { FighterSurfaceState::Airborne {
ecb_lock_frames_remaining: 4, ecb_lock_frames_remaining: 4,
ignored_polys: vec![],
} }
); );
} }
@@ -124,7 +179,41 @@ mod tests {
surface, surface,
FighterSurfaceState::Airborne { FighterSurfaceState::Airborne {
ecb_lock_frames_remaining: 5, 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));
}
} }