From 66a817cd059db166283e8a787beaa84fc081e195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Fri, 25 Sep 2026 21:43:49 +0000 Subject: [PATCH] Add mechanism to ignore platforms (#63) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewed-on: https://git.eirexe.moe/EIREXE/shinespark/pulls/63 Co-authored-by: Álex Román Núñez --- src/debug_tools/save_state.rs | 4 +- src/fighter/collision.rs | 53 ++++++++++++++++ src/fighter/ecb.rs | 5 +- src/fighter/mod.rs | 2 +- src/fighter/state/mod.rs | 2 + src/fighter/surface.rs | 111 ++++++++++++++++++++++++++++++---- 6 files changed, 161 insertions(+), 16 deletions(-) diff --git a/src/debug_tools/save_state.rs b/src/debug_tools/save_state.rs index 9e84422..70405c9 100644 --- a/src/debug_tools/save_state.rs +++ b/src/debug_tools/save_state.rs @@ -153,7 +153,7 @@ pub(super) fn save_or_restore(world: &mut World) { entity.insert(saved.hitboxes.clone()); entity.insert(saved.solved_hurtboxes.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.stage_collision); @@ -187,7 +187,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option { hitboxes: fighter.get::()?.clone(), solved_hurtboxes: fighter.get::()?.clone(), bone_matrices: fighter.get::()?.clone(), - surface: *fighter.get::()?, + surface: fighter.get::()?.clone(), }) } diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs index b93ed3e..9164259 100644 --- a/src/fighter/collision.rs +++ b/src/fighter/collision.rs @@ -44,6 +44,7 @@ pub fn air_collide_with_stage( prev_ecb: &FighterPreviousECB, ecb: &FighterECB, stage_collision: &StageCollision, + ignored_polys: &[usize], ) -> Option { let ray_segment = FGSegment2d::new( prev_translation.0 + prev_ecb.get_bottom_point(), @@ -54,6 +55,7 @@ pub fn air_collide_with_stage( .stage_polys .iter() .enumerate() + .filter(|(poly_idx, _)| !ignored_polys.contains(poly_idx)) .filter_map(|(poly_idx, poly)| { 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)) .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")); + } +} diff --git a/src/fighter/ecb.rs b/src/fighter/ecb.rs index 998b38f..ea60692 100644 --- a/src/fighter/ecb.rs +++ b/src/fighter/ecb.rs @@ -105,8 +105,8 @@ pub fn update_ecb( let left = FGVec2::new(min_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 { - ecb_lock_frames_remaining + let ecb_lock_frames_remaining = if let FighterSurfaceState::Airborne { ecb_lock_frames_remaining, .. } = surface_state { + *ecb_lock_frames_remaining } else { 0 }; @@ -129,6 +129,7 @@ pub fn update_ecb( pub fn decrement_ecb_lockout(surface_states: Query<&mut FighterSurfaceState>) { for mut state in surface_states { state.tick_ecb_lock(); + state.tick_ignored_polygons(); } } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 9c24b71..4700ea4 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -206,7 +206,7 @@ impl Plugin for FighterPlugin { .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() - .rollback_component_with_copy::() + .rollback_component_with_clone::() .checksum_component_with_hash::() .rollback_component_with_copy::() .rollback_component_with_copy::() diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 469e14d..8e9a38a 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -199,12 +199,14 @@ impl CollisionContext<'_> { pub fn air_collide_with_stage( &self, ) -> Option { + let ignored_polys = self.surface.ignored_polygon_ids(); crate::fighter::collision::air_collide_with_stage( self.prev_translation, self.translation, self.prev_ecb, self.ecb, self.stage_collision, + &ignored_polys, ) } } diff --git a/src/fighter/surface.rs b/src/fighter/surface.rs index 4845127..306eb6e 100644 --- a/src/fighter/surface.rs +++ b/src/fighter/surface.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use crate::{stage::line::StageLineID}; +use crate::stage::line::StageLineID; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum EcbLockPolicy { @@ -15,10 +15,17 @@ pub enum EcbLockPolicy { /// 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)] +#[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 { Airborne { - ecb_lock_frames_remaining: u32 + ecb_lock_frames_remaining: u32, + ignored_polys: Vec, }, Grounded { line_id: StageLineID, @@ -28,20 +35,21 @@ pub enum FighterSurfaceState { impl Default for FighterSurfaceState { fn default() -> Self { Self::Airborne { - ecb_lock_frames_remaining: 0 + ecb_lock_frames_remaining: 0, + ignored_polys: vec![], } } } impl FighterSurfaceState { - pub fn is_grounded(self) -> bool { + pub fn is_grounded(&self) -> bool { matches!(self, Self::Grounded { .. }) } - pub fn line_id(self) -> Option { + pub fn line_id(&self) -> Option { match self { 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 }; } + 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 { + match self { + Self::Airborne { ignored_polys, .. } => { + ignored_polys.iter().map(|ignored| ignored.polygon).collect() + } + Self::Grounded { .. } => vec![], + } + } + 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, + 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 { @@ -65,14 +106,27 @@ impl FighterSurfaceState { EcbLockPolicy::Preserve => current_lock, 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) { - 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); } } + + 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)] @@ -109,6 +163,7 @@ mod tests { surface, FighterSurfaceState::Airborne { ecb_lock_frames_remaining: 4, + ignored_polys: vec![], } ); } @@ -124,7 +179,41 @@ mod tests { surface, FighterSurfaceState::Airborne { 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)); + } }