Attack system revamp #82

Merged
EIREXE merged 5 commits from attack_groups into master 2026-10-04 18:57:29 +00:00
5 changed files with 59 additions and 23 deletions
Showing only changes of commit 2aa2c52555 - Show all commits
+2 -1
View File
@@ -52,7 +52,8 @@ pub enum HitReactionKind {
pub struct FighterHitReaction { pub struct FighterHitReaction {
pub launch_velocity: FGi32, pub launch_velocity: FGi32,
pub launch_angle: FGi32, pub launch_angle: FGi32,
pub hitstop_frames: HitStop, pub victim_hitstop_frames: HitStop,
pub attacker_hitstop_frames: HitStop,
pub hitstun_frames: u32, pub hitstun_frames: u32,
pub kind: HitReactionKind, pub kind: HitReactionKind,
pub damage: FighterDamage, pub damage: FighterDamage,
+27 -15
View File
@@ -25,6 +25,7 @@ pub fn resolve_fighter_hits(
&Player, &Player,
&mut FighterHitstop, &mut FighterHitstop,
&mut FighterHitstopShake, &mut FighterHitstopShake,
&FighterTranslation,
)>, )>,
mut receiver: Query<( mut receiver: Query<(
&mut FighterDamage, &mut FighterDamage,
@@ -33,6 +34,7 @@ pub fn resolve_fighter_hits(
&FighterState, &FighterState,
&FighterInvulnerabilityState, &FighterInvulnerabilityState,
&FighterSurfaceState, &FighterSurfaceState,
&FighterTranslation,
)>, )>,
attack_scripts: Res<Assets<FighterAttackScript>>, attack_scripts: Res<Assets<FighterAttackScript>>,
game_settings: Res<GameSettings>, game_settings: Res<GameSettings>,
@@ -55,6 +57,7 @@ pub fn resolve_fighter_hits(
receiver_state, receiver_state,
invuln_state, invuln_state,
receiver_surface_state, receiver_surface_state,
receiver_translation,
)) = receiver.get_mut(defender) )) = receiver.get_mut(defender)
else { else {
hit_offset = next_defender; hit_offset = next_defender;
@@ -64,7 +67,7 @@ pub fn resolve_fighter_hits(
let damage_data = hits[hit_offset..next_defender] let damage_data = hits[hit_offset..next_defender]
.iter() .iter()
.filter_map(|hit| { .filter_map(|hit| {
let Ok((stale_move_queue, facing_dir, _, player, _, _)) = let Ok((stale_move_queue, facing_dir, _, player, _, _, attacker_translation)) =
attacker.get(hit.attacker) attacker.get(hit.attacker)
else { else {
return None; return None;
@@ -74,12 +77,13 @@ pub fn resolve_fighter_hits(
let damage_received = stale_move_queue let damage_received = stale_move_queue
.get_attack_damage_multiplier(hit.attack_kind) .get_attack_damage_multiplier(hit.attack_kind)
* hitbox.damage.0; * hitbox.damage.0;
let angle_flip = receiver_translation.0.x <= attacker_translation.0.x;
Some(( Some((
hit.attacker, hit.attacker,
damage_received, damage_received,
hitbox, hitbox,
matches!(facing_dir, FighterFacingDirection::Left),
player.handle, player.handle,
angle_flip,
)) ))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@@ -93,28 +97,35 @@ pub fn resolve_fighter_hits(
.ok_or(FighterErrors::FighterManifestNotFound)?; .ok_or(FighterErrors::FighterManifestNotFound)?;
let mut reactions = Vec::with_capacity(damage_data.len()); let mut reactions = Vec::with_capacity(damage_data.len());
for (attacker_entity, attack_damage, hitbox, facing_left, attacker_slot) in damage_data { 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( let knockback = hitbox.calculate_knockback(
&fighter_manifest.attributes, &fighter_manifest.attributes,
*receiver_damage, *receiver_damage,
FighterDamage(total_damage_received_this_frame), FighterDamage(total_damage_received_this_frame),
false, false,
false, crouch_canceling,
); );
let angle = hitbox.angle.get_angle( let launch_angle = hitbox.angle.get_angle(
&knockback, &knockback,
receiver_surface_state.is_grounded(), receiver_surface_state.is_grounded(),
&game_settings, &game_settings,
); );
dbg!(knockback, angle);
let launch_angle = if facing_left { let launch_angle = if angle_flip {
FGi32::lit("180.0") - angle FGi32::lit("180.0") - launch_angle
} else { } else {
angle launch_angle
}; };
// Attacker hitstop and data // Attacker hitstop and data
let hitstop = HitStop::calculate(FighterDamage(attack_damage), matches!(receiver_state, FighterState::Crouch(..))); let victim_hitstop_frames = HitStop::calculate(FighterDamage(attack_damage), crouch_canceling);
let attacker_hitstop_frames = HitStop::calculate(FighterDamage(attack_damage), false);
reactions.push(( reactions.push((
attacker_entity, attacker_entity,
attacker_slot, attacker_slot,
@@ -122,7 +133,8 @@ pub fn resolve_fighter_hits(
launch_velocity: knockback.get_launch_speed(&game_settings), launch_velocity: knockback.get_launch_speed(&game_settings),
launch_angle, launch_angle,
hitstun_frames: knockback.get_hitstun_frames(&game_settings), hitstun_frames: knockback.get_hitstun_frames(&game_settings),
hitstop_frames: hitstop, victim_hitstop_frames,
attacker_hitstop_frames,
kind: HitReactionKind::Flinch, kind: HitReactionKind::Flinch,
damage: FighterDamage(attack_damage), damage: FighterDamage(attack_damage),
}, },
@@ -137,20 +149,20 @@ pub fn resolve_fighter_hits(
} }
for (attacker_entity, _, reaction) in &reactions { for (attacker_entity, _, reaction) in &reactions {
let Ok((.., _, mut hitstop, mut hitstop_shake)) = attacker.get_mut(*attacker_entity) let Ok((.., _, mut hitstop, mut hitstop_shake, _)) = attacker.get_mut(*attacker_entity)
else { else {
continue; continue;
}; };
if hitstop.remaining_frames < reaction.hitstop_frames.0 { if hitstop.remaining_frames < reaction.attacker_hitstop_frames.0 {
let shake_kind = if receiver_state.is_visually_grounded() { let shake_kind = if receiver_state.is_visually_grounded() {
FighterHitstopShakeKind::Horizontal FighterHitstopShakeKind::Horizontal
} else { } else {
FighterHitstopShakeKind::Vertical FighterHitstopShakeKind::Vertical
}; };
hitstop.remaining_frames = reaction.hitstop_frames.0; hitstop.remaining_frames = reaction.attacker_hitstop_frames.0;
let shake_frames = reaction let shake_frames = reaction
.hitstop_frames .attacker_hitstop_frames
.calculate_shake_frames(&game_settings); .calculate_shake_frames(&game_settings);
hitstop_shake.total_frames = shake_frames; hitstop_shake.total_frames = shake_frames;
hitstop_shake.remaining_frames = shake_frames; hitstop_shake.remaining_frames = shake_frames;
+9
View File
@@ -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> { pub fn check_input(state_context: &InterruptContext) -> Option<FighterState> {
let input_frame = state_context.input.get_last_frame(); let input_frame = state_context.input.get_last_frame();
+3 -3
View File
@@ -494,9 +494,9 @@ pub fn state_interrupt_system(
.expect("Fighter manifest should be valid"); .expect("Fighter manifest should be valid");
let new_state = if let Some(reaction) = pending_hit_reaction.take() { 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 let shake_frames = reaction
.hitstop_frames .victim_hitstop_frames
.calculate_shake_frames(&game_settings); .calculate_shake_frames(&game_settings);
hitstop_shake.role = FighterHitstopShakeRole::Receiver; hitstop_shake.role = FighterHitstopShakeRole::Receiver;
hitstop_shake.total_frames = shake_frames; hitstop_shake.total_frames = shake_frames;
@@ -515,7 +515,7 @@ pub fn state_interrupt_system(
if let hitstun::HitstopStateKind::Grounded = hitstop_kind { if let hitstun::HitstopStateKind::Grounded = hitstop_kind {
let straight_up_angle = FGi32::lit("90.0"); 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::new(game_settings.fighter_common.tumble_bounce_hitstop_threshold)
{ {
hitstop_kind = hitstun::HitstopStateKind::Airborne; hitstop_kind = hitstun::HitstopStateKind::Airborne;
+18 -4
View File
@@ -260,9 +260,8 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
); );
} }
// Keep the original script syntax working. A hitbox without an // Keep the original script syntax working. Melee's default collision
// explicit group behaves as its own group, which preserves the old // group is shared by all hitbox slots in an attack.
// one-hit-per-target behavior.
{ {
let mc = mc.clone(); let mc = mc.clone();
engine.register_fn( engine.register_fn(
@@ -280,7 +279,7 @@ pub fn compile_script(text: &str) -> Result<FighterAttackScript, String> {
add_hitbox( add_hitbox(
&mc, &mc,
id, id,
id, 0,
bone, bone,
damage, damage,
offset, offset,
@@ -461,4 +460,19 @@ mod tests {
assert_eq!(script.hitboxes[0].id, 7); assert_eq!(script.hitboxes[0].id, 7);
assert_eq!(script.hitboxes[0].group, 3); assert_eq!(script.hitboxes[0].group, 3);
} }
#[test]
fn legacy_hitboxes_share_the_default_collision_group() {
let script = compile_script(
r#"
frame(1);
hitbox(0, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle("0"), knockback_normal(), knockback("1"), fg32("1"));
hitbox(1, "hand_l", damage("1"), vec3("0", "0", "0"), fg32("1"), angle("0"), knockback_normal(), knockback("1"), fg32("1"));
frame(2);
"#,
)
.unwrap();
assert_eq!(script.hitboxes.iter().map(|hitbox| hitbox.group).collect::<Vec<_>>(), vec![0, 0]);
}
} }