Compare commits

...
Author SHA1 Message Date
EIREXE 7819b95bc6 Add ledge getup, ledge intangibility limit & fix walk-off velocity
CI / build-and-test (pull_request) Failing after 1m50s
CI / production-build (pull_request) Canceled after 9m14s
2026-10-13 12:14:12 +02:00
11 changed files with 195 additions and 54 deletions
+2
View File
@@ -89,6 +89,8 @@ pub enum AnimKind {
CrouchExit, CrouchExit,
LedgeWait, LedgeWait,
LedgeJump, LedgeJump,
LedgeGetUp,
LedgeGrab,
PlatformPass, PlatformPass,
+19 -18
View File
@@ -87,46 +87,47 @@ pub struct FighterHitstop {
} }
#[derive(Component, Default, Copy, Clone, Hash, Debug, PartialEq, Eq)] #[derive(Component, Default, Copy, Clone, Hash, Debug, PartialEq, Eq)]
pub struct FighterLedgeGrabLockout { pub struct FighterLedgeInformation {
pub remaining_frames: u32, pub regrab_lockout_remaining_frames: u32,
pub grab_count: u32,
} }
impl FighterLedgeGrabLockout { impl FighterLedgeInformation {
pub fn start(&mut self, duration_frames: u32) { 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 { pub fn is_regrab_lockout_active(&self) -> bool {
self.remaining_frames > 0 self.regrab_lockout_remaining_frames > 0
} }
fn tick(&mut self) { 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)] #[cfg(test)]
mod ledge_grab_lockout_tests { mod ledge_grab_lockout_tests {
use super::FighterLedgeGrabLockout; use super::FighterLedgeInformation;
#[test] #[test]
fn lockout_expires_after_the_requested_number_of_ticks() { fn lockout_expires_after_the_requested_number_of_ticks() {
let mut lockout = FighterLedgeGrabLockout::default(); let mut lockout = FighterLedgeInformation::default();
lockout.start(2); lockout.start(2);
assert!(lockout.is_active()); assert!(lockout.is_regrab_lockout_active());
lockout.tick(); lockout.tick();
assert!(lockout.is_active()); assert!(lockout.is_regrab_lockout_active());
lockout.tick(); lockout.tick();
assert!(!lockout.is_active()); assert!(!lockout.is_regrab_lockout_active());
} }
#[test] #[test]
fn zero_duration_does_not_enable_lockout() { fn zero_duration_does_not_enable_lockout() {
let mut lockout = FighterLedgeGrabLockout::default(); let mut lockout = FighterLedgeInformation::default();
lockout.start(0); lockout.start(0);
assert!(!lockout.is_active()); assert!(!lockout.is_regrab_lockout_active());
} }
} }
@@ -192,7 +193,7 @@ pub struct FighterPendingRespawn(bool);
FighterPendingHitReaction, FighterPendingHitReaction,
FighterPendingRespawn, FighterPendingRespawn,
FighterHitstop, FighterHitstop,
FighterLedgeGrabLockout, FighterLedgeInformation,
FighterKnockbackVelocity, FighterKnockbackVelocity,
FighterTranslation, FighterTranslation,
FighterInvulnerabilityState, FighterInvulnerabilityState,
@@ -298,8 +299,8 @@ impl Plugin for FighterPlugin {
.checksum_component_with_hash::<FighterPendingRespawn>() .checksum_component_with_hash::<FighterPendingRespawn>()
.rollback_component_with_copy::<FighterHitstop>() .rollback_component_with_copy::<FighterHitstop>()
.checksum_component_with_hash::<FighterHitstop>() .checksum_component_with_hash::<FighterHitstop>()
.rollback_component_with_copy::<FighterLedgeGrabLockout>() .rollback_component_with_copy::<FighterLedgeInformation>()
.checksum_component_with_hash::<FighterLedgeGrabLockout>() .checksum_component_with_hash::<FighterLedgeInformation>()
.rollback_component_with_copy::<FighterInvulnerabilityState>() .rollback_component_with_copy::<FighterInvulnerabilityState>()
.checksum_component_with_hash::<FighterInvulnerabilityState>() .checksum_component_with_hash::<FighterInvulnerabilityState>()
.rollback_component_with_copy::<FighterKnockbackVelocity>() .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 { for mut lockout in query {
lockout.tick(); lockout.tick();
} }
+3 -6
View File
@@ -1,6 +1,6 @@
use crate::fighter::state::jump::{self}; use crate::fighter::state::jump::{self};
use crate::fighter::state::land::{LandingState, LandingType}; 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::{ use crate::fighter::state::{
CollisionContext, FighterState, InterruptContext, air_attack, air_dodge, CollisionContext, FighterState, InterruptContext, air_attack, air_dodge,
}; };
@@ -129,10 +129,7 @@ pub fn collision_interrupt(
) -> Option<FighterState> { ) -> Option<FighterState> {
if ctx.velocity.y <= FGi32::ZERO { if ctx.velocity.y <= FGi32::ZERO {
if let Some(ledge_result) = ctx.air_collide_with_ledges() { if let Some(ledge_result) = ctx.air_collide_with_ledges() {
return Some(FighterState::LedgeWait(LedgeWaitState { return Some(FighterState::LedgeGrab(LedgeGrabState::new(ledge_result.ledge)));
airborne_state_common: AirborneStateCommon::default(),
ledge_id: ledge_result.ledge,
}));
} }
} }
@@ -144,7 +141,7 @@ pub fn collision_interrupt(
ctx.ecb.right -= diff; ctx.ecb.right -= diff;
ctx.translation.0 = res.hit_position; ctx.translation.0 = res.hit_position;
ctx.presentation_snap.0 = true; 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))) 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) { fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.play_animation(AnimKind::Fall, true); 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) { 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 { state_context.surface.land(StageLineID {
polygon: current_line_id.polygon, polygon: current_line_id.polygon,
segment: segment_idx, segment: segment_idx,
}); }, state_context.ledge_info);
GroundedMotionResult::InGround GroundedMotionResult::InGround
} }
GroundedPositionResult::InAir => { GroundedPositionResult::InAir => {
@@ -246,7 +246,7 @@ pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Op
mod tests { mod tests {
use super::*; use super::*;
use crate::fighter::ecb::FighterPreviousECB; 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::math::rect::FGRect;
use crate::stage::line::StagePolyType; use crate::stage::line::StagePolyType;
use crate::input::FighterInput; use crate::input::FighterInput;
@@ -371,6 +371,7 @@ use crate::stage::line::StagePolyType;
let mut translation = FighterTranslation(FGVec2::lit("11", "0")); let mut translation = FighterTranslation(FGVec2::lit("11", "0"));
let previous_translation = FighterPreviousTranslation(translation.0); let previous_translation = FighterPreviousTranslation(translation.0);
let mut velocity = FighterVelocity::default(); let mut velocity = FighterVelocity::default();
let mut ledge_info = FighterLedgeInformation::default();
let mut ecb = FighterECB { let mut ecb = FighterECB {
bottom: FGVec2::lit("0.0", "-0.5"), bottom: FGVec2::lit("0.0", "-0.5"),
top: 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, velocity: &mut velocity,
surface: &mut surface, surface: &mut surface,
ecb: &mut ecb, ecb: &mut ecb,
ledge_info: &mut ledge_info,
prev_ecb: &previous_ecb, prev_ecb: &previous_ecb,
stage_collision: &stage_collision, stage_collision: &stage_collision,
leave_ground_ecb_lock_duration: 10, leave_ground_ecb_lock_duration: 10,
@@ -395,7 +397,6 @@ use crate::stage::line::StagePolyType;
origin: FGVec2::ZERO, origin: FGVec2::ZERO,
end: FGVec2::ZERO, end: FGVec2::ZERO,
}, },
ledge_grab_lockout_active: false,
}; };
assert!(matches!( assert!(matches!(
@@ -440,6 +441,7 @@ use crate::stage::line::StagePolyType;
}; };
let previous_ecb = FighterPreviousECB::from(ecb); let previous_ecb = FighterPreviousECB::from(ecb);
let mut presentation_snap = crate::fighter::FighterPresentationSnap::default(); let mut presentation_snap = crate::fighter::FighterPresentationSnap::default();
let mut ledge_info = FighterLedgeInformation::default();
let mut context = CollisionContext { let mut context = CollisionContext {
translation: &mut translation, translation: &mut translation,
prev_translation: &previous_translation, prev_translation: &previous_translation,
@@ -447,6 +449,7 @@ use crate::stage::line::StagePolyType;
velocity: &mut velocity, velocity: &mut velocity,
surface: &mut surface, surface: &mut surface,
ecb: &mut ecb, ecb: &mut ecb,
ledge_info: &mut ledge_info,
prev_ecb: &previous_ecb, prev_ecb: &previous_ecb,
stage_collision: &stage_collision, stage_collision: &stage_collision,
leave_ground_ecb_lock_duration: 10, leave_ground_ecb_lock_duration: 10,
@@ -456,7 +459,6 @@ use crate::stage::line::StagePolyType;
origin: FGVec2::ZERO, origin: FGVec2::ZERO,
end: FGVec2::ZERO, end: FGVec2::ZERO,
}, },
ledge_grab_lockout_active: false,
}; };
assert!(matches!( assert!(matches!(
+21 -2
View File
@@ -2,6 +2,7 @@ use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext,
use crate::fighter::animation::AnimKind; use crate::fighter::animation::AnimKind;
use crate::fighter::state::air::AirborneStateCommon; use crate::fighter::state::air::AirborneStateCommon;
use crate::fighter::state::jump::{JumpState, JumpStateInitiator, JumpType}; use crate::fighter::state::jump::{JumpState, JumpStateInitiator, JumpType};
use crate::fighter::state::wait::WaitState;
use crate::fighter::state::{FighterState, ground}; use crate::fighter::state::{FighterState, ground};
use crate::fighter::FighterFacingDirection; use crate::fighter::FighterFacingDirection;
use crate::input::FighterCommands; use crate::input::FighterCommands;
@@ -11,7 +12,8 @@ use crate::stage::line::{StageLedgeSide, StageVertexID};
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
pub enum LedgeAction { pub enum LedgeAction {
Jump(JumpStateInitiator) Jump(JumpStateInitiator),
GetUp
} }
#[derive(Debug, Clone, Hash)] #[derive(Debug, Clone, Hash)]
@@ -29,6 +31,13 @@ impl LedgeActionState {
is_in_grounded_phase: false, 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 { impl FighterStateImpl for LedgeActionState {
@@ -50,6 +59,13 @@ impl FighterStateImpl for LedgeActionState {
None 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::Jump);
state_context.input.clear_command(FighterCommands::TapJump); state_context.input.clear_command(FighterCommands::TapJump);
}, },
LedgeAction::GetUp => {
state_context.play_animation(AnimKind::LedgeGetUp, false);
},
} }
state_context.velocity.0 = FGVec2::ZERO; state_context.velocity.0 = FGVec2::ZERO;
@@ -105,7 +124,7 @@ impl FighterStateImpl for LedgeActionState {
self.is_in_grounded_phase = true; self.is_in_grounded_phase = true;
state_context.translation.0 = ledge.position; 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) { } else if state_context.input.has_command(FighterCommands::TapJump) {
return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Tap))); 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, if is_ledge_drop_input(state_context.input.get_last_frame().movement.x,
state_context.game_settings.input_common.run_stick_threshold, state_context.game_settings.input_common.run_stick_threshold,
state_context.facing_direction) { state_context.facing_direction) {
@@ -58,14 +65,20 @@ impl FighterStateImpl for LedgeWaitState {
fn on_enter(&mut self, state_context: &mut EnterContext) { fn on_enter(&mut self, state_context: &mut EnterContext) {
state_context.velocity.0 = FGVec2::ZERO; state_context.velocity.0 = FGVec2::ZERO;
state_context.knockback_velocity.0 = FGVec2::ZERO; state_context.knockback_velocity.0 = FGVec2::ZERO;
*state_context.vulnerability = FighterInvulnerabilityState::Intangible {
frames_remaining: Some( if state_context.ledge_info.grab_count < state_context.game_settings.fighter_common.max_ledge_regrab_count {
state_context *state_context.vulnerability = FighterInvulnerabilityState::Intangible {
frames_remaining: Some(
state_context
.game_settings .game_settings
.fighter_common .fighter_common
.ledge_grab_invulnerability_duration, .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 { let Some(ledge) = state_context.stage_collision.find_ledge_by_vertex(self.ledge_id) else {
return; return;
}; };
@@ -135,3 +148,18 @@ fn is_ledge_drop_input(
false 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
}
+16 -10
View File
@@ -20,7 +20,7 @@ use crate::fighter::visual::FighterAnimations;
use crate::fighter::{ use crate::fighter::{
Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, Fighter, FighterDamage, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop,
FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole,
FighterInvulnerabilityState, FighterLedgeGrabLockout, FighterPendingRespawn, FighterInvulnerabilityState, FighterLedgeInformation, FighterPendingRespawn,
FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterPresentationSnap, FighterPreviousTranslation, FighterSurfaceState, FighterTranslation,
FighterVelocity, FighterVelocity,
}; };
@@ -56,6 +56,7 @@ pub mod wait;
pub mod walk; pub mod walk;
pub mod ledge_wait; pub mod ledge_wait;
pub mod ledge_action; pub mod ledge_action;
pub mod ledge_grab;
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum SurfacePolicy { pub enum SurfacePolicy {
@@ -139,7 +140,7 @@ pub struct AnimationStatus {
pub struct InterruptContext<'a> { pub struct InterruptContext<'a> {
pub input: &'a FighterInput, pub input: &'a FighterInput,
pub facing_direction: &'a mut FighterFacingDirection, 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 game_settings: &'a GameSettings,
pub fighter_attributes: &'a FighterAttributes, pub fighter_attributes: &'a FighterAttributes,
pub surface: &'a FighterSurfaceState, pub surface: &'a FighterSurfaceState,
@@ -171,6 +172,7 @@ pub struct EnterContext<'a> {
pub presentation_snap: &'a mut FighterPresentationSnap, pub presentation_snap: &'a mut FighterPresentationSnap,
pub surface: &'a mut FighterSurfaceState, pub surface: &'a mut FighterSurfaceState,
pub vulnerability: &'a mut FighterInvulnerabilityState, pub vulnerability: &'a mut FighterInvulnerabilityState,
pub ledge_info: &'a mut FighterLedgeInformation,
pub fighter_attributes: &'a FighterAttributes, pub fighter_attributes: &'a FighterAttributes,
pub attack_script_assets: &'a FighterAttackScriptAssets, pub attack_script_assets: &'a FighterAttackScriptAssets,
pub game_settings: &'a GameSettings, pub game_settings: &'a GameSettings,
@@ -217,6 +219,7 @@ pub struct UpdateContext<'a> {
pub surface: &'a mut FighterSurfaceState, pub surface: &'a mut FighterSurfaceState,
pub knockback_velocity: &'a mut FighterKnockbackVelocity, pub knockback_velocity: &'a mut FighterKnockbackVelocity,
pub vulnerability: &'a mut FighterInvulnerabilityState, pub vulnerability: &'a mut FighterInvulnerabilityState,
pub ledge_info: &'a mut FighterLedgeInformation,
pub game_settings: &'a GameSettings, pub game_settings: &'a GameSettings,
/// Root motion in the fighter's gameplay plane: X is world-right and Y is /// 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. /// 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 stage_collision: &'a StageCollision,
pub leave_ground_ecb_lock_duration: u32, pub leave_ground_ecb_lock_duration: u32,
pub presentation_snap: &'a mut FighterPresentationSnap, pub presentation_snap: &'a mut FighterPresentationSnap,
pub ledge_info: &'a mut FighterLedgeInformation,
pub fighter_ledge_collision_box: FGRect, pub fighter_ledge_collision_box: FGRect,
pub ledge_grab_lockout_active: bool,
pub downward_stick_threshold: FGi32, pub downward_stick_threshold: FGi32,
} }
@@ -282,7 +285,7 @@ impl CollisionContext<'_> {
pub fn air_collide_with_ledges( pub fn air_collide_with_ledges(
&self, &self,
) -> Option<crate::fighter::collision::AirCollisionWithLedgeResult> { ) -> Option<crate::fighter::collision::AirCollisionWithLedgeResult> {
if self.ledge_grab_lockout_active { if self.ledge_info.is_regrab_lockout_active() {
return None; return None;
} }
crate::fighter::collision::air_collide_with_ledges( crate::fighter::collision::air_collide_with_ledges(
@@ -305,6 +308,7 @@ fighter_states! {
Land => land::LandingState, Land => land::LandingState,
LedgeWait => ledge_wait::LedgeWaitState, LedgeWait => ledge_wait::LedgeWaitState,
LedgeAction => ledge_action::LedgeActionState, LedgeAction => ledge_action::LedgeActionState,
LedgeGrab => ledge_grab::LedgeGrabState,
AirDodge => air_dodge::AirDodgeState, AirDodge => air_dodge::AirDodgeState,
Turn => turn::TurnState, Turn => turn::TurnState,
GroundAttack => ground_attack::GroundAttackState, GroundAttack => ground_attack::GroundAttackState,
@@ -330,7 +334,7 @@ pub struct FighterFrameQuery {
pub facing_direction: &'static mut FighterFacingDirection, pub facing_direction: &'static mut FighterFacingDirection,
pub animation_frame: &'static mut FighterAnimationFrame, pub animation_frame: &'static mut FighterAnimationFrame,
pub vulnerability: &'static mut FighterInvulnerabilityState, pub vulnerability: &'static mut FighterInvulnerabilityState,
pub ledge_grab_lockout: &'static mut FighterLedgeGrabLockout, pub ledge_information: &'static mut FighterLedgeInformation,
pub fighter: &'static Fighter, pub fighter: &'static Fighter,
pub bone_matrices: &'static FighterBoneMatrices, pub bone_matrices: &'static FighterBoneMatrices,
} }
@@ -366,7 +370,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
InterruptContext { InterruptContext {
input: &self.input, input: &self.input,
facing_direction: &mut self.facing_direction, facing_direction: &mut self.facing_direction,
ledge_grab_lockout: &mut self.ledge_grab_lockout, ledge_grab_lockout: &mut self.ledge_information,
game_settings, game_settings,
fighter_attributes: attributes, fighter_attributes: attributes,
animation, animation,
@@ -398,6 +402,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
surface: &mut self.surface, surface: &mut self.surface,
vulnerability: &mut self.vulnerability, vulnerability: &mut self.vulnerability,
presentation_snap: &mut self.presentation_snap, presentation_snap: &mut self.presentation_snap,
ledge_info: &mut self.ledge_information,
fighter_attributes: attributes, fighter_attributes: attributes,
attack_script_assets, attack_script_assets,
game_settings, game_settings,
@@ -445,6 +450,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
animation_root_position, animation_root_position,
animation_frame: self.animation_frame.frame, animation_frame: self.animation_frame.frame,
vulnerability: &mut self.vulnerability, vulnerability: &mut self.vulnerability,
ledge_info: &mut self.ledge_information,
fighter_attributes: attributes, fighter_attributes: attributes,
attack_scripts, attack_scripts,
attack_script_assets, attack_script_assets,
@@ -471,6 +477,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
velocity: &mut self.velocity, velocity: &mut self.velocity,
surface: &mut self.surface, surface: &mut self.surface,
ecb: &mut self.ecb, ecb: &mut self.ecb,
ledge_info: &mut self.ledge_information,
prev_ecb: &self.prev_ecb, prev_ecb: &self.prev_ecb,
stage_collision, stage_collision,
leave_ground_ecb_lock_duration: game_settings leave_ground_ecb_lock_duration: game_settings
@@ -478,7 +485,6 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
.leave_ground_ecb_frame_lock_duration, .leave_ground_ecb_frame_lock_duration,
downward_stick_threshold: game_settings.input_common.run_stick_threshold, downward_stick_threshold: game_settings.input_common.run_stick_threshold,
fighter_ledge_collision_box, 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 velocity = FighterVelocity::default();
let mut surface = FighterSurfaceState::default(); let mut surface = FighterSurfaceState::default();
let mut ecb = FighterECB::default(); let mut ecb = FighterECB::default();
let mut ledge_info = FighterLedgeInformation::default();
let previous_ecb = FighterPreviousECB::from(ecb); let previous_ecb = FighterPreviousECB::from(ecb);
let mut presentation_snap = FighterPresentationSnap::default(); let mut presentation_snap = FighterPresentationSnap::default();
let mut context = CollisionContext { let context = CollisionContext {
translation: &mut translation, translation: &mut translation,
prev_translation: &previous_translation, prev_translation: &previous_translation,
input: &input, input: &input,
velocity: &mut velocity, velocity: &mut velocity,
surface: &mut surface, surface: &mut surface,
ecb: &mut ecb, ecb: &mut ecb,
ledge_info: &mut ledge_info,
prev_ecb: &previous_ecb, prev_ecb: &previous_ecb,
stage_collision: &stage_collision, stage_collision: &stage_collision,
leave_ground_ecb_lock_duration: 0, leave_ground_ecb_lock_duration: 0,
@@ -846,12 +854,10 @@ mod tests {
origin: FGVec2::lit("-1", "1"), origin: FGVec2::lit("-1", "1"),
end: FGVec2::lit("1", "-1"), end: FGVec2::lit("1", "-1"),
}, },
ledge_grab_lockout_active: true,
downward_stick_threshold: FGi32::ONE, downward_stick_threshold: FGi32::ONE,
}; };
assert!(context.air_collide_with_ledges().is_none()); assert!(context.air_collide_with_ledges().is_none());
context.ledge_grab_lockout_active = false;
assert_eq!( assert_eq!(
context.air_collide_with_ledges().map(|result| result.ledge), context.air_collide_with_ledges().map(|result| result.ledge),
Some(ledge_id) Some(ledge_id)
+13 -5
View File
@@ -1,6 +1,6 @@
use bevy::prelude::*; use bevy::prelude::*;
use crate::stage::line::StageLineID; use crate::{fighter::FighterLedgeInformation, stage::line::StageLineID};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
pub enum EcbLockPolicy { 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 }; *self = Self::Grounded { line_id };
ledge_info.grab_count = 0;
} }
pub fn clear_polygon_ignore(&mut self, polygon: usize) { pub fn clear_polygon_ignore(&mut self, polygon: usize) {
@@ -151,7 +152,11 @@ mod tests {
polygon: 2, polygon: 2,
segment: 3, 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!(surface.is_grounded());
assert_eq!(surface.line_id(), Some(line_id)); assert_eq!(surface.line_id(), Some(line_id));
@@ -220,8 +225,11 @@ mod tests {
surface.ignore_polygon(3, 5); surface.ignore_polygon(3, 5);
surface.apply_airborne_policy(EcbLockPolicy::Restart, 2); surface.apply_airborne_policy(EcbLockPolicy::Restart, 2);
assert!(surface.is_polygon_ignored(3)); assert!(surface.is_polygon_ignored(3));
let mut ledge_info = FighterLedgeInformation {
surface.land(StageLineID { polygon: 0, segment: 0 }); regrab_lockout_remaining_frames: 0,
grab_count: 0,
};
surface.land(StageLineID { polygon: 0, segment: 0 }, &mut ledge_info);
assert!(!surface.is_polygon_ignored(3)); assert!(!surface.is_polygon_ignored(3));
} }
} }
+6 -2
View File
@@ -45,14 +45,18 @@ pub struct FighterSettingsCommon {
pub leave_ground_ecb_frame_lock_duration: u32, pub leave_ground_ecb_frame_lock_duration: u32,
pub platform_pass_platform_ignore_duration_frames: u32, pub platform_pass_platform_ignore_duration_frames: u32,
pub platform_pass_initial_downwards_velocity: FGi32, 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_color: Color,
pub intangible_pulse_duration: u32, pub intangible_pulse_duration: u32,
pub angel_platform_invulnerability_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, 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 /// When hit while the receiver is in the air, use this angle
pub sakurai_angle_aerial_angle: FGi32, pub sakurai_angle_aerial_angle: FGi32,