From a857a3c814f98ae7b2becd80a9b4314234b5d51b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=80lex=20Rom=C3=A0n?= Date: Wed, 26 Aug 2026 21:32:42 +0200 Subject: [PATCH] Finish implementing jumping and landing --- assets/fighters/test/test.fighter.ron | 18 +++-- src/fighter/animation.rs | 3 + src/fighter/attributes.rs | 7 ++ src/fighter/state/air.rs | 42 ++++++++++++ src/fighter/state/dash.rs | 52 ++++++++------ src/fighter/state/fall.rs | 3 +- src/fighter/state/jump.rs | 93 +++++++++++++++++++------ src/fighter/state/land.rs | 98 +++++++++++++++++++++++++++ src/fighter/state/mod.rs | 14 +--- src/fighter/state/run.rs | 4 ++ src/fighter/state/walk.rs | 3 + src/math/segment.rs | 3 + 12 files changed, 285 insertions(+), 55 deletions(-) create mode 100644 src/fighter/state/land.rs diff --git a/assets/fighters/test/test.fighter.ron b/assets/fighters/test/test.fighter.ron index b6df1c8..2ef23e0 100644 --- a/assets/fighters/test/test.fighter.ron +++ b/assets/fighters/test/test.fighter.ron @@ -15,17 +15,25 @@ full_jump_vertical_velocity: "1.6", jump_horizontal_velocity: "0.7", dash_initial_velocity: "1.4", - terminal_velocity: "2.8", - gravity: "0.23", + terminal_velocity: "1.3", + gravity: "0.064", jumpsquat_duration: 4, + landing_iasa: 4, + landing_duration: 30, + + max_air_horizontal_velocity: "1.35", + air_acceleration_base: "0.19", + air_acceleration_stick: "0.09", + air_friction: "0.05", ), animations: { Wait: "PlyPurin5K_Share_ACTION_Wait", Walk: "PlyPurin5K_Share_ACTION_WalkFast", Run: "PlyPurin5K_Share_ACTION_Run", Dash: "PlyPurin5K_Share_ACTION_Dash", - JumpSquat: "PlyPurin5K_Share_ACTION_Landing" - JumpForward: "PlyPurin5K_Share_ACTION_JumpF" - JumpBack: "PlyPurin5K_Share_ACTION_JumpB" + JumpSquat: "PlyPurin5K_Share_ACTION_Landing", + JumpForward: "PlyPurin5K_Share_ACTION_JumpF", + JumpBack: "PlyPurin5K_Share_ACTION_JumpB", + Landing: "PlyPurin5K_Share_ACTION_Landing", } ) diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index 8ecf825..e384790 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -86,4 +86,7 @@ pub enum AnimKind { Run, Dash, JumpSquat, + JumpForward, + JumpBack, + Landing, } diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index b3eed2e..188af05 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -24,10 +24,17 @@ pub struct FighterAttributes { pub short_hop_vertical_velocity: FGi32, pub full_jump_vertical_velocity: FGi32, pub jump_horizontal_velocity: FGi32, + pub landing_iasa: u32, + pub landing_duration: u32, + + pub air_acceleration_base: FGi32, + pub air_acceleration_stick: FGi32, + pub max_air_horizontal_velocity: FGi32, pub ground_friction: FGi32, pub terminal_velocity: FGi32, pub gravity: FGi32, + pub air_friction: FGi32, } impl FighterAttributes { diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index c08cf60..958277a 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -1,4 +1,5 @@ use bevy::prelude::*; +use fixed::traits::Fixed; use crate::fighter::*; @@ -7,11 +8,52 @@ pub fn integrate_gravity(velocity: &mut FighterVelocity, attributes: &FighterAtt velocity.y = velocity.y.max(-attributes.terminal_velocity); } +pub fn apply_air_drift(velocity: &mut FighterVelocity, attribs: &FighterAttributes, input: &FighterInput) { + if input.get_last_frame().movement.x.is_zero() { + if velocity.x.abs() < attribs.air_friction { + velocity.x = FGi32::ZERO; + } else { + let velocity_sign = velocity.x.signum(); + velocity.x += (-velocity_sign) * attribs.air_friction; + } + } else { + let target_velocity = input.get_last_frame().movement.x.signum() * attribs.max_air_horizontal_velocity; + let acceleration = input.get_last_frame().movement.x.signum() * attribs.air_acceleration_base; + let mut acceleration = acceleration + input.get_last_frame().movement.x * attribs.air_acceleration_stick; + let velocity_x = velocity.x; + if !(velocity_x * acceleration < FGi32::ZERO) { + if acceleration > FGi32::ZERO { + if velocity_x + acceleration > target_velocity { + acceleration = -attribs.air_friction; + if velocity_x + acceleration < target_velocity { + acceleration = target_velocity - velocity_x; + } + if velocity_x + acceleration > attribs.max_air_horizontal_velocity { + acceleration = attribs.max_air_horizontal_velocity - velocity_x; + } + } + } else if velocity_x + acceleration < target_velocity { + acceleration = attribs.air_friction; + if velocity_x + acceleration > target_velocity { + acceleration = target_velocity - velocity_x; + } + if velocity_x + acceleration < -attribs.max_air_horizontal_velocity { + acceleration = -attribs.max_air_horizontal_velocity - velocity_x; + } + } + } + + velocity.x += acceleration; + } +} + + pub fn apply_air_motion( velocity: &FighterVelocity, translation: &mut FighterTranslation, prev_translation: &mut FighterPreviousTranslation, ) { + // Vertical velocity should be 0 on the ground prev_translation.0 = translation.0; translation.0 += velocity.0; diff --git a/src/fighter/state/dash.rs b/src/fighter/state/dash.rs index 27e2469..3b305dc 100644 --- a/src/fighter/state/dash.rs +++ b/src/fighter/state/dash.rs @@ -43,25 +43,35 @@ impl DashState { impl FighterStateImpl for DashState { const NAME: &'static str = "Dash"; - fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { - if self.frames_in_dash > state_context.fighter_attribs.dash_duration { - run::check_input(state_context, &self.ground_common) - .or_else(|| walk::check_input(state_context, &self.ground_common)) - .or_else(|| wait::check_input(state_context, &self.ground_common)) - } else if self.frames_in_dash > state_context.fighter_attribs.dash_acceleration_duration { - // Dash finish shenanigans - run::check_input(state_context, &self.ground_common) - .or_else(|| walk::check_input(state_context, &self.ground_common)) - } else { - check_smash_input_with_dir(state_context) - .filter(|dir| *dir != self.direction) - .and_then(|dir| { - Some(FighterState::Dash(DashState::new( - dir, - self.ground_common.clone(), - ))) - }) + fn check_interrupt(&self, ctx: &FighterStateContext) -> Option { + let ground = &self.ground_common; + let attributes = ctx.fighter_attribs; + + // A reverse dash can interrupt before IASA + if let Some(direction) = + check_smash_input_with_dir(ctx).filter(|direction| *direction != self.direction) + { + return Some(FighterState::Dash(DashState::new( + direction, + ground.clone(), + ))); } + + if let Some(state) = ground::grounded_movement_common_interrupts(ctx, ground) { + return Some(state); + } + + if self.frames_in_dash > attributes.dash_duration { + return run::check_input(ctx, ground) + .or_else(|| walk::check_input(ctx, ground)) + .or_else(|| wait::check_input(ctx, ground)); + } + + if self.frames_in_dash > attributes.dash_acceleration_duration { + return run::check_input(ctx, ground); + } + + None } fn on_enter(&mut self, state_context: &mut FighterStateContext) { @@ -75,12 +85,16 @@ impl FighterStateImpl for DashState { state_context.animations.clips[&AnimKind::Dash], Duration::ZERO, ); + + state_context.input.clear_buffer(); } fn update(&mut self, state_context: &mut FighterStateContext) { self.frames_in_dash += 1; - info!("{}", self.frames_in_dash); + if self.frames_in_dash < state_context.fighter_attribs.dash_acceleration_duration { + + } let last_frame = state_context.input.get_last_frame(); let (accel, target_vel) = state_context diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index 5c38c9a..4aa3a39 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -2,6 +2,7 @@ use bevy::prelude::*; use super::{FighterStateContext, FighterStateImpl, ground, walk}; use crate::fighter::state::ground::GroundedStateCommon; +use crate::fighter::state::land::LandingState; use crate::fighter::state::wait::WaitState; use crate::fighter::state::{FighterState, air}; use crate::fighter::{FighterAttributes, FighterVelocity, collision, motion}; @@ -38,7 +39,7 @@ impl FighterStateImpl for FallState { let grounded_common = GroundedStateCommon { current_line_id: res.line_id, }; - Some(FighterState::Wait(WaitState { grounded_common })) + Some(FighterState::Land(LandingState { grounded_common, duration_counter: 0 })) } else { None } diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index 27fb9cb..9db6b94 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -3,13 +3,13 @@ use std::time::Duration; use bevy::prelude::*; use crate::{ - fighter::{animation::AnimKind, state::{ - FighterState, FighterStateContext, FighterStateImpl, - fall::FallState, - ground::{self, GroundedMotionResult, GroundedStateCommon}, - }}, input::FighterCommands, + fighter::{ + animation::AnimKind, collision, state::{ + FighterState, FighterStateContext, FighterStateImpl, air, fall::FallState, ground::{self, GroundedMotionResult, GroundedStateCommon}, land::LandingState, wait::WaitState, + }, + }, input::FighterCommands, }; - +#[derive(Debug, Hash, Clone, Copy)] pub enum JumpType { ShortHop, LongJump, @@ -17,7 +17,9 @@ pub enum JumpType { } #[derive(Debug, Clone, Hash)] -pub struct JumpState {} +pub struct JumpState { + jump_type: JumpType +} #[derive(Debug, Clone, Hash)] pub struct JumpSquatState { @@ -39,7 +41,9 @@ impl FighterStateImpl for JumpSquatState { fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { if self.duration_counter >= state_context.fighter_attribs.jumpsquat_duration { - Some(FighterState::Jump(JumpState {})) + info!("Jump {}", state_context.input.get_last_frame().jump); + let jump_type = if state_context.input.get_last_frame().jump {JumpType::LongJump} else {JumpType::ShortHop}; + Some(FighterState::Jump(JumpState { jump_type})) } else { None } @@ -82,17 +86,14 @@ impl FighterStateImpl for JumpSquatState { None } } - - fn play_animation( - &self, - transitions: &mut AnimationTransitions, - player: &mut AnimationPlayer, - anims: &crate::fighter::visual::FighterAnimations, - ) { - transitions.play(player, anims.clips[&AnimKind::JumpSquat], Duration::ZERO); + + fn on_enter(&mut self, state_context: &mut FighterStateContext) { + state_context.animation_transitions.play( + state_context.animation_player, + state_context.animations.clips[&AnimKind::JumpSquat], + Duration::ZERO, + ); } - - fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} } impl FighterStateImpl for JumpState { @@ -101,7 +102,61 @@ impl FighterStateImpl for JumpState { None } - fn update(&mut self, _state_context: &mut FighterStateContext) {} + fn update(&mut self, state_context: &mut FighterStateContext) { + info!("TRF1 {}", state_context.translation.0); + air::integrate_gravity(state_context.velocity, state_context.fighter_attribs); + air::apply_air_drift(state_context.velocity, state_context.fighter_attribs, state_context.input); + air::apply_air_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + info!("TRF12 {}", state_context.translation.0); + } + + fn on_enter(&mut self, state_context: &mut FighterStateContext) { + + let movement_stick_x = state_context.input.get_last_frame().movement.x; + if movement_stick_x.is_zero() || movement_stick_x.signum() == state_context.facing_direction.to_sign() { + state_context.animation_transitions.play( + state_context.animation_player, + state_context.animations.clips[&AnimKind::JumpForward], + Duration::ZERO, + ); + } else { + state_context.animation_transitions.play( + state_context.animation_player, + state_context.animations.clips[&AnimKind::JumpBack], + Duration::ZERO, + ); + } + + let vertical_vel = match self.jump_type { + JumpType::ShortHop => state_context.fighter_attribs.short_hop_vertical_velocity, + JumpType::LongJump => state_context.fighter_attribs.full_jump_vertical_velocity, + JumpType::DoubleJump => todo!(), + }; + + state_context.velocity.y = vertical_vel; + state_context.velocity.x += state_context.fighter_attribs.jump_horizontal_velocity * movement_stick_x; + state_context.input.clear_buffer(); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let Some(res) = collision::air_collide_with_stage(state_context) { + // Adjust translation + state_context.translation.0 = res.hit_position - state_context.ecb.get_bottom_point(); + let grounded_common = GroundedStateCommon { + current_line_id: res.line_id, + }; + Some(FighterState::Land(LandingState { grounded_common, duration_counter: 0 })) + } else { + None + } + } } pub fn check_input( diff --git a/src/fighter/state/land.rs b/src/fighter/state/land.rs new file mode 100644 index 0000000..816c0b9 --- /dev/null +++ b/src/fighter/state/land.rs @@ -0,0 +1,98 @@ +use std::time::Duration; + +use bevy::prelude::*; + +use crate::{ + fighter::{ + animation::AnimKind, + collision, + state::{ + FighterState, FighterStateContext, FighterStateImpl, air, dash, + fall::FallState, + ground::{self, GroundedMotionResult, GroundedStateCommon}, + wait::WaitState, + walk, + }, + }, + input::FighterCommands, +}; + +#[derive(Debug, Clone, Hash)] +pub struct LandingState { + pub grounded_common: GroundedStateCommon, + pub duration_counter: u32, +} + +impl LandingState { + pub fn create(grounded_common: GroundedStateCommon) -> Self { + Self { + grounded_common, + duration_counter: 0, + } + } +} + +impl FighterStateImpl for LandingState { + const NAME: &'static str = "Landing"; + + fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { + if self.duration_counter >= state_context.fighter_attribs.landing_duration { + Some(FighterState::Wait(WaitState { + grounded_common: self.grounded_common.clone(), + })) + } else if self.duration_counter >= state_context.fighter_attribs.landing_iasa { + dash::check_input(state_context, &self.grounded_common).or_else(|| { + ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) + .or_else(|| walk::check_input(state_context, &self.grounded_common)) + }) + } else { + None + } + } + + fn update(&mut self, state_context: &mut FighterStateContext) { + self.duration_counter += 1; + + let friction = + if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel { + state_context.fighter_attribs.ground_friction + * state_context + .game_settings + .fighter_common + .ground_friction_over_walk_speed_multiplier + } else { + state_context.fighter_attribs.ground_friction + }; + + let ground_velocity = state_context.velocity.x; + + state_context.velocity.x += ground::apply_grounded_friction(friction, ground_velocity); + + ground::apply_grounded_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let GroundedMotionResult::InAir = + ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false) + { + Some(FighterState::Fall(FallState)) + } else { + None + } + } + + fn on_enter(&mut self, state_context: &mut FighterStateContext) { + state_context.animation_transitions.play( + state_context.animation_player, + state_context.animations.clips[&AnimKind::Landing], + Duration::ZERO, + ); + } +} diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index e6cf6fa..a7f33a8 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -23,6 +23,7 @@ pub mod jump; pub mod run; pub mod wait; pub mod walk; +pub mod land; macro_rules! fighter_states { ($($variant:ident => $ty:ty),* $(,)?) => { @@ -37,9 +38,6 @@ macro_rules! fighter_states { match self { $(Self::$variant(s) => s.check_interrupt(ctx)),* } } - pub fn play_animation(&self, transitions: &mut AnimationTransitions, player: &mut AnimationPlayer, anims: &FighterAnimations) { - match self { $(Self::$variant(s) => s.play_animation(transitions, player, anims)),* } - } pub fn update(&mut self, ctx: &mut FighterStateContext) { match self { $(Self::$variant(s) => s.update(ctx)),* } } @@ -62,13 +60,6 @@ pub trait FighterStateImpl: Sized { const NAME: &'static str; fn check_interrupt(&self, state_context: &FighterStateContext) -> Option; - fn play_animation( - &self, - _transitions: &mut AnimationTransitions, - _player: &mut AnimationPlayer, - _anims: &FighterAnimations, - ) { - } fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} fn update(&mut self, _state_context: &mut FighterStateContext); @@ -103,7 +94,8 @@ fighter_states! { Run => run::RunState, Fall => fall::FallState, JumpSquat => jump::JumpSquatState, - Jump => jump::JumpState + Jump => jump::JumpState, + Land => land::LandingState } #[derive(QueryData)] diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs index bea19fe..178ea25 100644 --- a/src/fighter/state/run.rs +++ b/src/fighter/state/run.rs @@ -20,6 +20,10 @@ pub struct RunState { impl FighterStateImpl for RunState { const NAME: &'static str = "Run"; fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { + if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) { + return Some(state); + } + let input_frame = state_context.input.get_last_frame(); if input_frame.movement.x.abs() >= state_context.game_settings.input_common.run_stick_threshold diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index 17dfea8..8d0f592 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -19,6 +19,9 @@ pub struct WalkState { impl FighterStateImpl for WalkState { const NAME: &'static str = "Walk"; fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { + if let Some(state) = ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) { + return Some(state); + } wait::check_input(state_context, &self.grounded_common) .or_else(|| dash::check_input(state_context, &self.grounded_common)) } diff --git a/src/math/segment.rs b/src/math/segment.rs index 6cf2b25..bd9d643 100644 --- a/src/math/segment.rs +++ b/src/math/segment.rs @@ -134,6 +134,9 @@ impl FGSegment2d { let t = (s2_x * (p0_y - p2_y) - s2_y * (p0_x - p2_x)) / denom; if s >= FGWide::ZERO && s <= FGWide::ONE && t >= FGWide::ZERO && t <= FGWide::ONE { + if self.left_normal().dot(rhs.point2() - rhs.point1()) >= FGi32::ZERO { + return None; + } Some(FGVec2::new( FGi32::from_num(p0_x + t * s1_x), FGi32::from_num(p0_y + t * s1_y),