From 81e60d9acb93bc71135b82a1b960126a6ab428c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 26 Sep 2026 13:03:14 +0200 Subject: [PATCH 1/2] Allow airborne fighters to pass through platforms while holding down --- src/fighter/collision.rs | 88 ++++++++++++++++++++++++++++++++++++- src/fighter/state/ground.rs | 4 ++ src/fighter/state/mod.rs | 9 +++- 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs index 9164259..4ea1b55 100644 --- a/src/fighter/collision.rs +++ b/src/fighter/collision.rs @@ -4,7 +4,7 @@ use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation}; use crate::math::segment::FGSegment2d; use crate::math::vec::FGVec2; -use crate::stage::line::{StageCollision, StageLineID}; +use crate::stage::line::{StageCollision, StageLineID, StagePolyType}; /* pub fn collide_fighter_with_scene( fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>, @@ -45,6 +45,7 @@ pub fn air_collide_with_stage( ecb: &FighterECB, stage_collision: &StageCollision, ignored_polys: &[usize], + ignore_soft_platforms: bool, ) -> Option { let ray_segment = FGSegment2d::new( prev_translation.0 + prev_ecb.get_bottom_point(), @@ -55,7 +56,10 @@ pub fn air_collide_with_stage( .stage_polys .iter() .enumerate() - .filter(|(poly_idx, _)| !ignored_polys.contains(poly_idx)) + .filter(|(poly_idx, poly)| { + !ignored_polys.contains(poly_idx) + && !(ignore_soft_platforms && matches!(poly.poly_type, StagePolyType::Platform)) + }) .filter_map(|(poly_idx, poly)| { poly.intersect_ray(ray_segment).map(|intersect_result| { ( @@ -95,6 +99,18 @@ mod tests { ) } + fn solid_floor() -> StagePoly { + StagePoly::build( + StagePolyType::Closed, + &[ + (FGVec2::lit("-2", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("2", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("2", "-2"), StagePolyLineSegmentType::Wall), + (FGVec2::lit("-2", "-2"), StagePolyLineSegmentType::Wall), + ], + ) + } + #[test] fn airborne_collision_skips_ignored_polygons() { let stage_collision = StageCollision { @@ -122,10 +138,78 @@ mod tests { &ecb, &stage_collision, &[1], + false, ) .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")); } + + #[test] + fn airborne_collision_skips_soft_platforms_while_passing_down() { + let stage_collision = StageCollision { + stage_polys: vec![horizontal_platform("0")], + }; + 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, + }; + + assert!(air_collide_with_stage( + &previous_translation, + &translation, + &previous_ecb, + &ecb, + &stage_collision, + &[], + true, + ) + .is_none()); + } + + #[test] + fn airborne_collision_still_hits_solid_floors_while_passing_down() { + let stage_collision = StageCollision { + stage_polys: vec![solid_floor()], + }; + 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, + &[], + true, + ) + .expect("solid floors remain collidable while passing down"); + + assert_eq!(collision.line_id.polygon, 0); + assert_eq!(collision.hit_position, FGVec2::lit("0", "0")); + } } diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index bf7ef58..760af72 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -378,12 +378,14 @@ mod tests { let mut context = CollisionContext { translation: &mut translation, prev_translation: &previous_translation, + input: &FighterInput::default(), velocity: &mut velocity, surface: &mut surface, ecb: &mut ecb, prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 10, + downward_stick_threshold: FGi32::ONE, }; assert!(matches!( @@ -429,12 +431,14 @@ mod tests { let mut context = CollisionContext { translation: &mut translation, prev_translation: &previous_translation, + input: &FighterInput::default(), velocity: &mut velocity, surface: &mut surface, ecb: &mut ecb, prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 10, + downward_stick_threshold: FGi32::ONE, }; assert!(matches!( diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 5d66460..85fca7f 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -193,12 +193,14 @@ impl UpdateContext<'_> { pub struct CollisionContext<'a> { pub translation: &'a mut FighterTranslation, pub prev_translation: &'a FighterPreviousTranslation, + pub input: &'a FighterInput, pub velocity: &'a mut FighterVelocity, pub surface: &'a mut FighterSurfaceState, pub ecb: &'a mut FighterECB, pub prev_ecb: &'a FighterPreviousECB, pub stage_collision: &'a StageCollision, pub leave_ground_ecb_lock_duration: u32, + pub downward_stick_threshold: FGi32, } impl CollisionContext<'_> { @@ -206,6 +208,8 @@ impl CollisionContext<'_> { &self, ) -> Option { let ignored_polys = self.surface.ignored_polygon_ids(); + let ignore_soft_platforms = self.input.get_last_frame().movement.y + <= -self.downward_stick_threshold; crate::fighter::collision::air_collide_with_stage( self.prev_translation, self.translation, @@ -213,6 +217,7 @@ impl CollisionContext<'_> { self.ecb, self.stage_collision, &ignored_polys, + ignore_soft_platforms, ) } } @@ -349,12 +354,14 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { CollisionContext { translation: &mut self.translation, prev_translation: &self.prev_translation, + input: &self.input, velocity: &mut self.velocity, surface: &mut self.surface, ecb: &mut self.ecb, prev_ecb: &self.prev_ecb, stage_collision, - leave_ground_ecb_lock_duration: game_settings.fighter_common.leave_ground_ecb_frame_lock_duration + leave_ground_ecb_lock_duration: game_settings.fighter_common.leave_ground_ecb_frame_lock_duration, + downward_stick_threshold: game_settings.input_common.run_stick_threshold, } } } -- 2.54.0 From e7a35a095ebbdcc35d2cb09ae692f7b9413cbb87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 26 Sep 2026 13:26:50 +0200 Subject: [PATCH 2/2] Fix build --- src/fighter/state/ground.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 760af72..cf64667 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -248,6 +248,7 @@ mod tests { use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::{FighterECB, FighterSurfaceState}; use crate::stage::line::StagePolyType; + use crate::input::FighterInput; fn two_segment_floor() -> StagePoly { StagePoly::build( -- 2.54.0