Files
shinespark/src/fighter/attack/reaction.rs
T
EIREXE d2d649b059
CI / build-and-test (push) Successful in 1m35s
CI / production-build (push) Successful in 11m29s
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>
2026-10-04 18:57:29 +00:00

216 lines
7.2 KiB
Rust

use bevy::prelude::*;
use bevy_ggrs::prelude::GgrsSchedule;
use super::*;
use crate::{
fighter::{
Fighter, FighterHitboxes, FighterHitstop, FighterHitstopShake,
FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPendingHitReaction,
FighterStaleMoveQueue, FighterSurfaceState, errors::FighterErrors,
manifest::FighterManifest, state::FighterState,
},
game_settings::GameSettings,
math::int::FGi32,
player::Player,
schedule::GameplaySet,
scripting::FighterAttackScript,
};
pub fn resolve_fighter_hits(
mut pending_hits: ResMut<FighterPendingHits>,
mut attacker: Query<(
&mut FighterStaleMoveQueue,
&mut FighterHitboxes,
&Player,
&mut FighterHitstop,
&mut FighterHitstopShake,
&FighterTranslation,
)>,
mut receiver: Query<(
&mut FighterDamage,
&Fighter,
&mut FighterPendingHitReaction,
&FighterState,
&FighterInvulnerabilityState,
&FighterSurfaceState,
&FighterTranslation,
)>,
attack_scripts: Res<Assets<FighterAttackScript>>,
game_settings: Res<GameSettings>,
fighter_manifests: Res<Assets<FighterManifest>>,
) -> Result {
let hits = std::mem::take(&mut pending_hits.0);
let mut hit_offset = 0;
while hit_offset < hits.len() {
let defender = hits[hit_offset].defender;
let next_defender = hits[hit_offset..]
.iter()
.position(|hit| hit.defender != defender)
.map_or(hits.len(), |offset| hit_offset + offset);
let Ok((
mut receiver_damage,
fighter,
mut pending_reaction,
receiver_state,
invuln_state,
receiver_surface_state,
receiver_translation,
)) = receiver.get_mut(defender)
else {
hit_offset = next_defender;
continue;
};
let damage_data = hits[hit_offset..next_defender]
.iter()
.filter_map(|hit| {
let Ok((stale_move_queue, _, player, _, _, attacker_translation)) =
attacker.get(hit.attacker)
else {
return None;
};
let attack_script = attack_scripts.get(&hit.attack_script)?;
let hitbox = attack_script.hitboxes.get(hit.hitbox_index)?;
let damage_received = stale_move_queue
.get_attack_damage_multiplier(hit.attack_kind)
* hitbox.damage.0;
let angle_flip = receiver_translation.0.x <= attacker_translation.0.x;
Some((
hit.attacker,
damage_received,
hitbox,
player.handle,
angle_flip,
))
})
.collect::<Vec<_>>();
let total_damage_received_this_frame = damage_data
.iter()
.map(|(_, damage, ..)| damage)
.sum::<FGi32>();
let fighter_manifest = fighter_manifests
.get(&fighter.manifest)
.ok_or(FighterErrors::FighterManifestNotFound)?;
let mut reactions = Vec::with_capacity(damage_data.len());
let crouch_canceling = if let FighterState::Crouch(crouch_state) = &receiver_state {
crouch_state.is_crouch_cancelling()
} else {
false
};
for (attacker_entity, attack_damage, hitbox, attacker_slot, angle_flip) in damage_data {
let knockback = hitbox.calculate_knockback(
&fighter_manifest.attributes,
*receiver_damage,
FighterDamage(total_damage_received_this_frame),
false,
crouch_canceling,
);
let launch_angle = hitbox.angle.get_angle(
&knockback,
receiver_surface_state.is_grounded(),
&game_settings,
);
let launch_angle = if angle_flip {
FGi32::lit("180.0") - launch_angle
} else {
launch_angle
};
// Attacker hitstop and data
let victim_hitstop_frames = HitStop::calculate(FighterDamage(attack_damage), crouch_canceling);
let attacker_hitstop_frames = HitStop::calculate(FighterDamage(attack_damage), false);
reactions.push((
attacker_entity,
attacker_slot,
FighterHitReaction {
launch_velocity: knockback.get_launch_speed(&game_settings),
launch_angle,
hitstun_frames: knockback.get_hitstun_frames(&game_settings),
victim_hitstop_frames,
attacker_hitstop_frames,
kind: HitReactionKind::Flinch,
damage: FighterDamage(attack_damage),
},
));
}
match invuln_state {
FighterInvulnerabilityState::Vulnerable => {
receiver_damage.0 += total_damage_received_this_frame
}
_ => (),
}
for (attacker_entity, _, reaction) in &reactions {
let Ok((.., _, mut hitstop, mut hitstop_shake, _)) = attacker.get_mut(*attacker_entity)
else {
continue;
};
if hitstop.remaining_frames < reaction.attacker_hitstop_frames.0 {
let shake_kind = if receiver_state.is_visually_grounded() {
FighterHitstopShakeKind::Horizontal
} else {
FighterHitstopShakeKind::Vertical
};
hitstop.remaining_frames = reaction.attacker_hitstop_frames.0;
let shake_frames = reaction
.attacker_hitstop_frames
.calculate_shake_frames(&game_settings);
hitstop_shake.total_frames = shake_frames;
hitstop_shake.remaining_frames = shake_frames;
hitstop_shake.kind = shake_kind;
hitstop_shake.role = FighterHitstopShakeRole::Attacker;
}
}
match invuln_state {
FighterInvulnerabilityState::Vulnerable => {
pending_reaction.0 = reactions
.into_iter()
.min_by(|(_, port_a, _), (_, port_b, _)| port_a.cmp(port_b))
.map(|(_, _, reaction)| reaction);
}
_ => (),
}
hit_offset = next_defender;
}
for hit in hits {
let Ok((mut stale_move_queue, mut hitboxes, ..)) =
attacker.get_mut(hit.attacker)
else {
continue;
};
if let Some(ref mut hitbox) = hitboxes.0 {
if !hitbox.stalled {
hitbox.stalled = true;
stale_move_queue.push(hitbox.kind);
}
}
}
Ok(())
}
pub struct FighterReactionPlugin;
impl Plugin for FighterReactionPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
GgrsSchedule,
resolve_fighter_hits
.in_set(GameplaySet::Attacks)
.after(super::intersect_attacks_with_hurtboxes),
);
}
}