From f22f5c5dbcf956343637169d3627891684617eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sat, 19 Sep 2026 00:07:49 +0200 Subject: [PATCH] Implement fast falling --- assets/fighters/test/test.fighter.ron | 3 +- src/fighter/attributes.rs | 1 + src/fighter/debug.rs | 2 +- src/fighter/mod.rs | 4 +-- src/fighter/state/air.rs | 47 ++++++++++++++++++++++++--- src/fighter/state/air_dodge.rs | 12 +++---- src/fighter/state/fall.rs | 26 ++++++++------- src/fighter/state/ground.rs | 4 +-- src/fighter/state/hitstun.rs | 35 ++++++++++++-------- src/fighter/state/jump.rs | 19 +++++------ src/fighter/state/mod.rs | 4 +-- src/input/buffer.rs | 21 ++++++++++++ 12 files changed, 123 insertions(+), 55 deletions(-) diff --git a/assets/fighters/test/test.fighter.ron b/assets/fighters/test/test.fighter.ron index f3cbd17..52faa91 100644 --- a/assets/fighters/test/test.fighter.ron +++ b/assets/fighters/test/test.fighter.ron @@ -22,7 +22,8 @@ air_jump_horizontal_velocity: "0.96", air_jump_multiplier: "1.0", dash_initial_velocity: "1.4", - terminal_velocity: "1.6", + terminal_velocity: "3.0", + fast_fall_speed: "3.5", gravity: "0.084", weight: "100.0", jumpsquat_duration: 5, diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index 7c7caeb..1c6c8a3 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -37,6 +37,7 @@ pub struct FighterAttributes { pub ground_friction: FGi32, pub terminal_velocity: FGi32, pub gravity: FGi32, + pub fast_fall_speed: FGi32, pub weight: FGi32, pub air_friction: FGi32, diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index 2713d50..7686b5a 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -412,7 +412,7 @@ pub fn attack_debug_gather( points.reserve(hit.hitstun_frames as usize); for _ in 1..hit.hitstun_frames { - air::integrate_gravity(&mut fighter_vel, &manifest.attributes); + air::integrate_gravity(&mut fighter_vel, &manifest.attributes, false); let vel = knockback_vel.length(); let new_vel = vel diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 0a73c85..24d1317 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -38,7 +38,7 @@ use crate::{ AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes, FighterStaleMoveQueue, - }, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, manifest::FighterManifest, state::{FighterState, fall::FallState}, visual::FighterAnimations, + }, baked_animation::{FighterBoneIndexMap, FighterBoneMatrices}, manifest::FighterManifest, state::{FighterState, air::AirborneStateCommon, fall::FallState}, visual::FighterAnimations, }, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySet, scripting::FighterAttackScript, stage::line::StageCollision, }; use state::state_interrupt_system; @@ -299,7 +299,7 @@ pub fn spawn_fighter( visual_root, animations, attack_scripts, - FighterState::Fall(FallState), + FighterState::Fall(FallState::new(AirborneStateCommon::default())), animation::FighterAnimationFrame::new(animation::AnimKind::Wait, true), ( baked_animation::FighterBoneMatrices::default(), diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index a049269..f4ab6da 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -2,10 +2,44 @@ use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::land::{LandingState, LandingType}; use crate::fighter::state::{CollisionContext, FighterState}; use crate::fighter::*; +use crate::input::FighterCommands; -pub fn integrate_gravity(velocity: &mut FighterVelocity, attributes: &FighterAttributes) { +#[derive(Default, Debug, Clone, Copy, Hash)] +pub struct AirborneStateCommon { + pub is_fast_falling: bool, +} + +pub fn process_fast_fall( + airborne_state_common: &mut AirborneStateCommon, + input: &mut FighterInput, + velocity: &mut FighterVelocity, + attributes: &FighterAttributes, +) { + if velocity.y.is_negative() { + if input.has_command(FighterCommands::SmashMoveDown) { + airborne_state_common.is_fast_falling = true; + } + input.clear_command(FighterCommands::SmashMoveDown); + } + + if airborne_state_common.is_fast_falling { + velocity.y = -attributes.fast_fall_speed; + } +} + +pub fn integrate_gravity( + velocity: &mut FighterVelocity, + attributes: &FighterAttributes, + fast_fall: bool +) { velocity.y -= attributes.gravity; - velocity.y = velocity.y.max(-attributes.terminal_velocity); + + let terminal_vel = if fast_fall { + -attributes.fast_fall_speed + } else { + -attributes.terminal_velocity + }; + velocity.y = velocity.y.max(terminal_vel); } pub fn apply_air_drift( @@ -56,8 +90,12 @@ pub fn apply_air_motion( translation.0 += velocity.0; } -pub fn update_standard_motion(ctx: &mut crate::fighter::state::UpdateContext) { - integrate_gravity(ctx.velocity, ctx.fighter_attributes); +pub fn update_standard_motion( + ctx: &mut crate::fighter::state::UpdateContext, + airborne_common: &mut AirborneStateCommon, +) { + process_fast_fall(airborne_common, ctx.input, &mut ctx.velocity, ctx.fighter_attributes); + integrate_gravity(ctx.velocity, ctx.fighter_attributes, airborne_common.is_fast_falling); apply_air_drift(ctx.velocity, ctx.fighter_attributes, ctx.input); apply_air_motion(ctx.velocity, ctx.translation, ctx.prev_translation); } @@ -110,6 +148,7 @@ mod tests { air_drift_max: FGi32::lit("1.2"), ground_friction: FGi32::ZERO, terminal_velocity: FGi32::ZERO, + fast_fall_speed: FGi32::ZERO, gravity: FGi32::ZERO, weight: FGi32::lit("1"), air_friction: FGi32::lit("0.001"), diff --git a/src/fighter/state/air_dodge.rs b/src/fighter/state/air_dodge.rs index 787c7d7..9517ce7 100644 --- a/src/fighter/state/air_dodge.rs +++ b/src/fighter/state/air_dodge.rs @@ -1,13 +1,9 @@ use crate::{ fighter::{ - animation::AnimKind, - state::{ - CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, - UpdateContext, air, fall::FallState, land::LandingType, + animation::AnimKind, state::{ + CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, land::LandingType, }, - }, - input, - math::vec::FGVec2, + }, input, math::vec::FGVec2, }; #[derive(Debug, Clone, Hash)] @@ -20,7 +16,7 @@ impl FighterStateImpl for AirDodgeState { fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.is_current_animation_finished() { - Some(FighterState::Fall(FallState)) + Some(FighterState::Fall(FallState::new(AirborneStateCommon::default()))) } else { None } diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index 3249b43..ea55fe8 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -1,12 +1,24 @@ use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext}; use crate::fighter::animation::AnimKind; +use crate::fighter::state::air::AirborneStateCommon; use crate::fighter::state::{FighterState, air, jump}; -#[derive(Debug, Clone, Copy, Hash)] -pub struct FallState; +#[derive(Debug, Clone, Copy, Hash, Default)] +pub struct FallState { + airborne_state_common: AirborneStateCommon +} + +impl FallState { + pub fn new(airborne_state_common: AirborneStateCommon) -> Self { + Self { + airborne_state_common + } + } +} impl FighterStateImpl for FallState { const NAME: &'static str = "Fall"; + fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { jump::check_input_double_jump(state_context) } @@ -16,7 +28,7 @@ impl FighterStateImpl for FallState { } fn update(&mut self, state_context: &mut UpdateContext) { - air::update_standard_motion(state_context); + air::update_standard_motion(state_context, &mut self.airborne_state_common); } fn check_collision_interrupt( @@ -36,11 +48,3 @@ impl FighterStateImpl for FallState { } } -pub fn check_input(state_context: &InterruptContext) -> Option { - let input_frame = state_context.input.get_last_frame(); - - if input_frame.movement.x.abs() < state_context.game_settings.input_common.stick_deadzone { - return Some(FighterState::Fall(FallState)); - } - None -} diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 597548e..880f40b 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -3,7 +3,7 @@ //! bit-for-bit deterministic for rollback. use crate::fighter::state::{ - CollisionContext, FighterState, InterruptContext, dash, fall, ground_attack, turn, wait, walk, + CollisionContext, FighterState, InterruptContext, air, dash, fall, ground_attack, turn, wait, walk, }; use crate::fighter::{ FighterAttributes, FighterPreviousTranslation, FighterTranslation, FighterVelocity, @@ -233,7 +233,7 @@ pub fn collision_interrupt( ) -> Option { match collide_with_stage_grounded(ctx, common, can_walk_off) { GroundedMotionResult::InGround => None, - GroundedMotionResult::InAir => Some(FighterState::Fall(fall::FallState)), + GroundedMotionResult::InAir => Some(FighterState::Fall(fall::FallState::new(air::AirborneStateCommon::default()))), } } diff --git a/src/fighter/state/hitstun.rs b/src/fighter/state/hitstun.rs index a10e795..05234f3 100644 --- a/src/fighter/state/hitstun.rs +++ b/src/fighter/state/hitstun.rs @@ -1,14 +1,18 @@ use crate::{ fighter::{ - animation::AnimKind, attack::{self, FighterHitReaction}, state::{ + animation::AnimKind, + attack::{self, FighterHitReaction}, + state::{ CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, - InterruptContext, UpdateContext, air, + InterruptContext, UpdateContext, + air::{self, AirborneStateCommon}, fall::FallState, ground::{GroundedStateCommon, collision_interrupt}, land::LandingType, wait::WaitState, }, - }, math::int::FGi32, + }, + math::int::FGi32, }; #[derive(Clone, Debug, Hash)] @@ -21,7 +25,7 @@ pub enum HitstopStateKind { pub struct HitstunState { pub hit_reaction: FighterHitReaction, pub hitstop_kind: HitstopStateKind, - knockback_applied: bool + knockback_applied: bool, } impl HitstunState { @@ -29,7 +33,7 @@ impl HitstunState { Self { hit_reaction, hitstop_kind, - knockback_applied: false + knockback_applied: false, } } } @@ -59,7 +63,9 @@ impl FighterStateImpl for HitstunState { grounded_common: grounded_state_common.clone(), })) } - HitstopStateKind::Airborne => Some(FighterState::Fall(FallState)), + HitstopStateKind::Airborne => { + Some(FighterState::Fall(FallState::new(AirborneStateCommon::default()))) + } } } else { None @@ -69,16 +75,21 @@ impl FighterStateImpl for HitstunState { fn update(&mut self, ctx: &mut UpdateContext) { if !self.knockback_applied { let launch_direction = attack::directional_influence::apply_directional_influence( - ctx.input.get_last_frame().movement, + ctx.input.get_last_frame().movement, self.hit_reaction.launch_angle, - ctx.game_settings.fighter_common.di_max_angle + ctx.game_settings.fighter_common.di_max_angle, ); ctx.knockback_velocity.0 = launch_direction * self.hit_reaction.launch_velocity; self.knockback_applied = true; } - air::integrate_gravity(ctx.velocity, ctx.fighter_attributes); + match self.hitstop_kind { + HitstopStateKind::Grounded(_) => {} + HitstopStateKind::Airborne => { + air::integrate_gravity(ctx.velocity, ctx.fighter_attributes, false); + } + } let speed = ctx.knockback_velocity.length(); let next_speed = speed @@ -113,10 +124,8 @@ impl FighterStateImpl for HitstunState { } } } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { match self.hitstop_kind { HitstopStateKind::Grounded(_) => true, HitstopStateKind::Airborne => false, diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index ec6f7f1..7f43435 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -2,16 +2,10 @@ use bevy::prelude::*; use crate::{ fighter::{ - animation::AnimKind, - state::{ - CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, - UpdateContext, air, - air_dodge::{self, AirDodgeState}, - ground::{self, GroundedStateCommon}, + animation::AnimKind, state::{ + CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_dodge::{self, AirDodgeState}, ground::{self, GroundedStateCommon}, }, - }, - input::FighterCommands, - math::{int::FGi32, vec::FGVec2}, + }, input::FighterCommands, math::{int::FGi32, vec::FGVec2}, }; #[derive(Debug, Hash, Clone, Copy)] pub enum JumpType { @@ -23,6 +17,7 @@ pub enum JumpType { #[derive(Debug, Clone, Hash)] pub struct JumpState { jump_type: JumpType, + airborne_state_common: AirborneStateCommon, } #[derive(Debug, Clone, Hash)] @@ -74,7 +69,7 @@ impl FighterStateImpl for JumpSquatState { } else { JumpType::ShortHop }; - Some(FighterState::Jump(JumpState { jump_type })) + Some(FighterState::Jump(JumpState { jump_type, airborne_state_common: AirborneStateCommon::default() })) } else { None } @@ -110,6 +105,7 @@ impl FighterStateImpl for JumpState { if state_context.input.has_command(FighterCommands::Jump) { Some(FighterState::Jump(JumpState { jump_type: JumpType::DoubleJump, + airborne_state_common: AirborneStateCommon::default() })) } else { air_dodge::check_input(state_context) @@ -117,7 +113,7 @@ impl FighterStateImpl for JumpState { } fn update(&mut self, state_context: &mut UpdateContext) { - air::update_standard_motion(state_context); + air::update_standard_motion(state_context, &mut self.airborne_state_common); } fn on_enter(&mut self, state_context: &mut EnterContext) { @@ -205,6 +201,7 @@ pub fn check_input_double_jump(state_context: &InterruptContext) -> Option { pub struct UpdateContext<'a> { pub translation: &'a mut FighterTranslation, pub prev_translation: &'a mut FighterPreviousTranslation, - pub input: &'a FighterInput, + pub input: &'a mut FighterInput, pub hitboxes: &'a mut FighterHitboxes, pub velocity: &'a mut FighterVelocity, pub knockback_velocity: &'a mut FighterKnockbackVelocity, @@ -289,7 +289,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { UpdateContext { translation: &mut self.translation, prev_translation: &mut self.prev_translation, - input: &self.input, + input: &mut self.input, hitboxes: &mut self.hitboxes, velocity: &mut self.velocity, knockback_velocity: &mut self.knockback_velocity, diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 67d6b9e..2e500cb 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -20,6 +20,8 @@ pub enum FighterCommands { Jump, Shield, Attack, + SmashMoveUp, + SmashMoveDown, SmashMoveLeft, SmashMoveRight, } @@ -60,6 +62,8 @@ impl FighterAttackCommandType { pub struct FighterCommandLifetimes { smash_move_left: u8, smash_move_right: u8, + smash_move_down: u8, + smash_move_up: u8, jump: u8, shield: u8, attack: u8, @@ -72,6 +76,8 @@ impl Index for FighterCommandLifetimes { match command { FighterCommands::SmashMoveLeft => &self.smash_move_left, FighterCommands::SmashMoveRight => &self.smash_move_right, + FighterCommands::SmashMoveDown => &self.smash_move_down, + FighterCommands::SmashMoveUp => &self.smash_move_up, FighterCommands::Jump => &self.jump, FighterCommands::Shield => &self.shield, FighterCommands::Attack => &self.attack, @@ -84,6 +90,8 @@ impl IndexMut for FighterCommandLifetimes { match command { FighterCommands::SmashMoveLeft => &mut self.smash_move_left, FighterCommands::SmashMoveRight => &mut self.smash_move_right, + FighterCommands::SmashMoveDown => &mut self.smash_move_down, + FighterCommands::SmashMoveUp => &mut self.smash_move_up, FighterCommands::Jump => &mut self.jump, FighterCommands::Shield => &mut self.shield, FighterCommands::Attack => &mut self.attack, @@ -221,6 +229,19 @@ pub fn postprocess_input( let mut x_flick_detected = x_abs >= axis_threshold && input.frames_in_x_flick_deadzone <= frame_threshold; + if y_flick_detected { + input.clear_command(FighterCommands::SmashMoveUp); + input.clear_command(FighterCommands::SmashMoveDown); + + let command_to_set = if input.current_frame.movement.y > 0.0 { + FighterCommands::SmashMoveUp + } else { + FighterCommands::SmashMoveDown + }; + + input.set_lifetime(command_to_set, game_settings.input_common.input_buffer_size); + } + // if both flicks are detected, only one wins if x_flick_detected && y_flick_detected { -- 2.54.0