use bevy::{asset::Assets, log::error}; use crate::{ fighter::{ attack::{AttackKind, FighterAttackScriptAssets, update_active_attack}, state::{ CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, ground, wait::WaitState, }, }, scripting::FighterAttackScript, }; #[derive(Clone, Debug, Hash)] pub struct GroundAttackState { pub attack_kind: AttackKind, pub use_root_motion: bool, } impl FighterStateImpl for GroundAttackState { const NAME: &'static str = "GroundAttack"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { // Allow interrupting dash attack only into an up/down smash if let AttackKind::DashAttack = self.attack_kind { if state_context.get_current_animation_frame() < state_context.fighter_attributes.dash_attack_cancel_window { if let Some(state) = check_interrupt(state_context) { if let FighterState::GroundAttack(state) = state { if matches!(state.attack_kind, AttackKind::UpSmash) || matches!(state.attack_kind, AttackKind::DownSmash) { return Some(FighterState::GroundAttack(state)); } } } } } if state_context.is_current_animation_finished() { Some(FighterState::Wait(WaitState)) } 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, ); // root motion if script.use_root_motion { state_context.velocity.0.x = state_context.animation_root_motion_delta.x; ground::apply_grounded_motion( state_context.velocity, state_context.translation, ); } else { ground::update_friction_motion(state_context); } } } 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: script.clone(), 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 { ground::collision_interrupt(state_context, !self.use_root_motion) } fn is_visually_grounded<'a>(&'a self) -> bool { true } } impl GroundAttackState { fn create( attack_kind: AttackKind, attack_script_assets: &FighterAttackScriptAssets, attack_scripts: &Assets, ) -> Result { let asset_script = attack_script_assets.scripts.get(&attack_kind).ok_or( AirAttackStateCreateError::AttackScriptAssetNotFound { attack: attack_kind, }, )?; let script = attack_scripts.get(asset_script).ok_or( AirAttackStateCreateError::AttackScriptNotFound { attack: attack_kind, }, )?; Ok(Self { attack_kind, use_root_motion: script.use_root_motion }) } } #[derive(thiserror::Error, Debug)] pub enum AirAttackStateCreateError { #[error("Attack script: {attack:?} not found")] AttackScriptNotFound { attack: AttackKind }, #[error("Attack script asset: {attack:?} not found")] AttackScriptAssetNotFound { attack: AttackKind }, } pub fn check_interrupt_shared(state_context: &InterruptContext, attack_kind: AttackKind) -> Option { let state = GroundAttackState::create(attack_kind, state_context.attack_script_assets, state_context.attack_scripts); match state { Ok(state) => Some(FighterState::GroundAttack(state)), Err(create_error) => { error!("{}", create_error); None }, } } pub fn check_interrupt(state_context: &InterruptContext) -> Option { if state_context .input .has_command(crate::input::FighterCommands::Attack) { let attack_kind = state_context .input .get_attack_command_type() .to_attack_kind_grounded(); check_interrupt_shared(state_context, attack_kind) } else { None } } pub fn check_interrupt_dash_attack(state_context: &InterruptContext) -> Option { if state_context .input .has_command(crate::input::FighterCommands::Attack) { check_interrupt_shared(state_context, AttackKind::DashAttack) } else { None } }