use bevy::log::error; use crate::fighter::{ attack::{AttackKind, FighterAttackScriptAssets, update_active_attack}, state::{ CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_attack, fall::FallState, jump, }, }; #[derive(Clone, Debug, Hash)] pub struct AirAttackState { pub attack_kind: AttackKind, pub airborne_state_common: AirborneStateCommon, pub is_in_autocancel_window: bool, } impl FighterStateImpl for AirAttackState { const NAME: &'static str = "AirAttack"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { let animation_frame = state_context.get_current_animation_frame(); let script = state_context .attack_scripts .get(&state_context.attack_script_assets.scripts[&self.attack_kind]); if let Some(script) = script { if let Some(iasa_frame) = script.iasa_frame { if animation_frame >= iasa_frame { let new_state = jump::check_input_double_jump(state_context).or_else(|| air_attack::check_interrupt(state_context, &self.airborne_state_common)); if let Some(new_state) = new_state { return Some(new_state); } } } } if state_context.is_current_animation_finished() { Some(FighterState::Fall(FallState::new( self.airborne_state_common, ))) } else { None } } fn update(&mut self, state_context: &mut UpdateContext) { let animation_frame = state_context.get_current_animation_frame(); let script = state_context .attack_scripts .get(&state_context.attack_script_assets.scripts[&self.attack_kind]); if let Some(script) = script { update_active_attack( script, state_context .hitboxes .0 .as_mut() .expect("ground attack must have active hitboxes"), animation_frame, ); if let Some((autocancel_start, autocancel_end)) = script.wont_autocancel_window { self.is_in_autocancel_window = animation_frame < autocancel_start || animation_frame > autocancel_end; } } air::update_standard_motion(state_context, &mut self.airborne_state_common); } fn on_enter(&mut self, state_context: &mut EnterContext) { state_context .input .consume_command(crate::input::FighterCommands::Attack); state_context.play_animation(self.attack_kind.get_animation(), false); let script = state_context.attack_script_assets.scripts[&self.attack_kind].clone(); state_context.hitboxes.0 = Some(crate::fighter::FighterActiveAttack { kind: self.attack_kind, script, active_hitboxes: Vec::new(), solved_hitboxes: Vec::new(), hit_groups: Vec::new(), stalled: false, }); } fn on_exit(&mut self, state_context: &mut ExitContext) { state_context.hitboxes.0 = None; } fn check_collision_interrupt( &mut self, state_context: &mut CollisionContext, ) -> Option { let coll_interrupt = air::collision_interrupt( state_context, crate::fighter::state::land::LandingType::Normal, false, ); coll_interrupt.map(|state| { if self.is_in_autocancel_window { state } else { // TODO: Implement proper landing lag state } }) } fn is_visually_grounded<'a>(&'a self) -> bool { false } } #[derive(thiserror::Error, Debug)] pub enum AirAttackStateCreateError { #[error("Attack script: {attack:?} not found")] AttackScriptNotFound { attack: AttackKind }, } impl AirAttackState { pub fn create( attack_kind: AttackKind, attack_script_assets: &FighterAttackScriptAssets, airborne_state_common: &AirborneStateCommon, ) -> Result { if attack_script_assets.scripts.contains_key(&attack_kind) { Ok(AirAttackState { airborne_state_common: airborne_state_common.clone(), attack_kind, is_in_autocancel_window: false, }) } else { Err(AirAttackStateCreateError::AttackScriptNotFound { attack: attack_kind, }) } } } pub fn check_interrupt( state_context: &InterruptContext, airborne_state_common: &AirborneStateCommon, ) -> Option { if state_context .input .has_command(crate::input::FighterCommands::Attack) { let attack_kind = state_context .input .get_attack_command_type() .to_attack_kind_airborne(state_context.facing_direction); let state = AirAttackState::create( attack_kind, state_context.attack_script_assets, airborne_state_common, ); match state { Ok(state) => Some(FighterState::AirAttack(state)), Err(create_error) => { error!("{}", create_error); None } } } else { None } }