- Add crouch cancelling - Add IASA for aerial attacks - Add scaffolding for autocancel - Add hitbox group system - Add new ergonomic features for attack scripting - Add more information to the hitbox viewer - Improve the looks of debug capsules Reviewed-on: #82 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
169 lines
5.5 KiB
Rust
169 lines
5.5 KiB
Rust
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<FighterState> {
|
|
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<FighterState> {
|
|
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<Self, AirAttackStateCreateError> {
|
|
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<FighterState> {
|
|
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
|
|
}
|
|
}
|