From dd30dec1a0faaa30d5b70ada53b2fdd6974312bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Thu, 17 Sep 2026 19:55:49 +0200 Subject: [PATCH] Specify ground to air transitions and thresholds --- src/fighter/attack/mod.rs | 6 +++++- src/fighter/attack/reaction.rs | 1 + src/fighter/state/mod.rs | 26 +++++++++++++++++++++++--- src/game_settings.rs | 16 ++++++++++++++++ 4 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/fighter/attack/mod.rs b/src/fighter/attack/mod.rs index 316ac4f..6d9aa90 100644 --- a/src/fighter/attack/mod.rs +++ b/src/fighter/attack/mod.rs @@ -43,6 +43,7 @@ pub enum HitReactionKind { #[derive(Debug, Hash, Clone)] pub struct FighterHitReaction { pub launch_velocity: FGVec2, + pub launch_angle: FGi32, pub hitstop_frames: HitStop, pub hitstun_frames: u32, pub kind: HitReactionKind, @@ -62,10 +63,13 @@ pub struct FighterLastAttackDebug { pub trajectory_points: Vec, } -#[derive(Debug, Default, Clone, Deref, DerefMut, Hash)] +#[derive(Debug, Default, Clone, Deref, DerefMut, Hash, PartialEq, PartialOrd)] pub struct HitStop(u32); impl HitStop { + pub fn new(hitstop: u32) -> Self { + HitStop(hitstop) + } pub fn calculate(attack_damage_stalled: FighterDamage, crouch_cancelling: bool) -> Self { let a = (attack_damage_stalled.0 / FGi32::lit("3.0") + FGi32::lit("4.0")).round_to_zero(); let crouch_cancelling_factor = if crouch_cancelling { diff --git a/src/fighter/attack/reaction.rs b/src/fighter/attack/reaction.rs index ca7f27a..10e827d 100644 --- a/src/fighter/attack/reaction.rs +++ b/src/fighter/attack/reaction.rs @@ -104,6 +104,7 @@ pub fn resolve_fighter_hits( attacker_slot, FighterHitReaction { launch_velocity: launch_direction * knockback.get_launch_speed(&game_settings), + launch_angle, hitstun_frames: knockback.get_hitstun_frames(&game_settings), hitstop_frames: hitstop, kind: HitReactionKind::Flinch, diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index c026097..3ef6bec 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -7,8 +7,7 @@ use bevy::prelude::*; use crate::fighter::animation::{AnimKind, FighterAnimationFrame}; use crate::fighter::attack::{ - FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, - FighterStaleMoveQueue, + FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, FighterStaleMoveQueue, HitStop, }; use crate::fighter::attributes::FighterAttributes; use crate::fighter::baked_animation::BakedFighterAnimations; @@ -22,6 +21,7 @@ use crate::fighter::{ }; use crate::game_settings::GameSettings; use crate::input::FighterInput; +use crate::math::int::FGi32; use crate::scripting::FighterAttackScript; use crate::stage::line::StageCollision; @@ -391,7 +391,7 @@ pub fn state_interrupt_system( let new_state = if let Some(reaction) = pending_hit_reaction.take() { hitstop.remaining_frames = *reaction.hitstop_frames; - let hitstop_kind = match &*state { + let mut hitstop_kind = match &*state { FighterState::Wait(value) => { hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) } @@ -412,6 +412,26 @@ pub fn state_interrupt_system( } _ => hitstun::HitstopStateKind::Airborne, }; + + if let hitstun::HitstopStateKind::Grounded(_) = hitstop_kind { + let straight_up_angle = FGi32::lit("90.0"); + if reaction.hitstop_frames >= HitStop::new(game_settings.fighter_common.tumble_bounce_hitstop_threshold) { + hitstop_kind = hitstun::HitstopStateKind::Airborne; + // TODO: Use floor normal here + } else if reaction.launch_angle.abs_diff(straight_up_angle) < game_settings.fighter_common.attack_ground_to_airborne_transition_vertical_angle_threshold { + hitstop_kind = hitstun::HitstopStateKind::Airborne; + } else { + // straight down is 270 + let straight_down_angle = FGi32::lit("270.0"); + let meteor_threshold = game_settings.fighter_common.meteor_angle_threshold; + if (reaction.launch_angle - straight_down_angle).abs() <= meteor_threshold { + // TODO: Floor bounce + hitstop_kind = hitstun::HitstopStateKind::Airborne; + } + } + } + + info!("Entering hitstop kind: {:?}", hitstop_kind); Some(FighterState::Hitstun(HitstunState { hit_reaction: reaction, hitstop_kind, diff --git a/src/game_settings.rs b/src/game_settings.rs index 9f2f2dd..fb76538 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -9,12 +9,28 @@ use crate::{math::int::FGi32, menus::style::FGUiStyle}; #[reflect(opaque)] pub struct FighterSettingsCommon { pub walk_speed_ease: FGi32, + + /// Maximum allowed horizontal velocity pub ground_max_horizontal_velocity: FGi32, + + /// Ground friction applied when the fighter is above the walk speed pub ground_friction_over_walk_speed_multiplier: FGi32, pub knockback_stacking_window: u32, pub knockback_decay_per_frame: FGi32, + + /// Multiplier that translates knockback -> game unit velocity pub knockback_launch_velocity_multiplier: FGi32, pub hitstun_frames_per_unit_knockback: FGi32, + + /// If an attack's hitstop > this value it will cause a grounded fighter to either bounce (if a meteor/spike) + /// or to become airborne + pub tumble_bounce_hitstop_threshold: u32, + + /// Angle diff from straight down under which the attack will count as a meteor + pub meteor_angle_threshold: FGi32, + + /// Angle diff from straight up under which the attack will force an airborne state + pub attack_ground_to_airborne_transition_vertical_angle_threshold: FGi32, } #[derive(Reflect, Serialize, Deserialize, Clone)] #[reflect(opaque)]