use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext}; use crate::fighter::animation::AnimKind; use crate::fighter::state::air::AirborneStateCommon; use crate::fighter::state::fall::FallState; use crate::fighter::state::FighterState; use crate::fighter::state::jump::JumpStateInitiator; use crate::fighter::state::ledge_action::{LedgeActionState}; use crate::fighter::{FighterFacingDirection, FighterInvulnerabilityState}; use crate::input::FighterCommands; use crate::math::vec::FGVec2; use crate::stage::line::{StageLedgeSide, StageVertexID}; #[derive(Debug, Clone, Hash)] pub struct LedgeWaitState { pub airborne_state_common: AirborneStateCommon, pub ledge_id: StageVertexID, } impl FighterStateImpl for LedgeWaitState { const NAME: &'static str = "LedgeWait"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.input.has_command(FighterCommands::Jump) { return Some(FighterState::LedgeAction(LedgeActionState::create_jump(self.ledge_id, JumpStateInitiator::Button))); } 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) { state_context.ledge_grab_lockout.start( state_context .game_settings .fighter_common .ledge_drop_regrab_lockout_duration_frames, ); return Some(FighterState::Fall(FallState::new( AirborneStateCommon { is_fast_falling: false }, ))); } if is_ledge_drop_fastfall_input( state_context.input.get_last_frame().movement.y, state_context.game_settings.input_common.run_stick_threshold, ) { state_context.ledge_grab_lockout.start( state_context .game_settings .fighter_common .ledge_drop_regrab_lockout_duration_frames, ); return Some(FighterState::Fall(FallState::new( AirborneStateCommon { is_fast_falling: true }, ))); } None } fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.velocity.0 = FGVec2::ZERO; state_context.knockback_velocity.0 = FGVec2::ZERO; 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; }; state_context.translation.0 = ledge.position; *state_context.facing_direction = match ledge.hang_side { StageLedgeSide::Left => FighterFacingDirection::Right, StageLedgeSide::Right => FighterFacingDirection::Left, }; state_context.play_animation(AnimKind::LedgeWait, true); if let Some(root_position) = state_context.initial_animation_root_position() { state_context.translation.0 = root_position; } state_context.presentation_snap.0 = true; } fn update(&mut self, state_context: &mut UpdateContext) { if let Some(root_position) = state_context.animation_root_position { state_context.translation.0 = root_position; } } fn check_collision_interrupt( &mut self, _state_context: &mut CollisionContext, ) -> Option { None } fn is_visually_grounded<'a>(&'a self) -> bool { false } } #[cfg(test)] mod tests { use super::*; use crate::math::int::FGi32; #[test] fn ledge_drop_uses_the_run_stick_threshold() { let threshold = FGi32::lit("0.625"); assert!(is_ledge_drop_fastfall_input(FGi32::lit("-0.625"), threshold)); assert!(is_ledge_drop_fastfall_input(FGi32::lit("-1.0"), threshold)); assert!(!is_ledge_drop_fastfall_input(FGi32::lit("-0.624"), threshold)); } } fn is_ledge_drop_fastfall_input( vertical_input: crate::math::int::FGi32, threshold: crate::math::int::FGi32, ) -> bool { vertical_input <= -threshold } fn is_ledge_drop_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 } 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 }