diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index f87cce0..667391f 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -98,4 +98,5 @@ pub enum AnimKind { AttackUpTilt, AttackForwardTilt, AttackDownTilt, + AttackNeutralAir, } diff --git a/src/fighter/attack/mod.rs b/src/fighter/attack/mod.rs index 134943e..d1581e3 100644 --- a/src/fighter/attack/mod.rs +++ b/src/fighter/attack/mod.rs @@ -179,7 +179,7 @@ pub enum AttackKind { impl AttackKind { pub fn get_animation(&self) -> AnimKind { match self { - AttackKind::NeutralAir => AnimKind::AttackJab1, + AttackKind::NeutralAir => AnimKind::AttackNeutralAir, AttackKind::Jab => AnimKind::AttackJab1, AttackKind::UpTilt => AnimKind::AttackUpTilt, AttackKind::ForwardAir => AnimKind::AttackUpTilt, diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index 17a7f3b..697a736 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -1,5 +1,6 @@ +use crate::fighter::state::jump::{JumpState, JumpType}; use crate::fighter::state::land::{LandingState, LandingType}; -use crate::fighter::state::{CollisionContext, FighterState}; +use crate::fighter::state::{CollisionContext, FighterState, InterruptContext, air_attack, air_dodge}; use crate::fighter::*; use crate::input::FighterCommands; @@ -112,6 +113,17 @@ pub fn update_standard_motion( ); } +pub fn air_interrupts_common(ctx: &mut InterruptContext, airborne_state_common: &AirborneStateCommon) -> Option { + if ctx.input.has_command(FighterCommands::Jump) { + Some(FighterState::Jump(JumpState { + jump_type: JumpType::DoubleJump, + airborne_state_common: AirborneStateCommon::default(), + })) + } else { + air_dodge::check_input(ctx).or_else(|| air_attack::check_interrupt(ctx, airborne_state_common)) + } +} + pub fn collision_interrupt( ctx: &mut CollisionContext, landing_type: LandingType, diff --git a/src/fighter/state/air_attack.rs b/src/fighter/state/air_attack.rs new file mode 100644 index 0000000..30fc8a0 --- /dev/null +++ b/src/fighter/state/air_attack.rs @@ -0,0 +1,124 @@ +use bevy::log::error; + +use crate::fighter::{ + FighterId, attack::{AttackKind, FighterAttackScriptAssets, update_active_hitbox_list}, state::{ + CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, wait::WaitState, + }, +}; + +#[derive(Clone, Debug, Hash)] +pub struct AirAttackState { + pub attack_kind: AttackKind, + pub airborne_state_common: AirborneStateCommon +} + +impl FighterStateImpl for AirAttackState { + const NAME: &'static str = "AirAttack"; + + fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { + 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_hitbox_list( + script, + &mut state_context + .hitboxes + .0 + .as_mut() + .expect("ground attack must have active hitboxes") + .active_hitboxes, + animation_frame, + ); + } + + air::update_standard_motion(state_context, &mut self.airborne_state_common); + } + + fn on_enter(&mut self, state_context: &mut EnterContext) { + 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_fighters: 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 { + air::collision_interrupt( + state_context, + crate::fighter::state::land::LandingType::Normal, + false, + ) + } + + 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 + }) + } 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 + } +} diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index e945184..e40f9a8 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -1,7 +1,7 @@ use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext}; use crate::fighter::animation::AnimKind; use crate::fighter::state::air::AirborneStateCommon; -use crate::fighter::state::{FighterState, air, jump}; +use crate::fighter::state::{FighterState, air, air_attack, jump}; #[derive(Debug, Clone, Copy, Hash, Default)] pub struct FallState { @@ -20,7 +20,7 @@ impl FighterStateImpl for FallState { const NAME: &'static str = "Fall"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { - jump::check_input_double_jump(state_context) + jump::check_input_double_jump(state_context).or_else(|| air_attack::check_interrupt(state_context, &self.airborne_state_common)) } fn on_enter(&mut self, state_context: &mut EnterContext) { diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index 77efb44..5f07da4 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -23,8 +23,8 @@ pub enum JumpType { #[derive(Debug, Clone, Hash)] pub struct JumpState { - jump_type: JumpType, - airborne_state_common: AirborneStateCommon, + pub jump_type: JumpType, + pub airborne_state_common: AirborneStateCommon, } #[derive(Debug, Clone, Hash)] @@ -108,14 +108,7 @@ impl FighterStateImpl for JumpSquatState { impl FighterStateImpl for JumpState { const NAME: &'static str = "Jump"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { - if state_context.input.has_command(FighterCommands::Jump) { - Some(FighterState::Jump(JumpState { - jump_type: JumpType::DoubleJump, - airborne_state_common: AirborneStateCommon::default(), - })) - } else { - air_dodge::check_input(state_context) - } + air::air_interrupts_common(state_context, &self.airborne_state_common) } fn update(&mut self, state_context: &mut UpdateContext) { diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 7491998..6738c0f 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -47,6 +47,7 @@ pub mod run; pub mod turn; pub mod wait; pub mod walk; +pub mod air_attack; #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub enum SurfacePolicy { @@ -135,6 +136,7 @@ pub struct InterruptContext<'a> { pub surface: &'a FighterSurfaceState, pub animation: AnimationStatus, pub stage_collision: &'a StageCollision, + pub attack_script_assets: &'a FighterAttackScriptAssets, } impl InterruptContext<'_> { @@ -246,6 +248,7 @@ fighter_states! { GroundAttack => ground_attack::GroundAttackState, Hitstun => hitstun::HitstunState, Angel => angel::AngelState, + AirAttack => air_attack::AirAttackState, } #[derive(QueryData)] @@ -292,6 +295,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { baked_animations: &'a Assets, attributes: &'a FighterAttributes, stage_collision: &'a StageCollision, + attack_script_assets: &'a FighterAttackScriptAssets, ) -> InterruptContext<'a> { let animation = self.animation_status(animations, baked_animations); InterruptContext { @@ -302,6 +306,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { animation, surface: &self.surface, stage_collision, + attack_script_assets: attack_script_assets } } @@ -535,6 +540,7 @@ pub fn state_interrupt_system( &baked_animations, &manifest.attributes, &stage_collision, + attack_script_assets )) }; diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 5426425..33f111a 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -9,6 +9,7 @@ use bevy_ggrs::{PlayerInputs, Rollback}; use strum::IntoEnumIterator; use strum_macros::EnumIter; +use crate::fighter::FighterFacingDirection; use crate::fighter::attack::AttackKind; use crate::game_settings::GameSettings; use crate::input::FighterInputFrame; @@ -54,6 +55,26 @@ impl FighterAttackCommandType { FighterAttackCommandType::TiltAttackRight => AttackKind::ForwardTilt, } } + pub fn to_attack_kind_airborne(&self, facing_direction: &FighterFacingDirection) -> AttackKind { + match self { + FighterAttackCommandType::Neutral => AttackKind::NeutralAir, + FighterAttackCommandType::SmashAttackUp | FighterAttackCommandType::TiltAttackUp => AttackKind::UpAir, + FighterAttackCommandType::SmashAttackDown | FighterAttackCommandType::TiltAttackDown => AttackKind::DownAir, + + FighterAttackCommandType::SmashAttackLeft | FighterAttackCommandType::TiltAttackLeft => { + match facing_direction { + FighterFacingDirection::Left => AttackKind::ForwardAir, + FighterFacingDirection::Right => AttackKind::BackAir, + } + }, + FighterAttackCommandType::SmashAttackRight | FighterAttackCommandType::TiltAttackRight => { + match facing_direction { + FighterFacingDirection::Left => AttackKind::BackAir, + FighterFacingDirection::Right => AttackKind::ForwardAir, + } + }, + } + } } /// Frames of life remaining for each buffered command. Indexed by