diff --git a/src/fighter/attack/mod.rs b/src/fighter/attack/mod.rs index 8a9d7c4..5f60914 100644 --- a/src/fighter/attack/mod.rs +++ b/src/fighter/attack/mod.rs @@ -52,7 +52,8 @@ pub enum HitReactionKind { pub struct FighterHitReaction { pub launch_velocity: FGi32, pub launch_angle: FGi32, - pub hitstop_frames: HitStop, + pub victim_hitstop_frames: HitStop, + pub attacker_hitstop_frames: HitStop, pub hitstun_frames: u32, pub kind: HitReactionKind, pub damage: FighterDamage, diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index c5b79b5..1a41522 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -25,6 +25,7 @@ pub fn resolve_fighter_hits( &Player, &mut FighterHitstop, &mut FighterHitstopShake, + &FighterTranslation, )>, mut receiver: Query<( &mut FighterDamage, @@ -33,6 +34,7 @@ pub fn resolve_fighter_hits( &FighterState, &FighterInvulnerabilityState, &FighterSurfaceState, + &FighterTranslation, )>, attack_scripts: Res>, game_settings: Res, @@ -55,6 +57,7 @@ pub fn resolve_fighter_hits( receiver_state, invuln_state, receiver_surface_state, + receiver_translation, )) = receiver.get_mut(defender) else { hit_offset = next_defender; @@ -64,7 +67,7 @@ pub fn resolve_fighter_hits( let damage_data = hits[hit_offset..next_defender] .iter() .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) else { return None; @@ -74,12 +77,13 @@ pub fn resolve_fighter_hits( 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, - matches!(facing_dir, FighterFacingDirection::Left), player.handle, + angle_flip, )) }) .collect::>(); @@ -93,28 +97,35 @@ pub fn resolve_fighter_hits( .ok_or(FighterErrors::FighterManifestNotFound)?; 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( &fighter_manifest.attributes, *receiver_damage, FighterDamage(total_damage_received_this_frame), false, - false, + crouch_canceling, ); - let angle = hitbox.angle.get_angle( + let launch_angle = hitbox.angle.get_angle( &knockback, receiver_surface_state.is_grounded(), &game_settings, ); - dbg!(knockback, angle); - let launch_angle = if facing_left { - FGi32::lit("180.0") - angle + + let launch_angle = if angle_flip { + FGi32::lit("180.0") - launch_angle } else { - angle + launch_angle }; // 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(( attacker_entity, attacker_slot, @@ -122,7 +133,8 @@ pub fn resolve_fighter_hits( launch_velocity: knockback.get_launch_speed(&game_settings), launch_angle, hitstun_frames: knockback.get_hitstun_frames(&game_settings), - hitstop_frames: hitstop, + victim_hitstop_frames, + attacker_hitstop_frames, kind: HitReactionKind::Flinch, damage: FighterDamage(attack_damage), }, @@ -137,20 +149,20 @@ pub fn resolve_fighter_hits( } 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 { 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() { FighterHitstopShakeKind::Horizontal } else { FighterHitstopShakeKind::Vertical }; - hitstop.remaining_frames = reaction.hitstop_frames.0; + hitstop.remaining_frames = reaction.attacker_hitstop_frames.0; let shake_frames = reaction - .hitstop_frames + .attacker_hitstop_frames .calculate_shake_frames(&game_settings); hitstop_shake.total_frames = shake_frames; hitstop_shake.remaining_frames = shake_frames; diff --git a/src/fighter/state/crouch.rs b/src/fighter/state/crouch.rs index 1cc4b10..18cd98c 100644 --- a/src/fighter/state/crouch.rs +++ b/src/fighter/state/crouch.rs @@ -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 { let input_frame = state_context.input.get_last_frame(); diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index b5879ad..0090ce5 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -494,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; @@ -515,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; diff --git a/src/scripting/move_compiler.rs b/src/scripting/move_compiler.rs index 622c779..ff17355 100644 --- a/src/scripting/move_compiler.rs +++ b/src/scripting/move_compiler.rs @@ -260,9 +260,8 @@ pub fn compile_script(text: &str) -> Result { ); } - // Keep the original script syntax working. A hitbox without an - // explicit group behaves as its own group, which preserves the old - // one-hit-per-target behavior. + // Keep the original script syntax working. Melee's default collision + // group is shared by all hitbox slots in an attack. { let mc = mc.clone(); engine.register_fn( @@ -280,7 +279,7 @@ pub fn compile_script(text: &str) -> Result { add_hitbox( &mc, id, - id, + 0, bone, damage, offset, @@ -461,4 +460,19 @@ mod tests { assert_eq!(script.hitboxes[0].id, 7); 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![0, 0]); + } }