use bevy::prelude::*; use crate::{ fighter::{ animation::AnimKind, state::{ CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_dodge::AirDodgeState, ground, }, }, input::{DigitalAction, FighterCommands}, math::{int::FGi32, vec::FGVec2}, }; #[derive(Debug, Hash, Clone, Copy)] pub enum JumpType { ShortHop, LongJump, DoubleJump, } #[derive(Debug, Clone, Hash)] pub struct JumpState { pub jump_type: JumpType, pub airborne_state_common: AirborneStateCommon, } #[derive(Debug, Clone, Hash)] pub struct JumpSquatState { pub duration_counter: u32, } impl JumpSquatState { pub fn create() -> Self { Self { duration_counter: 0, } } } impl FighterStateImpl for JumpSquatState { const NAME: &'static str = "JumpSquat"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { // Special handling for wavedash from jump squat if state_context.input.has_command(FighterCommands::Shield) { // If aiming sideways and in the deadzone, do a perfect wavedash // TODO: Improve this so we don't have to go through the airborne state first let mut wavedash_dir = state_context .input .get_last_frame() .movement .normalize_or_zero(); let movement = state_context.input.get_last_frame().movement; if movement.y.is_zero() && !movement.x.is_zero() { wavedash_dir.y = -state_context.game_settings.input_common.stick_deadzone - FGi32::lit("-0.01"); wavedash_dir.x = movement.x.signum(); } if wavedash_dir == FGVec2::ZERO { wavedash_dir = FGVec2::lit("0.0", "-1.0"); } Some(FighterState::AirDodge(AirDodgeState { direction: wavedash_dir.normalize_or_zero(), })) } else if self.duration_counter >= state_context.fighter_attributes.jumpsquat_duration { info!( "Jump {}", state_context .input .get_last_frame() .is_held(DigitalAction::Jump) ); let jump_type = if state_context .input .get_last_frame() .is_held(DigitalAction::Jump) { JumpType::LongJump } else { JumpType::ShortHop }; Some(FighterState::Jump(JumpState { jump_type, airborne_state_common: AirborneStateCommon::default(), })) } else { None } } fn update(&mut self, state_context: &mut UpdateContext) { self.duration_counter += 1; ground::update_friction_motion(state_context); } fn check_collision_interrupt( &mut self, state_context: &mut CollisionContext, ) -> Option { ground::collision_interrupt(state_context, false) } fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.play_animation(AnimKind::JumpSquat, false); // Preserve buffered inputs while jump squat makes the transition // temporarily unavailable, matching Ultimate/HDR-style hold input. state_context.input.enable_hold_all(10); state_context.input.clear_command(FighterCommands::Jump); } fn on_exit(&mut self, state_context: &mut crate::fighter::state::ExitContext) { // The hold policy is state-scoped and must never leak into the next // state. Command lifetime itself remains independent of this policy. state_context.input.disable_hold_all(); } fn is_visually_grounded<'a>(&'a self) -> bool { true } } impl FighterStateImpl for JumpState { const NAME: &'static str = "Jump"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { air::air_interrupts_common(state_context, &self.airborne_state_common) } fn update(&mut self, state_context: &mut UpdateContext) { air::update_standard_motion(state_context, &mut self.airborne_state_common); } fn on_enter(&mut self, state_context: &mut EnterContext) { if let JumpType::DoubleJump = self.jump_type { state_context.input.clear_command(FighterCommands::Jump); } let movement_stick_x = state_context.input.get_last_frame().movement.x; match self.jump_type { JumpType::ShortHop | JumpType::LongJump => { if movement_stick_x.is_zero() || movement_stick_x.signum() == state_context.facing_direction.to_sign() { state_context.play_animation(AnimKind::JumpForward, false); } else { state_context.play_animation(AnimKind::JumpBack, false); } } JumpType::DoubleJump => { if movement_stick_x.is_zero() || movement_stick_x.signum() == state_context.facing_direction.to_sign() { state_context.play_animation(AnimKind::DoubleJumpForward, false); } else { state_context.play_animation(AnimKind::DoubleJumpBack, false); } } } let vertical_vel = match self.jump_type { JumpType::ShortHop => state_context.fighter_attributes.short_hop_vertical_velocity, JumpType::LongJump => state_context.fighter_attributes.full_jump_vertical_velocity, JumpType::DoubleJump => { state_context.fighter_attributes.full_jump_vertical_velocity * state_context.fighter_attributes.air_jump_multiplier } }; match self.jump_type { JumpType::ShortHop | JumpType::LongJump => { state_context.velocity.x += state_context.fighter_attributes.jump_horizontal_velocity * movement_stick_x; } JumpType::DoubleJump => { state_context.velocity.x = state_context .fighter_attributes .air_jump_horizontal_velocity * movement_stick_x; } }; state_context.velocity.y = vertical_vel; } fn check_collision_interrupt( &mut self, state_context: &mut CollisionContext, ) -> Option { air::collision_interrupt( state_context, crate::fighter::state::land::LandingType::Normal, true, ) } fn is_visually_grounded<'a>(&'a self) -> bool { false } fn surface_policy(&self) -> super::SurfacePolicy { use crate::fighter::surface::EcbLockPolicy; match self.jump_type { JumpType::DoubleJump => super::SurfacePolicy::Airborne(EcbLockPolicy::Restart), JumpType::ShortHop | JumpType::LongJump => { super::SurfacePolicy::Airborne(EcbLockPolicy::StartOnDetach) } } } } pub fn check_input(state_context: &InterruptContext) -> Option { state_context .input .has_command(FighterCommands::Jump) .then(|| FighterState::JumpSquat(JumpSquatState::create())) } pub fn check_input_double_jump(state_context: &InterruptContext) -> Option { state_context .input .has_command(FighterCommands::Jump) .then(|| { FighterState::Jump(JumpState { jump_type: JumpType::DoubleJump, airborne_state_common: AirborneStateCommon::default(), }) }) }