Add ledge getup, ledge intangibility limit & fix walk-off velocity #93

Merged
EIREXE merged 4 commits from ledge_getup into master 2026-10-13 15:52:10 +00:00
12 changed files with 208 additions and 55 deletions
+4 -1
View File
@@ -2,7 +2,7 @@ use bevy::prelude::*;
use crate::{
camera::MatchCamera, fighter::{
FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterInvulnerabilityState, FighterPendingRespawn, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, animation::FighterAnimationFrame, attack::{
FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterInvulnerabilityState, FighterLedgeInformation, FighterPendingRespawn, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, animation::FighterAnimationFrame, attack::{
FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits,
FighterSolvedHurtboxes, FighterStaleMoveQueue,
}, baked_animation::FighterBoneMatrices, state::FighterState,
@@ -48,6 +48,7 @@ struct FighterCheckpoint {
surface: FighterSurfaceState,
pending_respawn: FighterPendingRespawn,
invulnerability_state: FighterInvulnerabilityState,
ledge_information: FighterLedgeInformation,
}
pub(super) fn save_or_restore(world: &mut World) {
@@ -151,6 +152,7 @@ pub(super) fn save_or_restore(world: &mut World) {
entity.insert(saved.surface.clone());
entity.insert(saved.pending_respawn.clone());
entity.insert(saved.invulnerability_state.clone());
entity.insert(saved.ledge_information.clone());
}
world.insert_resource(checkpoint.pending_hits.clone());
world.insert_resource(checkpoint.stage_collision);
@@ -188,6 +190,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option<FighterCheckpoint> {
surface: fighter.get::<FighterSurfaceState>()?.clone(),
pending_respawn: fighter.get::<FighterPendingRespawn>()?.clone(),
invulnerability_state: fighter.get::<FighterInvulnerabilityState>()?.clone(),
ledge_information: fighter.get::<FighterLedgeInformation>()?.clone(),
})
}
+2
View File
@@ -89,6 +89,8 @@ pub enum AnimKind {
CrouchExit,
LedgeWait,
LedgeJump,
LedgeGetUp,
LedgeGrab,
PlatformPass,
+19 -18
View File
@@ -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::<FighterPendingRespawn>()
.rollback_component_with_copy::<FighterHitstop>()
.checksum_component_with_hash::<FighterHitstop>()
.rollback_component_with_copy::<FighterLedgeGrabLockout>()
.checksum_component_with_hash::<FighterLedgeGrabLockout>()
.rollback_component_with_copy::<FighterLedgeInformation>()
.checksum_component_with_hash::<FighterLedgeInformation>()
.rollback_component_with_copy::<FighterInvulnerabilityState>()
.checksum_component_with_hash::<FighterInvulnerabilityState>()
.rollback_component_with_copy::<FighterKnockbackVelocity>()
@@ -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();
}
+3 -6
View File
@@ -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<FighterState> {
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)))
}
+1
View File
@@ -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) {
+6 -4
View File
@@ -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!(
+21 -2
View File
@@ -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);
}
}
+73
View File
@@ -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<FighterState> {
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;
}
}
}
+33 -5
View File
@@ -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_grab_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
}
+25 -10
View File
@@ -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<crate::fighter::collision::AirCollisionWithLedgeResult> {
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(),
}
}
}
@@ -494,6 +500,7 @@ pub struct StatefulFighterQuery {
pub player: &'static Player,
pub pending_hit_reaction: &'static mut FighterPendingHitReaction,
pub hitstop: &'static mut FighterHitstop,
pub ledge_info: &'static mut FighterLedgeInformation,
pub hitstop_shake: &'static mut FighterHitstopShake,
pub pending_respawn: &'static mut FighterPendingRespawn,
pub damage: &'static mut FighterDamage,
@@ -703,6 +710,7 @@ pub fn state_collision_interrupt_system(
mut pending_respawn,
player,
mut damage,
mut ledge_info,
..
} = fighter;
@@ -751,6 +759,9 @@ pub fn state_collision_interrupt_system(
// reset percentage
*damage = FighterDamage::default();
// reset ledge
*ledge_info = FighterLedgeInformation::default();
transition_state(
&mut state,
@@ -829,15 +840,20 @@ mod tests {
let mut velocity = FighterVelocity::default();
let mut surface = FighterSurfaceState::default();
let mut ecb = FighterECB::default();
let mut ledge_info = FighterLedgeInformation {
regrab_lockout_remaining_frames: 5,
..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 +862,11 @@ 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;
context.ledge_info.regrab_lockout_remaining_frames = 0;
assert_eq!(
context.air_collide_with_ledges().map(|result| result.ledge),
Some(ledge_id)
+13 -5
View File
@@ -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));
}
}
+8 -4
View File
@@ -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 grab the ledge without landing and still get invulnerability
pub max_ledge_grab_count: u32,
/// When hit while the receiver is in the air, use this angle
pub sakurai_angle_aerial_angle: FGi32,