Attack system revamp (#82)
- 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>
This commit was merged in pull request #82.
This commit is contained in:
@@ -1,23 +1,42 @@
|
||||
use bevy::log::error;
|
||||
|
||||
use crate::fighter::{
|
||||
attack::{AttackKind, FighterAttackScriptAssets, update_active_hitbox_list}, state::{
|
||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState,
|
||||
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 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)))
|
||||
Some(FighterState::Fall(FallState::new(
|
||||
self.airborne_state_common,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -29,16 +48,19 @@ impl FighterStateImpl for AirAttackState {
|
||||
.attack_scripts
|
||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||
if let Some(script) = script {
|
||||
update_active_hitbox_list(
|
||||
update_active_attack(
|
||||
script,
|
||||
&mut state_context
|
||||
state_context
|
||||
.hitboxes
|
||||
.0
|
||||
.as_mut()
|
||||
.expect("ground attack must have active hitboxes")
|
||||
.active_hitboxes,
|
||||
.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);
|
||||
@@ -55,7 +77,7 @@ impl FighterStateImpl for AirAttackState {
|
||||
script,
|
||||
active_hitboxes: Vec::new(),
|
||||
solved_hitboxes: Vec::new(),
|
||||
hit_fighters: Vec::new(),
|
||||
hit_groups: Vec::new(),
|
||||
stalled: false,
|
||||
});
|
||||
}
|
||||
@@ -68,11 +90,20 @@ impl FighterStateImpl for AirAttackState {
|
||||
&mut self,
|
||||
state_context: &mut CollisionContext,
|
||||
) -> Option<FighterState> {
|
||||
air::collision_interrupt(
|
||||
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 {
|
||||
@@ -83,25 +114,33 @@ impl FighterStateImpl for AirAttackState {
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AirAttackStateCreateError {
|
||||
#[error("Attack script: {attack:?} not found")]
|
||||
AttackScriptNotFound{attack: AttackKind},
|
||||
AttackScriptNotFound { attack: AttackKind },
|
||||
}
|
||||
|
||||
impl AirAttackState {
|
||||
pub fn create(attack_kind: AttackKind, attack_script_assets: &FighterAttackScriptAssets, airborne_state_common: &AirborneStateCommon) -> Result<Self, AirAttackStateCreateError> {
|
||||
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
|
||||
attack_kind,
|
||||
is_in_autocancel_window: false,
|
||||
})
|
||||
} else {
|
||||
Err(AirAttackStateCreateError::AttackScriptNotFound { attack: attack_kind })
|
||||
Err(AirAttackStateCreateError::AttackScriptNotFound {
|
||||
attack: attack_kind,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common: &AirborneStateCommon) -> Option<FighterState> {
|
||||
pub fn check_interrupt(
|
||||
state_context: &InterruptContext,
|
||||
airborne_state_common: &AirborneStateCommon,
|
||||
) -> Option<FighterState> {
|
||||
if state_context
|
||||
.input
|
||||
.has_command(crate::input::FighterCommands::Attack)
|
||||
@@ -110,16 +149,18 @@ pub fn check_interrupt(state_context: &InterruptContext, airborne_state_common:
|
||||
.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);
|
||||
let state = AirAttackState::create(
|
||||
attack_kind,
|
||||
state_context.attack_script_assets,
|
||||
airborne_state_common,
|
||||
);
|
||||
|
||||
match state {
|
||||
Ok(state) => {
|
||||
Some(FighterState::AirAttack(state))
|
||||
},
|
||||
Ok(state) => Some(FighterState::AirAttack(state)),
|
||||
Err(create_error) => {
|
||||
error!("{}", create_error);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -148,6 +148,15 @@ impl FighterStateImpl for CrouchState {
|
||||
}
|
||||
}
|
||||
|
||||
impl CrouchState {
|
||||
pub fn is_crouch_cancelling(&self) -> bool {
|
||||
match self.sub_state {
|
||||
CrouchSubState::Entering { .. } | CrouchSubState::Idle => true,
|
||||
CrouchSubState::Exiting => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
|
||||
let input_frame = state_context.input.get_last_frame();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::fighter::{
|
||||
attack::{AttackKind, update_active_hitbox_list},
|
||||
attack::{AttackKind, update_active_attack},
|
||||
state::{
|
||||
CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl,
|
||||
InterruptContext, UpdateContext, wait::WaitState,
|
||||
@@ -28,14 +28,13 @@ impl FighterStateImpl for GroundAttackState {
|
||||
.attack_scripts
|
||||
.get(&state_context.attack_script_assets.scripts[&self.attack_kind]);
|
||||
if let Some(script) = script {
|
||||
update_active_hitbox_list(
|
||||
update_active_attack(
|
||||
script,
|
||||
&mut state_context
|
||||
state_context
|
||||
.hitboxes
|
||||
.0
|
||||
.as_mut()
|
||||
.expect("ground attack must have active hitboxes")
|
||||
.active_hitboxes,
|
||||
.expect("ground attack must have active hitboxes"),
|
||||
animation_frame,
|
||||
);
|
||||
}
|
||||
@@ -52,7 +51,7 @@ impl FighterStateImpl for GroundAttackState {
|
||||
script,
|
||||
active_hitboxes: Vec::new(),
|
||||
solved_hitboxes: Vec::new(),
|
||||
hit_fighters: Vec::new(),
|
||||
hit_groups: Vec::new(),
|
||||
stalled: false,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ pub struct InterruptContext<'a> {
|
||||
pub surface: &'a FighterSurfaceState,
|
||||
pub animation: AnimationStatus,
|
||||
pub stage_collision: &'a StageCollision,
|
||||
pub attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
pub attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
}
|
||||
|
||||
@@ -300,6 +301,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
baked_animations: &'a Assets<BakedFighterAnimations>,
|
||||
attributes: &'a FighterAttributes,
|
||||
stage_collision: &'a StageCollision,
|
||||
attack_scripts: &'a Assets<FighterAttackScript>,
|
||||
attack_script_assets: &'a FighterAttackScriptAssets,
|
||||
) -> InterruptContext<'a> {
|
||||
let animation = self.animation_status(animations, baked_animations);
|
||||
@@ -312,6 +314,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> {
|
||||
surface: &self.surface,
|
||||
stage_collision,
|
||||
attack_script_assets: attack_script_assets,
|
||||
attack_scripts,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,6 +474,7 @@ pub fn state_interrupt_system(
|
||||
game_settings: Res<GameSettings>,
|
||||
baked_animations: Res<Assets<BakedFighterAnimations>>,
|
||||
fighter_manifests: Res<Assets<FighterManifest>>,
|
||||
attack_scripts: Res<Assets<FighterAttackScript>>,
|
||||
stage_collision: Res<StageCollision>,
|
||||
) {
|
||||
for fighter in &mut query {
|
||||
@@ -490,9 +494,9 @@ pub fn state_interrupt_system(
|
||||
.expect("Fighter manifest should be valid");
|
||||
|
||||
let new_state = if let Some(reaction) = pending_hit_reaction.take() {
|
||||
hitstop.remaining_frames = *reaction.hitstop_frames;
|
||||
hitstop.remaining_frames = *reaction.victim_hitstop_frames;
|
||||
let shake_frames = reaction
|
||||
.hitstop_frames
|
||||
.victim_hitstop_frames
|
||||
.calculate_shake_frames(&game_settings);
|
||||
hitstop_shake.role = FighterHitstopShakeRole::Receiver;
|
||||
hitstop_shake.total_frames = shake_frames;
|
||||
@@ -511,7 +515,7 @@ pub fn state_interrupt_system(
|
||||
|
||||
if let hitstun::HitstopStateKind::Grounded = hitstop_kind {
|
||||
let straight_up_angle = FGi32::lit("90.0");
|
||||
if reaction.hitstop_frames
|
||||
if reaction.victim_hitstop_frames
|
||||
>= HitStop::new(game_settings.fighter_common.tumble_bounce_hitstop_threshold)
|
||||
{
|
||||
hitstop_kind = hitstun::HitstopStateKind::Airborne;
|
||||
@@ -547,6 +551,7 @@ pub fn state_interrupt_system(
|
||||
&baked_animations,
|
||||
&manifest.attributes,
|
||||
&stage_collision,
|
||||
&attack_scripts,
|
||||
attack_script_assets,
|
||||
))
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user