use crate::{ fighter::{ animation::AnimKind, attack::{self, FighterHitReaction}, state::{ CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, ground::collision_interrupt, land::LandingType, wait::WaitState, }, }, math::int::FGi32, }; #[derive(Clone, Debug, Hash)] pub enum HitstopStateKind { Grounded, Airborne, } #[derive(Clone, Debug, Hash)] pub struct HitstunState { pub hit_reaction: FighterHitReaction, pub hitstop_kind: HitstopStateKind, knockback_applied: bool, } impl HitstunState { pub fn new(hit_reaction: FighterHitReaction, hitstop_kind: HitstopStateKind) -> Self { Self { hit_reaction, hitstop_kind, knockback_applied: false, } } } impl FighterStateImpl for HitstunState { const NAME: &'static str = "Hitstun"; fn name(&self) -> String { let hitstop_kind = match self.hitstop_kind { HitstopStateKind::Grounded => "Grounded", HitstopStateKind::Airborne => "Airborne", }; let hit_reaction_type = match &self.hit_reaction.kind { crate::fighter::attack::HitReactionKind::Flinch => "Flinch", crate::fighter::attack::HitReactionKind::Tumble => "Tumble", }; format!("Hitstun ({hitstop_kind}, {hit_reaction_type})") } fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.get_current_animation_frame() > self.hit_reaction.hitstun_frames { match &self.hitstop_kind { HitstopStateKind::Grounded => Some(FighterState::Wait(WaitState)), HitstopStateKind::Airborne => Some(FighterState::Fall(FallState::new( AirborneStateCommon::default(), ))), } } else { None } } fn update(&mut self, ctx: &mut UpdateContext) { if !self.knockback_applied { let launch_direction = attack::directional_influence::apply_directional_influence( ctx.input.get_last_frame().movement, self.hit_reaction.launch_angle, ctx.game_settings.fighter_common.di_max_angle, ); ctx.knockback_velocity.0 = launch_direction * self.hit_reaction.launch_velocity; self.knockback_applied = true; } match self.hitstop_kind { HitstopStateKind::Grounded => {} HitstopStateKind::Airborne => { air::integrate_gravity(ctx.velocity, ctx.fighter_attributes, false); } } let speed = ctx.knockback_velocity.length(); let next_speed = speed .saturating_sub(ctx.game_settings.fighter_common.knockback_decay_per_frame) .max(FGi32::ZERO); ctx.knockback_velocity.0 = ctx.knockback_velocity.normalize_or_zero() * next_speed; ctx.translation.0 += ctx.velocity.0 + ctx.knockback_velocity.0; } fn on_enter(&mut self, state_context: &mut EnterContext) { let animation = match self.hitstop_kind { HitstopStateKind::Grounded => AnimKind::FlinchGround, HitstopStateKind::Airborne => AnimKind::FlinchAir, }; state_context.play_animation(animation, false); } fn on_exit(&mut self, _state_context: &mut ExitContext) {} fn check_collision_interrupt( &mut self, state_context: &mut CollisionContext, ) -> Option { match &mut self.hitstop_kind { HitstopStateKind::Grounded => collision_interrupt(state_context, true), HitstopStateKind::Airborne => { air::collision_interrupt(state_context, LandingType::Normal) } } } fn is_visually_grounded<'a>(&'a self) -> bool { match self.hitstop_kind { HitstopStateKind::Grounded => true, HitstopStateKind::Airborne => false, } } fn surface_policy(&self) -> super::SurfacePolicy { match self.hitstop_kind { HitstopStateKind::Grounded => super::SurfacePolicy::Grounded, HitstopStateKind::Airborne => super::SurfacePolicy::Airborne( crate::fighter::surface::EcbLockPolicy::StartOnDetach, ), } } }