From 5be69cab52d520ce43d7e6ec25dd356a3a6db558 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Tue, 13 Oct 2026 22:49:56 +0200 Subject: [PATCH 1/2] Add waveland snapping --- src/fighter/state/air_dodge.rs | 35 +++++++++++++++++++++++----------- src/fighter/state/mod.rs | 3 +++ src/game_settings.rs | 2 ++ src/input/buffer.rs | 1 - src/stage/debug.rs | 20 ++++++++++++++----- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/fighter/state/air_dodge.rs b/src/fighter/state/air_dodge.rs index b993201..e03608c 100644 --- a/src/fighter/state/air_dodge.rs +++ b/src/fighter/state/air_dodge.rs @@ -1,17 +1,9 @@ use crate::{ fighter::{ - FighterInvulnerabilityState, - animation::AnimKind, - state::{ - CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, - UpdateContext, - air::{self, AirborneStateCommon}, - fall::FallState, - land::LandingType, + FighterInvulnerabilityState, animation::AnimKind, state::{ + CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, land::{LandingState, LandingType}, }, - }, - input, - math::vec::FGVec2, + }, input, math::vec::FGVec2, stage::line::{StageLineID, StagePolyLineSegmentType}, }; #[derive(Debug, Clone, Hash)] @@ -52,6 +44,27 @@ impl FighterStateImpl for AirDodgeState { &mut self, state_context: &mut CollisionContext, ) -> Option { + let can_wavesnap = self.direction.y.is_zero() || self.direction.y.is_negative(); + + if can_wavesnap { + for (poly_idx, poly) in state_context.stage_collision.stage_polys.iter().enumerate() { + for (segment_idx, segment) in poly.segments.iter().enumerate() { + if !matches!(segment.segment_type, StagePolyLineSegmentType::Floor) { + continue; + } + let bottom_point = state_context.translation.0 + state_context.ecb.get_bottom_point(); + let point_in_segment = segment.segment.closest_point(bottom_point); + let dist_to_segment = point_in_segment.distance(bottom_point); + if dist_to_segment <= state_context.waveland_snap_distance { + state_context.ecb.bottom = point_in_segment; + state_context.ecb.rebase_onto_bottom(); + state_context.surface.land(StageLineID { polygon: poly_idx, segment: segment_idx }, state_context.ledge_info); + return Some(FighterState::Land(LandingState::create(LandingType::Special))); + } + } + } + } + air::collision_interrupt(state_context, LandingType::Special, false) } diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index a465453..f6f1d7e 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -260,6 +260,7 @@ pub struct CollisionContext<'a> { pub ledge_info: &'a mut FighterLedgeInformation, pub fighter_ledge_collision_box: FGRect, pub downward_stick_threshold: FGi32, + pub waveland_snap_distance: FGi32, pub(crate) ecb_advanced: bool, } @@ -503,6 +504,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { downward_stick_threshold: game_settings.input_common.run_stick_threshold, fighter_ledge_collision_box, ecb_advanced: false, + waveland_snap_distance: game_settings.fighter_common.waveland_snap_distance, } } } @@ -891,6 +893,7 @@ mod tests { }, downward_stick_threshold: FGi32::ONE, ecb_advanced: false, + waveland_snap_distance: FGi32::ZERO, }; assert!(context.air_collide_with_ledges().is_none()); diff --git a/src/game_settings.rs b/src/game_settings.rs index bcf4169..09c922d 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -66,6 +66,8 @@ pub struct FighterSettingsCommon { pub sakurai_angle_high_knockback_angle: FGi32, /// past this threshold, the knockback is considered high, so it will use [sakurai_angle_high_knockback_angle] pub sakurai_angle_knockback_threshold: FGi32, + + pub waveland_snap_distance: FGi32, } #[derive(Reflect, Serialize, Deserialize, Clone)] #[reflect(opaque)] diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 8b6e183..e8fcb8b 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -717,7 +717,6 @@ pub fn postprocess_input( FighterAttackCommandType::Neutral }; - dbg!(input.movement_flick.y, attack_type); input.set_lifetime(FighterCommands::Attack, tap); input.set_attack_command_type(attack_type); } diff --git a/src/stage/debug.rs b/src/stage/debug.rs index 4a3bb12..d0b3234 100644 --- a/src/stage/debug.rs +++ b/src/stage/debug.rs @@ -1,15 +1,15 @@ use bevy::{color::palettes::css::RED, prelude::*}; use crate::{ - match_loading::MatchConfig, math::{int::FGi32, vec3::FGVec3}, stage::{ - line::{StageCollision, StageLedgeSide}, - manifest::{StageManifest, StageManifestRegistry}, + game_settings::GameSettings, match_loading::MatchConfig, math::{int::FGi32, vec3::FGVec3}, stage::{ + line::{StageCollision, StageLedgeSide, StagePolyLineSegmentType}, manifest::{StageManifest, StageManifestRegistry}, }, }; -pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res) { +pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res, game_settings: Res) { for poly in stage_collision.stage_polys.iter() { - for segment in &poly.segments { + for (segment, next_segment) in poly.segments.iter().zip(poly.segments.iter().cycle().skip(1)) { + let wavedash_normal = ((next_segment.normal.to_vec2() + segment.normal.to_vec2()) * 0.5).normalize(); let from = segment.segment.point1().to_vec2(); let to = segment.segment.point2().to_vec2(); let from_3d = Vec3::new(from.x, from.y, 0.0); @@ -18,6 +18,16 @@ pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res gizmos.line(from_3d, end_3d, RED); let midpoint = (from_3d + end_3d) * 0.5; gizmos.arrow(midpoint, midpoint + normal_3d, RED); + if matches!(segment.segment_type, StagePolyLineSegmentType::Floor) { + let radius = game_settings.fighter_common.waveland_snap_distance.to_num::(); + let a = segment.segment.point1().to_vec2(); + let a = Vec3::new(a.x, a.y, 0.0); + let b = segment.segment.point2().to_vec2(); + let b = Vec3::new(b.x, b.y, 0.0); + let offset = Vec3::new(wavedash_normal.x, wavedash_normal.y, 0.0) * radius; + gizmos.line(a + offset, b + offset, bevy::color::palettes::css::HOT_PINK); + gizmos.line(a - offset, b - offset, bevy::color::palettes::css::HOT_PINK); + } } } -- 2.54.0 From 42945449445fa3c3b3f3f8b52918e8cb8b976f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Tue, 13 Oct 2026 23:16:08 +0200 Subject: [PATCH 2/2] Fix unit tests and remove advanced_ecb --- src/fighter/state/air.rs | 1 + src/fighter/state/air_dodge.rs | 2 +- src/fighter/state/angel.rs | 5 +++-- src/fighter/state/ground.rs | 7 ++----- src/fighter/state/ledge_action.rs | 3 ++- src/fighter/state/ledge_wait.rs | 3 ++- src/fighter/state/mod.rs | 33 ++++++++----------------------- 7 files changed, 19 insertions(+), 35 deletions(-) diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index e8d3415..ed9c8ed 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -129,6 +129,7 @@ pub fn collision_interrupt( ) -> Option { if ctx.velocity.y <= FGi32::ZERO { if let Some(ledge_result) = ctx.air_collide_with_ledges() { + ctx.apply_desired_ecb(); return Some(FighterState::LedgeGrab(LedgeGrabState::new(ledge_result.ledge))); } } diff --git a/src/fighter/state/air_dodge.rs b/src/fighter/state/air_dodge.rs index e03608c..9d5f2f9 100644 --- a/src/fighter/state/air_dodge.rs +++ b/src/fighter/state/air_dodge.rs @@ -56,7 +56,7 @@ impl FighterStateImpl for AirDodgeState { let point_in_segment = segment.segment.closest_point(bottom_point); let dist_to_segment = point_in_segment.distance(bottom_point); if dist_to_segment <= state_context.waveland_snap_distance { - state_context.ecb.bottom = point_in_segment; + state_context.ecb.bottom = point_in_segment - state_context.translation.0; state_context.ecb.rebase_onto_bottom(); state_context.surface.land(StageLineID { polygon: poly_idx, segment: segment_idx }, state_context.ledge_info); return Some(FighterState::Land(LandingState::create(LandingType::Special))); diff --git a/src/fighter/state/angel.rs b/src/fighter/state/angel.rs index 6d021ce..fb5dcbe 100644 --- a/src/fighter/state/angel.rs +++ b/src/fighter/state/angel.rs @@ -43,12 +43,13 @@ impl FighterStateImpl for AngelState { fn check_collision_interrupt( &mut self, - _state_context: &mut super::CollisionContext, + state_context: &mut super::CollisionContext, ) -> Option { + state_context.apply_desired_ecb(); None } fn surface_policy(&self) -> super::SurfacePolicy { super::SurfacePolicy::Airborne(crate::fighter::surface::EcbLockPolicy::Clear) } -} \ No newline at end of file +} diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 039ef75..9b124d6 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -211,7 +211,6 @@ pub fn collide_with_stage_grounded( crate::fighter::surface::EcbLockPolicy::StartOnDetach, state_context.leave_ground_ecb_lock_duration, ); - state_context.ecb_advanced = true; return GroundedMotionResult::InAir; }; match resolve_grounded_position( @@ -238,7 +237,6 @@ pub fn collide_with_stage_grounded( crate::fighter::surface::EcbLockPolicy::StartOnDetach, state_context.leave_ground_ecb_lock_duration, ); - state_context.ecb_advanced = true; return GroundedMotionResult::InAir; } } @@ -247,7 +245,6 @@ pub fn collide_with_stage_grounded( *state_context.ecb = current_ecb; state_context.translation.0 = current_position; state_context.surface.land(line_id, state_context.ledge_info); - state_context.ecb_advanced = true; GroundedMotionResult::InGround } @@ -419,7 +416,7 @@ mod tests { origin: FGVec2::ZERO, end: FGVec2::ZERO, }, - ecb_advanced: false, + waveland_snap_distance: FGi32::ZERO, }; assert!(matches!( @@ -484,7 +481,7 @@ mod tests { origin: FGVec2::ZERO, end: FGVec2::ZERO, }, - ecb_advanced: false, + waveland_snap_distance: FGi32::ZERO, }; assert!(matches!( diff --git a/src/fighter/state/ledge_action.rs b/src/fighter/state/ledge_action.rs index 04d8528..4c2dca5 100644 --- a/src/fighter/state/ledge_action.rs +++ b/src/fighter/state/ledge_action.rs @@ -144,8 +144,9 @@ impl FighterStateImpl for LedgeActionState { fn check_collision_interrupt( &mut self, - _state_context: &mut CollisionContext, + state_context: &mut CollisionContext, ) -> Option { + state_context.apply_desired_ecb(); None } diff --git a/src/fighter/state/ledge_wait.rs b/src/fighter/state/ledge_wait.rs index 9fe5524..c401dfe 100644 --- a/src/fighter/state/ledge_wait.rs +++ b/src/fighter/state/ledge_wait.rs @@ -102,8 +102,9 @@ impl FighterStateImpl for LedgeWaitState { fn check_collision_interrupt( &mut self, - _state_context: &mut CollisionContext, + state_context: &mut CollisionContext, ) -> Option { + state_context.apply_desired_ecb(); None } diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index f6f1d7e..156a471 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -120,8 +120,9 @@ pub trait FighterStateImpl: Sized { fn check_collision_interrupt( &mut self, - _state_context: &mut CollisionContext, + state_context: &mut CollisionContext, ) -> Option { + state_context.apply_desired_ecb(); None } @@ -261,7 +262,6 @@ pub struct CollisionContext<'a> { pub fighter_ledge_collision_box: FGRect, pub downward_stick_threshold: FGi32, pub waveland_snap_distance: FGi32, - pub(crate) ecb_advanced: bool, } impl CollisionContext<'_> { @@ -269,7 +269,6 @@ impl CollisionContext<'_> { /// substep resolver can later replace this with fractional steps. pub fn apply_desired_ecb(&mut self) { ecb::interpolate_ecb(self.ecb, *self.desired_ecb, FGi32::ONE); - self.ecb_advanced = true; } pub fn air_collide_with_stage( @@ -279,7 +278,7 @@ impl CollisionContext<'_> { let ignored_polys = self.surface.ignored_polygon_ids(); let ignore_soft_platforms = can_ignore_soft_platforms && self.input.get_last_frame().movement.y <= -self.downward_stick_threshold; - let result = crate::fighter::collision::air_collide_with_stage_substepped( + crate::fighter::collision::air_collide_with_stage_substepped( self.prev_translation, self.translation, self.velocity, @@ -288,15 +287,7 @@ impl CollisionContext<'_> { self.stage_collision, &ignored_polys, ignore_soft_platforms, - ); - self.ecb_advanced = true; - result - } - - fn finish_ecb_frame(&mut self) { - if !self.ecb_advanced { - self.apply_desired_ecb(); - } + ) } pub fn air_collide_with_ledges( &self, @@ -503,7 +494,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { .leave_ground_ecb_frame_lock_duration, downward_stick_threshold: game_settings.input_common.run_stick_threshold, fighter_ledge_collision_box, - ecb_advanced: false, waveland_snap_distance: game_settings.fighter_common.waveland_snap_distance, } } @@ -799,13 +789,9 @@ pub fn state_collision_interrupt_system( continue; } - let new_state = { - let mut collision_context = - frame.collision_context(&stage_collision, manifest, &game_settings); - let new_state = state.check_collision_interrupt(&mut collision_context); - collision_context.finish_ecb_frame(); - new_state - }; + let new_state = state.check_collision_interrupt( + &mut frame.collision_context(&stage_collision, manifest, &game_settings), + ); if let Some(new_state) = new_state { transition_state( &mut state, @@ -874,7 +860,7 @@ mod tests { }; let previous_ecb = FighterPreviousECB::from(ecb); let mut presentation_snap = FighterPresentationSnap::default(); - let mut context = CollisionContext { + let context = CollisionContext { translation: &mut translation, prev_translation: &previous_translation, input: &input, @@ -892,7 +878,6 @@ mod tests { end: FGVec2::lit("1", "-1"), }, downward_stick_threshold: FGi32::ONE, - ecb_advanced: false, waveland_snap_distance: FGi32::ZERO, }; @@ -902,7 +887,5 @@ mod tests { context.air_collide_with_ledges().map(|result| result.ledge), Some(ledge_id) ); - context.finish_ecb_frame(); - assert_eq!(context.ecb.top, FGVec2::lit("0", "2")); } } -- 2.54.0