Specify ground to air transitions and thresholds (#41)
CI / build-and-test (push) Successful in 1m10s

Reviewed-on: #41
Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #41.
This commit is contained in:
2026-09-17 17:57:57 +00:00
committed by EIREXE
parent 0977de8457
commit b9f1a52bbc
4 changed files with 45 additions and 4 deletions
+5 -1
View File
@@ -43,6 +43,7 @@ pub enum HitReactionKind {
#[derive(Debug, Hash, Clone)] #[derive(Debug, Hash, Clone)]
pub struct FighterHitReaction { pub struct FighterHitReaction {
pub launch_velocity: FGVec2, pub launch_velocity: FGVec2,
pub launch_angle: FGi32,
pub hitstop_frames: HitStop, pub hitstop_frames: HitStop,
pub hitstun_frames: u32, pub hitstun_frames: u32,
pub kind: HitReactionKind, pub kind: HitReactionKind,
@@ -62,10 +63,13 @@ pub struct FighterLastAttackDebug {
pub trajectory_points: Vec<Vec3>, pub trajectory_points: Vec<Vec3>,
} }
#[derive(Debug, Default, Clone, Deref, DerefMut, Hash)] #[derive(Debug, Default, Clone, Deref, DerefMut, Hash, PartialEq, PartialOrd)]
pub struct HitStop(u32); pub struct HitStop(u32);
impl HitStop { impl HitStop {
pub fn new(hitstop: u32) -> Self {
HitStop(hitstop)
}
pub fn calculate(attack_damage_stalled: FighterDamage, crouch_cancelling: bool) -> Self { 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 a = (attack_damage_stalled.0 / FGi32::lit("3.0") + FGi32::lit("4.0")).round_to_zero();
let crouch_cancelling_factor = if crouch_cancelling { let crouch_cancelling_factor = if crouch_cancelling {
+1
View File
@@ -104,6 +104,7 @@ pub fn resolve_fighter_hits(
attacker_slot, attacker_slot,
FighterHitReaction { FighterHitReaction {
launch_velocity: launch_direction * knockback.get_launch_speed(&game_settings), launch_velocity: launch_direction * knockback.get_launch_speed(&game_settings),
launch_angle,
hitstun_frames: knockback.get_hitstun_frames(&game_settings), hitstun_frames: knockback.get_hitstun_frames(&game_settings),
hitstop_frames: hitstop, hitstop_frames: hitstop,
kind: HitReactionKind::Flinch, kind: HitReactionKind::Flinch,
+23 -3
View File
@@ -7,8 +7,7 @@ use bevy::prelude::*;
use crate::fighter::animation::{AnimKind, FighterAnimationFrame}; use crate::fighter::animation::{AnimKind, FighterAnimationFrame};
use crate::fighter::attack::{ use crate::fighter::attack::{
FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, FighterStaleMoveQueue, HitStop,
FighterStaleMoveQueue,
}; };
use crate::fighter::attributes::FighterAttributes; use crate::fighter::attributes::FighterAttributes;
use crate::fighter::baked_animation::BakedFighterAnimations; use crate::fighter::baked_animation::BakedFighterAnimations;
@@ -22,6 +21,7 @@ use crate::fighter::{
}; };
use crate::game_settings::GameSettings; use crate::game_settings::GameSettings;
use crate::input::FighterInput; use crate::input::FighterInput;
use crate::math::int::FGi32;
use crate::scripting::FighterAttackScript; use crate::scripting::FighterAttackScript;
use crate::stage::line::StageCollision; 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() { let new_state = if let Some(reaction) = pending_hit_reaction.take() {
hitstop.remaining_frames = *reaction.hitstop_frames; hitstop.remaining_frames = *reaction.hitstop_frames;
let hitstop_kind = match &*state { let mut hitstop_kind = match &*state {
FighterState::Wait(value) => { FighterState::Wait(value) => {
hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) hitstun::HitstopStateKind::Grounded(value.grounded_common.clone())
} }
@@ -412,6 +412,26 @@ pub fn state_interrupt_system(
} }
_ => hitstun::HitstopStateKind::Airborne, _ => 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 { Some(FighterState::Hitstun(HitstunState {
hit_reaction: reaction, hit_reaction: reaction,
hitstop_kind, hitstop_kind,
+16
View File
@@ -9,12 +9,28 @@ use crate::{math::int::FGi32, menus::style::FGUiStyle};
#[reflect(opaque)] #[reflect(opaque)]
pub struct FighterSettingsCommon { pub struct FighterSettingsCommon {
pub walk_speed_ease: FGi32, pub walk_speed_ease: FGi32,
/// Maximum allowed horizontal velocity
pub ground_max_horizontal_velocity: FGi32, 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 ground_friction_over_walk_speed_multiplier: FGi32,
pub knockback_stacking_window: u32, pub knockback_stacking_window: u32,
pub knockback_decay_per_frame: FGi32, pub knockback_decay_per_frame: FGi32,
/// Multiplier that translates knockback -> game unit velocity
pub knockback_launch_velocity_multiplier: FGi32, pub knockback_launch_velocity_multiplier: FGi32,
pub hitstun_frames_per_unit_knockback: 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)] #[derive(Reflect, Serialize, Deserialize, Clone)]
#[reflect(opaque)] #[reflect(opaque)]