From 7819b95bc6d62a1d2725f60e1d81084d9d55e0cf 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 12:14:12 +0200 Subject: [PATCH] Add ledge getup, ledge intangibility limit & fix walk-off velocity --- src/fighter/animation.rs | 2 + src/fighter/mod.rs | 37 ++++++++-------- src/fighter/state/air.rs | 9 ++-- src/fighter/state/fall.rs | 1 + src/fighter/state/ground.rs | 10 +++-- src/fighter/state/ledge_action.rs | 23 +++++++++- src/fighter/state/ledge_grab.rs | 73 +++++++++++++++++++++++++++++++ src/fighter/state/ledge_wait.rs | 38 +++++++++++++--- src/fighter/state/mod.rs | 26 ++++++----- src/fighter/surface.rs | 18 +++++--- src/game_settings.rs | 12 +++-- 11 files changed, 195 insertions(+), 54 deletions(-) create mode 100644 src/fighter/state/ledge_grab.rs diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index 6d78db4..e84fba0 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -89,6 +89,8 @@ pub enum AnimKind { CrouchExit, LedgeWait, LedgeJump, + LedgeGetUp, + LedgeGrab, PlatformPass, diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index eda26b1..8ae863e 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -87,46 +87,47 @@ pub struct FighterHitstop { } #[derive(Component, Default, Copy, Clone, Hash, Debug, PartialEq, Eq)] -pub struct FighterLedgeGrabLockout { - pub remaining_frames: u32, +pub struct FighterLedgeInformation { + pub regrab_lockout_remaining_frames: u32, + pub grab_count: u32, } -impl FighterLedgeGrabLockout { +impl FighterLedgeInformation { pub fn start(&mut self, duration_frames: u32) { - self.remaining_frames = duration_frames; + self.regrab_lockout_remaining_frames = duration_frames; } - pub fn is_active(&self) -> bool { - self.remaining_frames > 0 + pub fn is_regrab_lockout_active(&self) -> bool { + self.regrab_lockout_remaining_frames > 0 } fn tick(&mut self) { - self.remaining_frames = self.remaining_frames.saturating_sub(1); + self.regrab_lockout_remaining_frames = self.regrab_lockout_remaining_frames.saturating_sub(1); } } #[cfg(test)] mod ledge_grab_lockout_tests { - use super::FighterLedgeGrabLockout; + use super::FighterLedgeInformation; #[test] fn lockout_expires_after_the_requested_number_of_ticks() { - let mut lockout = FighterLedgeGrabLockout::default(); + let mut lockout = FighterLedgeInformation::default(); lockout.start(2); - assert!(lockout.is_active()); + assert!(lockout.is_regrab_lockout_active()); lockout.tick(); - assert!(lockout.is_active()); + assert!(lockout.is_regrab_lockout_active()); lockout.tick(); - assert!(!lockout.is_active()); + assert!(!lockout.is_regrab_lockout_active()); } #[test] fn zero_duration_does_not_enable_lockout() { - let mut lockout = FighterLedgeGrabLockout::default(); + let mut lockout = FighterLedgeInformation::default(); lockout.start(0); - assert!(!lockout.is_active()); + assert!(!lockout.is_regrab_lockout_active()); } } @@ -192,7 +193,7 @@ pub struct FighterPendingRespawn(bool); FighterPendingHitReaction, FighterPendingRespawn, FighterHitstop, - FighterLedgeGrabLockout, + FighterLedgeInformation, FighterKnockbackVelocity, FighterTranslation, FighterInvulnerabilityState, @@ -298,8 +299,8 @@ impl Plugin for FighterPlugin { .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() - .rollback_component_with_copy::() - .checksum_component_with_hash::() + .rollback_component_with_copy::() + .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() .rollback_component_with_copy::() @@ -430,7 +431,7 @@ pub fn decay_hitstop(query: Query<(&mut FighterHitstop, &mut FighterHitstopShake } } -pub fn decay_ledge_grab_lockout(query: Query<&mut FighterLedgeGrabLockout>) { +pub fn decay_ledge_grab_lockout(query: Query<&mut FighterLedgeInformation>) { for mut lockout in query { lockout.tick(); } diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index 0a835ff..b3d8ef2 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -1,6 +1,6 @@ use crate::fighter::state::jump::{self}; use crate::fighter::state::land::{LandingState, LandingType}; -use crate::fighter::state::ledge_wait::LedgeWaitState; +use crate::fighter::state::ledge_grab::LedgeGrabState; use crate::fighter::state::{ CollisionContext, FighterState, InterruptContext, air_attack, air_dodge, }; @@ -129,10 +129,7 @@ pub fn collision_interrupt( ) -> Option { if ctx.velocity.y <= FGi32::ZERO { if let Some(ledge_result) = ctx.air_collide_with_ledges() { - return Some(FighterState::LedgeWait(LedgeWaitState { - airborne_state_common: AirborneStateCommon::default(), - ledge_id: ledge_result.ledge, - })); + return Some(FighterState::LedgeGrab(LedgeGrabState::new(ledge_result.ledge))); } } @@ -144,7 +141,7 @@ pub fn collision_interrupt( ctx.ecb.right -= diff; ctx.translation.0 = res.hit_position; ctx.presentation_snap.0 = true; - ctx.surface.land(res.line_id); + ctx.surface.land(res.line_id, ctx.ledge_info); Some(FighterState::Land(LandingState::create(landing_type))) } diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index e40f9a8..97945b7 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -25,6 +25,7 @@ impl FighterStateImpl for FallState { fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.play_animation(AnimKind::Fall, true); + state_context.velocity.x = state_context.velocity.x.clamp(-state_context.fighter_attributes.air_drift_max, state_context.fighter_attributes.air_drift_max); } fn update(&mut self, state_context: &mut UpdateContext) { diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 4b1798b..9703f19 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -213,7 +213,7 @@ pub fn collide_with_stage_grounded( state_context.surface.land(StageLineID { polygon: current_line_id.polygon, segment: segment_idx, - }); + }, state_context.ledge_info); GroundedMotionResult::InGround } GroundedPositionResult::InAir => { @@ -246,7 +246,7 @@ pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Op mod tests { use super::*; use crate::fighter::ecb::FighterPreviousECB; - use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterSurfaceState}; + use crate::fighter::{FighterECB, FighterLedgeInformation, FighterPreviousTranslation, FighterSurfaceState}; use crate::math::rect::FGRect; use crate::stage::line::StagePolyType; use crate::input::FighterInput; @@ -371,6 +371,7 @@ use crate::stage::line::StagePolyType; let mut translation = FighterTranslation(FGVec2::lit("11", "0")); let previous_translation = FighterPreviousTranslation(translation.0); let mut velocity = FighterVelocity::default(); + let mut ledge_info = FighterLedgeInformation::default(); let mut ecb = FighterECB { bottom: FGVec2::lit("0.0", "-0.5"), top: FGVec2::lit("0.0", "0.5"), @@ -386,6 +387,7 @@ use crate::stage::line::StagePolyType; velocity: &mut velocity, surface: &mut surface, ecb: &mut ecb, + ledge_info: &mut ledge_info, prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 10, @@ -395,7 +397,6 @@ use crate::stage::line::StagePolyType; origin: FGVec2::ZERO, end: FGVec2::ZERO, }, - ledge_grab_lockout_active: false, }; assert!(matches!( @@ -440,6 +441,7 @@ use crate::stage::line::StagePolyType; }; let previous_ecb = FighterPreviousECB::from(ecb); let mut presentation_snap = crate::fighter::FighterPresentationSnap::default(); + let mut ledge_info = FighterLedgeInformation::default(); let mut context = CollisionContext { translation: &mut translation, prev_translation: &previous_translation, @@ -447,6 +449,7 @@ use crate::stage::line::StagePolyType; velocity: &mut velocity, surface: &mut surface, ecb: &mut ecb, + ledge_info: &mut ledge_info, prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 10, @@ -456,7 +459,6 @@ use crate::stage::line::StagePolyType; origin: FGVec2::ZERO, end: FGVec2::ZERO, }, - ledge_grab_lockout_active: false, }; assert!(matches!( diff --git a/src/fighter/state/ledge_action.rs b/src/fighter/state/ledge_action.rs index d504b9f..04d8528 100644 --- a/src/fighter/state/ledge_action.rs +++ b/src/fighter/state/ledge_action.rs @@ -2,6 +2,7 @@ use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, use crate::fighter::animation::AnimKind; use crate::fighter::state::air::AirborneStateCommon; use crate::fighter::state::jump::{JumpState, JumpStateInitiator, JumpType}; +use crate::fighter::state::wait::WaitState; use crate::fighter::state::{FighterState, ground}; use crate::fighter::FighterFacingDirection; use crate::input::FighterCommands; @@ -11,7 +12,8 @@ use crate::stage::line::{StageLedgeSide, StageVertexID}; #[derive(Debug, Clone, Hash)] pub enum LedgeAction { - Jump(JumpStateInitiator) + Jump(JumpStateInitiator), + GetUp } #[derive(Debug, Clone, Hash)] @@ -29,6 +31,13 @@ impl LedgeActionState { is_in_grounded_phase: false, } } + pub fn create_getup(ledge_id: StageVertexID) -> Self { + LedgeActionState { + ledge_id, + ledge_action: LedgeAction::GetUp, + is_in_grounded_phase: false, + } + } } impl FighterStateImpl for LedgeActionState { @@ -50,6 +59,13 @@ impl FighterStateImpl for LedgeActionState { None } }, + LedgeAction::GetUp => { + if state_context.is_current_animation_finished() { + Some(FighterState::Wait(WaitState)) + } else { + None + } + }, } } @@ -61,6 +77,9 @@ impl FighterStateImpl for LedgeActionState { state_context.input.clear_command(FighterCommands::Jump); state_context.input.clear_command(FighterCommands::TapJump); }, + LedgeAction::GetUp => { + state_context.play_animation(AnimKind::LedgeGetUp, false); + }, } state_context.velocity.0 = FGVec2::ZERO; @@ -105,7 +124,7 @@ impl FighterStateImpl for LedgeActionState { self.is_in_grounded_phase = true; state_context.translation.0 = ledge.position; - state_context.surface.land(segment_id); + state_context.surface.land(segment_id, state_context.ledge_info); } } diff --git a/src/fighter/state/ledge_grab.rs b/src/fighter/state/ledge_grab.rs new file mode 100644 index 0000000..133bbe8 --- /dev/null +++ b/src/fighter/state/ledge_grab.rs @@ -0,0 +1,73 @@ +use super::{ + EnterContext, FighterStateImpl, InterruptContext, UpdateContext, +}; +use crate::fighter::{FighterFacingDirection, FighterInvulnerabilityState}; +use crate::fighter::animation::AnimKind; +use crate::fighter::state::{ExitContext, FighterState}; +use crate::fighter::state::air::AirborneStateCommon; +use crate::fighter::state::ledge_wait::LedgeWaitState; +use crate::stage::line::{StageLedgeSide, StageVertexID}; + +#[derive(Debug, Clone, Hash)] +pub struct LedgeGrabState { + ledge_id: StageVertexID +} + +impl LedgeGrabState { + pub fn new(ledge_id: StageVertexID) -> Self { + Self { + ledge_id + } + } +} + +impl FighterStateImpl for LedgeGrabState { + const NAME: &'static str = "LedgeGrab"; + fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { + if state_context.is_current_animation_finished() { + Some(FighterState::LedgeWait(LedgeWaitState { + airborne_state_common: AirborneStateCommon::default(), + ledge_id: self.ledge_id.clone() + })) + } else { + None + } + } + + fn on_enter(&mut self, state_context: &mut EnterContext) { + state_context.play_animation(AnimKind::LedgeGrab, false); + + *state_context.vulnerability = FighterInvulnerabilityState::Intangible { + frames_remaining: None, + }; + + let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else { + return; + }; + state_context.translation.0 = ledge.position; + *state_context.facing_direction = match ledge.hang_side { + StageLedgeSide::Left => FighterFacingDirection::Right, + StageLedgeSide::Right => FighterFacingDirection::Left, + }; + if let Some(root_position) = state_context.initial_animation_root_position() { + state_context.translation.0 = root_position; + } + state_context.presentation_snap.0 = true; + + + } + + fn on_exit(&mut self, state_context: &mut ExitContext) { + *state_context.vulnerability = FighterInvulnerabilityState::Vulnerable; + } + + fn is_visually_grounded<'a>(&'a self) -> bool { + false + } + + fn update(&mut self, state_context: &mut UpdateContext) { + if let Some(root_position) = state_context.animation_root_position { + state_context.translation.0 = root_position; + } + } +} diff --git a/src/fighter/state/ledge_wait.rs b/src/fighter/state/ledge_wait.rs index 57149e4..109abfb 100644 --- a/src/fighter/state/ledge_wait.rs +++ b/src/fighter/state/ledge_wait.rs @@ -24,6 +24,13 @@ impl FighterStateImpl for LedgeWaitState { } else if state_context.input.has_command(FighterCommands::TapJump) { return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Tap))); } + + if is_ledge_getup_input(state_context.input.get_last_frame().movement.x, + state_context.game_settings.input_common.run_stick_threshold, + state_context.facing_direction) { + return Some(FighterState::LedgeAction(LedgeActionState::create_getup(self.ledge_id))); + } + if is_ledge_drop_input(state_context.input.get_last_frame().movement.x, state_context.game_settings.input_common.run_stick_threshold, state_context.facing_direction) { @@ -58,14 +65,20 @@ impl FighterStateImpl for LedgeWaitState { fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.velocity.0 = FGVec2::ZERO; state_context.knockback_velocity.0 = FGVec2::ZERO; - *state_context.vulnerability = FighterInvulnerabilityState::Intangible { - frames_remaining: Some( - state_context + + if state_context.ledge_info.grab_count < state_context.game_settings.fighter_common.max_ledge_regrab_count { + *state_context.vulnerability = FighterInvulnerabilityState::Intangible { + frames_remaining: Some( + state_context .game_settings .fighter_common .ledge_grab_invulnerability_duration, - ), - }; + ), + }; + } + + state_context.ledge_info.grab_count = state_context.ledge_info.grab_count.saturating_add(1); + let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else { return; }; @@ -135,3 +148,18 @@ fn is_ledge_drop_input( false } + +fn is_ledge_getup_input( + horizontal_input: crate::math::int::FGi32, + threshold: crate::math::int::FGi32, + facing_dir: &FighterFacingDirection, +) -> bool { + let dir = facing_dir.to_sign(); + if horizontal_input.abs() > threshold { + if dir == horizontal_input.signum() { + return true; + } + } + + false +} diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 466f6c1..7707c31 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -20,7 +20,7 @@ use crate::fighter::visual::FighterAnimations; use crate::fighter::{ Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, - FighterInvulnerabilityState, FighterLedgeGrabLockout, FighterPendingRespawn, + FighterInvulnerabilityState, FighterLedgeInformation, FighterPendingRespawn, FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, }; @@ -56,6 +56,7 @@ pub mod wait; pub mod walk; pub mod ledge_wait; pub mod ledge_action; +pub mod ledge_grab; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum SurfacePolicy { @@ -139,7 +140,7 @@ pub struct AnimationStatus { pub struct InterruptContext<'a> { pub input: &'a FighterInput, pub facing_direction: &'a mut FighterFacingDirection, - pub ledge_grab_lockout: &'a mut FighterLedgeGrabLockout, + pub ledge_grab_lockout: &'a mut FighterLedgeInformation, pub game_settings: &'a GameSettings, pub fighter_attributes: &'a FighterAttributes, pub surface: &'a FighterSurfaceState, @@ -171,6 +172,7 @@ pub struct EnterContext<'a> { pub presentation_snap: &'a mut FighterPresentationSnap, pub surface: &'a mut FighterSurfaceState, pub vulnerability: &'a mut FighterInvulnerabilityState, + pub ledge_info: &'a mut FighterLedgeInformation, pub fighter_attributes: &'a FighterAttributes, pub attack_script_assets: &'a FighterAttackScriptAssets, pub game_settings: &'a GameSettings, @@ -217,6 +219,7 @@ pub struct UpdateContext<'a> { pub surface: &'a mut FighterSurfaceState, pub knockback_velocity: &'a mut FighterKnockbackVelocity, pub vulnerability: &'a mut FighterInvulnerabilityState, + pub ledge_info: &'a mut FighterLedgeInformation, pub game_settings: &'a GameSettings, /// Root motion in the fighter's gameplay plane: X is world-right and Y is /// world-up. Animation +Z is forward and is mirrored by facing direction. @@ -256,8 +259,8 @@ pub struct CollisionContext<'a> { pub stage_collision: &'a StageCollision, pub leave_ground_ecb_lock_duration: u32, pub presentation_snap: &'a mut FighterPresentationSnap, + pub ledge_info: &'a mut FighterLedgeInformation, pub fighter_ledge_collision_box: FGRect, - pub ledge_grab_lockout_active: bool, pub downward_stick_threshold: FGi32, } @@ -282,7 +285,7 @@ impl CollisionContext<'_> { pub fn air_collide_with_ledges( &self, ) -> Option { - if self.ledge_grab_lockout_active { + if self.ledge_info.is_regrab_lockout_active() { return None; } crate::fighter::collision::air_collide_with_ledges( @@ -305,6 +308,7 @@ fighter_states! { Land => land::LandingState, LedgeWait => ledge_wait::LedgeWaitState, LedgeAction => ledge_action::LedgeActionState, + LedgeGrab => ledge_grab::LedgeGrabState, AirDodge => air_dodge::AirDodgeState, Turn => turn::TurnState, GroundAttack => ground_attack::GroundAttackState, @@ -330,7 +334,7 @@ pub struct FighterFrameQuery { pub facing_direction: &'static mut FighterFacingDirection, pub animation_frame: &'static mut FighterAnimationFrame, pub vulnerability: &'static mut FighterInvulnerabilityState, - pub ledge_grab_lockout: &'static mut FighterLedgeGrabLockout, + pub ledge_information: &'static mut FighterLedgeInformation, pub fighter: &'static Fighter, pub bone_matrices: &'static FighterBoneMatrices, } @@ -366,7 +370,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { InterruptContext { input: &self.input, facing_direction: &mut self.facing_direction, - ledge_grab_lockout: &mut self.ledge_grab_lockout, + ledge_grab_lockout: &mut self.ledge_information, game_settings, fighter_attributes: attributes, animation, @@ -398,6 +402,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { surface: &mut self.surface, vulnerability: &mut self.vulnerability, presentation_snap: &mut self.presentation_snap, + ledge_info: &mut self.ledge_information, fighter_attributes: attributes, attack_script_assets, game_settings, @@ -445,6 +450,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { animation_root_position, animation_frame: self.animation_frame.frame, vulnerability: &mut self.vulnerability, + ledge_info: &mut self.ledge_information, fighter_attributes: attributes, attack_scripts, attack_script_assets, @@ -471,6 +477,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { velocity: &mut self.velocity, surface: &mut self.surface, ecb: &mut self.ecb, + ledge_info: &mut self.ledge_information, prev_ecb: &self.prev_ecb, stage_collision, leave_ground_ecb_lock_duration: game_settings @@ -478,7 +485,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, - ledge_grab_lockout_active: self.ledge_grab_lockout.is_active(), } } } @@ -829,15 +835,17 @@ mod tests { let mut velocity = FighterVelocity::default(); let mut surface = FighterSurfaceState::default(); let mut ecb = FighterECB::default(); + let mut ledge_info = FighterLedgeInformation::default(); 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, velocity: &mut velocity, surface: &mut surface, ecb: &mut ecb, + ledge_info: &mut ledge_info, prev_ecb: &previous_ecb, stage_collision: &stage_collision, leave_ground_ecb_lock_duration: 0, @@ -846,12 +854,10 @@ mod tests { origin: FGVec2::lit("-1", "1"), end: FGVec2::lit("1", "-1"), }, - ledge_grab_lockout_active: true, downward_stick_threshold: FGi32::ONE, }; assert!(context.air_collide_with_ledges().is_none()); - context.ledge_grab_lockout_active = false; assert_eq!( context.air_collide_with_ledges().map(|result| result.ledge), Some(ledge_id) diff --git a/src/fighter/surface.rs b/src/fighter/surface.rs index 33b0ad4..7740f6e 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::{fighter::FighterLedgeInformation, stage::line::StageLineID}; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum EcbLockPolicy { @@ -53,8 +53,9 @@ impl FighterSurfaceState { } } - pub(crate) fn land(&mut self, line_id: StageLineID) { + pub(crate) fn land(&mut self, line_id: StageLineID, ledge_info: &mut FighterLedgeInformation) { *self = Self::Grounded { line_id }; + ledge_info.grab_count = 0; } pub fn clear_polygon_ignore(&mut self, polygon: usize) { @@ -151,7 +152,11 @@ mod tests { polygon: 2, segment: 3, }; - surface.land(line_id); + let mut ledge_info = FighterLedgeInformation { + regrab_lockout_remaining_frames: 0, + grab_count: 0, + }; + surface.land(line_id, &mut ledge_info); assert!(surface.is_grounded()); assert_eq!(surface.line_id(), Some(line_id)); @@ -220,8 +225,11 @@ mod tests { 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 }); + let mut ledge_info = FighterLedgeInformation { + regrab_lockout_remaining_frames: 0, + grab_count: 0, + }; + surface.land(StageLineID { polygon: 0, segment: 0 }, &mut ledge_info); assert!(!surface.is_polygon_ignored(3)); } } diff --git a/src/game_settings.rs b/src/game_settings.rs index 62b4138..956ed0c 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -45,14 +45,18 @@ pub struct FighterSettingsCommon { pub leave_ground_ecb_frame_lock_duration: u32, pub platform_pass_platform_ignore_duration_frames: u32, pub platform_pass_initial_downwards_velocity: FGi32, - /// Frames during which a fighter cannot grab any ledge after dropping from one. - pub ledge_drop_regrab_lockout_duration_frames: u32, - + pub intangible_color: Color, pub intangible_pulse_duration: u32, - + pub angel_platform_invulnerability_duration: u32, + + /// Frames during which a fighter cannot grab any ledge after dropping from one. + pub ledge_drop_regrab_lockout_duration_frames: u32, + /// How many frames (+ the ledge animation duration) the fighter will be invulnerable after grabbing the ledge pub ledge_grab_invulnerability_duration: u32, + /// How many times the fighter can regrab the ledge and still get invulnerability + pub max_ledge_regrab_count: u32, /// When hit while the receiver is in the air, use this angle pub sakurai_angle_aerial_angle: FGi32,