From d5214616256cf8d3bca650909c739928be1c7dbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Fri, 25 Sep 2026 10:45:27 +0000 Subject: [PATCH] Add authoritative fighter surface state (#60) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - add a rollback-tracked `FighterSurfaceState` as the single source of truth for stage contact - move active stage line ownership out of individual grounded fighter states - update landing, airborne motion, ledge departure, hit reactions, and debug save states to maintain surface contact - add tests for contact lifecycle, connected-segment traversal, and ledge departure Reviewed-on: https://git.eirexe.moe/EIREXE/shinespark/pulls/60 Co-authored-by: Álex Román Núñez --- src/debug_tools/save_state.rs | 28 +++--- src/fighter/mod.rs | 37 +++++--- src/fighter/motion.rs | 27 ++++-- src/fighter/state/air.rs | 34 ++++--- src/fighter/state/air_dodge.rs | 24 +++-- src/fighter/state/dash.rs | 41 +++----- src/fighter/state/ground.rs | 144 +++++++++++++++++++++++------ src/fighter/state/ground_attack.rs | 21 ++--- src/fighter/state/hitstun.rs | 29 +++--- src/fighter/state/jump.rs | 47 +++++----- src/fighter/state/land.rs | 26 ++---- src/fighter/state/mod.rs | 66 ++++++------- src/fighter/state/run.rs | 29 ++---- src/fighter/state/turn.rs | 33 +++---- src/fighter/state/wait.rs | 33 ++----- src/fighter/state/walk.rs | 29 ++---- src/fighter/surface.rs | 62 +++++++++++++ src/stage/line.rs | 2 +- 18 files changed, 411 insertions(+), 301 deletions(-) create mode 100644 src/fighter/surface.rs diff --git a/src/debug_tools/save_state.rs b/src/debug_tools/save_state.rs index 2809915..9e84422 100644 --- a/src/debug_tools/save_state.rs +++ b/src/debug_tools/save_state.rs @@ -1,12 +1,22 @@ use bevy::prelude::*; use crate::{ - camera::MatchCamera, fighter::{ - FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded, animation::FighterAnimationFrame, attack::{ + camera::MatchCamera, + fighter::{ + FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, + FighterPreviousFacingDirection, FighterPreviousTranslation, FighterSurfaceState, + FighterTranslation, FighterVelocity, + animation::FighterAnimationFrame, + attack::{ FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterPendingHits, FighterSolvedHurtboxes, FighterStaleMoveQueue, - }, baked_animation::FighterBoneMatrices, state::FighterState, - }, input::FighterInput, player::Player, stage::line::StageCollision, + }, + baked_animation::FighterBoneMatrices, + state::FighterState, + }, + input::FighterInput, + player::Player, + stage::line::StageCollision, }; #[derive(Resource, Default)] @@ -44,7 +54,7 @@ struct FighterCheckpoint { hitboxes: FighterHitboxes, solved_hurtboxes: FighterSolvedHurtboxes, bone_matrices: FighterBoneMatrices, - grounded: bool, + surface: FighterSurfaceState, } pub(super) fn save_or_restore(world: &mut World) { @@ -143,11 +153,7 @@ pub(super) fn save_or_restore(world: &mut World) { entity.insert(saved.hitboxes.clone()); entity.insert(saved.solved_hurtboxes.clone()); entity.insert(saved.bone_matrices.clone()); - if saved.grounded { - entity.insert(Grounded); - } else { - entity.remove::(); - } + entity.insert(saved.surface); } world.insert_resource(checkpoint.pending_hits.clone()); world.insert_resource(checkpoint.stage_collision); @@ -181,7 +187,7 @@ fn capture_fighter(world: &World, entity: Entity) -> Option { hitboxes: fighter.get::()?.clone(), solved_hurtboxes: fighter.get::()?.clone(), bone_matrices: fighter.get::()?.clone(), - grounded: fighter.contains::(), + surface: *fighter.get::()?, }) } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 9eb70f5..f178141 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -18,6 +18,7 @@ pub mod hurtbox; pub mod manifest; pub mod motion; pub mod state; +pub mod surface; pub mod visual; // The fighter components are re-exported so the rest of the crate can say @@ -28,18 +29,29 @@ pub use attributes::FighterAttributes; pub use ecb::FighterECB; pub use manifest::FighterId; pub use motion::{ - FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, - FighterVelocity, Grounded, + FighterPreviousFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; +pub use surface::FighterSurfaceState; use crate::{ - AppState, fighter::{ + AppState, + fighter::{ attack::{ AttackKind, FighterAttackPlugin, FighterAttackScriptAssets, FighterDamage, FighterKnockbackVelocity, FighterPendingHitReaction, FighterSolvedHurtboxes, FighterStaleMoveQueue, - }, 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, + }, + 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; @@ -58,14 +70,14 @@ pub enum FighterFacingDirection { pub enum FighterHitstopShakeKind { #[default] Horizontal, - Vertical + Vertical, } #[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect, Default)] pub enum FighterHitstopShakeRole { #[default] Attacker, - Receiver + Receiver, } #[derive(Component, Default, Copy, Clone, Hash)] @@ -73,7 +85,7 @@ pub struct FighterHitstopShake { pub remaining_frames: u32, pub total_frames: u32, pub kind: FighterHitstopShakeKind, - pub role: FighterHitstopShakeRole + pub role: FighterHitstopShakeRole, } #[derive(Component, Default, Copy, Clone, Hash)] @@ -133,7 +145,7 @@ pub struct FighterVisual; FighterPendingHitReaction, FighterHitstop, FighterKnockbackVelocity, - FighterTranslation, + FighterTranslation )] pub struct Fighter { pub manifest: Handle, @@ -200,8 +212,8 @@ impl Plugin for FighterPlugin { .checksum_component_with_hash::() .rollback_component_with_copy::() .checksum_component_with_hash::() - .rollback_component_with_copy::() - .checksum_component_with_hash::() + .rollback_component_with_copy::() + .checksum_component_with_hash::() .rollback_component_with_copy::() .rollback_component_with_copy::() .checksum_component_with_hash::() @@ -259,7 +271,7 @@ pub struct FighterActiveAttack { pub active_hitboxes: Vec, pub solved_hitboxes: Vec, pub hit_fighters: Vec, - pub stalled: bool + pub stalled: bool, } #[derive(Component, Clone, Debug, Default, Hash)] @@ -284,6 +296,7 @@ pub fn spawn_fighter( Fighter { manifest: manifest }, FighterFacingDirection::Right, FighterPreviousFacingDirection(FighterFacingDirection::Right), + FighterSurfaceState::Airborne, Transform::from_scale(Vec3::splat(model_scale.to_num())), ( FighterECB { diff --git a/src/fighter/motion.rs b/src/fighter/motion.rs index f1a0657..1a1ef35 100644 --- a/src/fighter/motion.rs +++ b/src/fighter/motion.rs @@ -5,7 +5,7 @@ use bevy::prelude::*; use bevy_ggrs::GgrsFrameTiming; use crate::debug_tools::PresentationMotionMode; -use crate::fighter::{FighterHitstopShake}; +use crate::fighter::FighterHitstopShake; use crate::game_settings::GameSettings; use crate::{ fighter::{FighterECB, FighterFacingDirection, baked_animation::FixedMat4}, @@ -15,9 +15,6 @@ use crate::{ #[derive(Component, Clone, Copy, Hash)] pub struct FighterPreviousFacingDirection(pub FighterFacingDirection); -#[derive(Component, Clone, Copy, Hash)] -pub struct Grounded; - #[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy, Hash)] pub struct FighterVelocity(pub FGVec2); @@ -82,19 +79,29 @@ pub fn apply_fighter_translation_to_visuals( 0.0, ); - if hitstop_shake.remaining_frames > 0 && hitstop_shake.total_frames > 0 && !game_settings.fighter_common.visual_shake_table.is_empty() { + if hitstop_shake.remaining_frames > 0 + && hitstop_shake.total_frames > 0 + && !game_settings.fighter_common.visual_shake_table.is_empty() + { let total = hitstop_shake.total_frames as f32; let curr = hitstop_shake.remaining_frames as f32; let elapsed = 1.0 - curr / total; - - let access_index = (hitstop_shake.remaining_frames as usize) % game_settings.fighter_common.visual_shake_table.len(); + let access_index = (hitstop_shake.remaining_frames as usize) + % game_settings.fighter_common.visual_shake_table.len(); let shake_multiplier = match hitstop_shake.role { - super::FighterHitstopShakeRole::Attacker => game_settings.fighter_common.attacker_visual_shake_multiplier, + super::FighterHitstopShakeRole::Attacker => { + game_settings + .fighter_common + .attacker_visual_shake_multiplier + } super::FighterHitstopShakeRole::Receiver => 1.0, }; - let shake_amount: f32 = 1.0 - EaseFunction::QuinticIn.sample(elapsed).unwrap_or_default(); - let shake_amount = game_settings.fighter_common.visual_shake_table[access_index] * shake_amount * shake_multiplier; + let shake_amount: f32 = + 1.0 - EaseFunction::QuinticIn.sample(elapsed).unwrap_or_default(); + let shake_amount = game_settings.fighter_common.visual_shake_table[access_index] + * shake_amount + * shake_multiplier; let shake_offset = match hitstop_shake.kind { super::FighterHitstopShakeKind::Horizontal => Vec3::new(shake_amount, 0.0, 0.0), diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index f4ab6da..c87544c 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -1,4 +1,3 @@ -use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::land::{LandingState, LandingType}; use crate::fighter::state::{CollisionContext, FighterState}; use crate::fighter::*; @@ -30,7 +29,7 @@ pub fn process_fast_fall( pub fn integrate_gravity( velocity: &mut FighterVelocity, attributes: &FighterAttributes, - fast_fall: bool + fast_fall: bool, ) { velocity.y -= attributes.gravity; @@ -84,7 +83,9 @@ pub fn apply_air_motion( velocity: &FighterVelocity, translation: &mut FighterTranslation, prev_translation: &mut FighterPreviousTranslation, + surface: &mut FighterSurfaceState, ) { + surface.leave_ground(); // Vertical velocity should be 0 on the ground prev_translation.0 = translation.0; translation.0 += velocity.0; @@ -94,10 +95,24 @@ 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); + 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); + apply_air_motion( + ctx.velocity, + ctx.translation, + ctx.prev_translation, + ctx.surface, + ); } pub fn collision_interrupt( @@ -106,13 +121,8 @@ pub fn collision_interrupt( ) -> Option { let res = ctx.air_collide_with_stage()?; ctx.translation.0 = res.hit_position - ctx.ecb.get_bottom_point(); - let grounded_common = GroundedStateCommon { - current_line_id: res.line_id, - }; - Some(FighterState::Land(LandingState::create( - grounded_common, - landing_type, - ))) + ctx.surface.land(res.line_id); + Some(FighterState::Land(LandingState::create(landing_type))) } #[cfg(test)] diff --git a/src/fighter/state/air_dodge.rs b/src/fighter/state/air_dodge.rs index 9517ce7..2ee61c7 100644 --- a/src/fighter/state/air_dodge.rs +++ b/src/fighter/state/air_dodge.rs @@ -1,9 +1,16 @@ use crate::{ fighter::{ - animation::AnimKind, state::{ - CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, 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)] @@ -16,7 +23,9 @@ impl FighterStateImpl for AirDodgeState { fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.is_current_animation_finished() { - Some(FighterState::Fall(FallState::new(AirborneStateCommon::default()))) + Some(FighterState::Fall(FallState::new( + AirborneStateCommon::default(), + ))) } else { None } @@ -29,6 +38,7 @@ impl FighterStateImpl for AirDodgeState { state_context.velocity, state_context.translation, state_context.prev_translation, + state_context.surface, ); } @@ -45,10 +55,8 @@ impl FighterStateImpl for AirDodgeState { state_context.velocity.0 = self.direction * state_context.fighter_attributes.air_dodge_velocity; } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { false } } diff --git a/src/fighter/state/dash.rs b/src/fighter/state/dash.rs index 6ea001a..a2858de 100644 --- a/src/fighter/state/dash.rs +++ b/src/fighter/state/dash.rs @@ -5,52 +5,43 @@ use super::{ use crate::fighter::FighterFacingDirection; use crate::fighter::animation::AnimKind; use crate::fighter::state::FighterState; -use crate::fighter::state::ground::GroundedStateCommon; use crate::input::FighterCommands::{SmashMoveLeft, SmashMoveRight}; #[derive(Debug, Clone, Hash)] pub struct DashState { direction: FighterFacingDirection, - pub ground_common: GroundedStateCommon, } impl DashState { - pub fn new(direction: FighterFacingDirection, ground_common: GroundedStateCommon) -> Self { - Self { - direction, - ground_common, - } + pub fn new(direction: FighterFacingDirection) -> Self { + Self { direction } } } impl FighterStateImpl for DashState { const NAME: &'static str = "Dash"; fn check_interrupt(&self, ctx: &mut InterruptContext) -> Option { - let ground = &self.ground_common; let attributes = ctx.fighter_attributes; // 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(), - ))); + return Some(FighterState::Dash(DashState::new(direction))); } - if let Some(state) = ground::grounded_movement_common_interrupts(ctx, ground) { + if let Some(state) = ground::grounded_movement_common_interrupts(ctx) { return Some(state); } if ctx.is_current_animation_finished() { - return run::check_input(ctx, ground) - .or_else(|| walk::check_input(ctx, ground)) - .or_else(|| wait::check_input(ctx, ground)); + return run::check_input(ctx) + .or_else(|| walk::check_input(ctx)) + .or_else(|| wait::check_input(ctx)); } if ctx.get_current_animation_frame() > attributes.dash_acceleration_duration { - return run::check_input(ctx, ground); + return run::check_input(ctx); } None @@ -106,12 +97,10 @@ impl FighterStateImpl for DashState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.ground_common, true) + ground::collision_interrupt(state_context, true) } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } @@ -126,10 +115,6 @@ fn check_smash_input_with_dir(state_context: &InterruptContext) -> Option Option { - check_smash_input_with_dir(state_context) - .map(|dir| FighterState::Dash(DashState::new(dir, ground_common.clone()))) +pub fn check_input(state_context: &InterruptContext) -> Option { + check_smash_input_with_dir(state_context).map(|dir| FighterState::Dash(DashState::new(dir))) } diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 880f40b..181a297 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -3,7 +3,8 @@ //! bit-for-bit deterministic for rollback. use crate::fighter::state::{ - CollisionContext, FighterState, InterruptContext, air, 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, @@ -13,13 +14,6 @@ use crate::math::int::{FGWide, FGi32}; use crate::math::vec::FGVec2; use crate::stage::line::{StageLineID, StagePoly, StagePolyLineSegment, StagePolyLineSegmentType}; -use bevy::prelude::*; - -#[derive(Reflect, Hash, Clone, Debug)] -pub struct GroundedStateCommon { - pub current_line_id: StageLineID, -} - pub fn apply_grounded_friction(friction: FGi32, ground_vel: FGi32) -> FGi32 { if friction.abs() > ground_vel.abs() { -ground_vel @@ -181,22 +175,23 @@ pub fn update_friction_motion(ctx: &mut crate::fighter::state::UpdateContext) { pub fn collide_with_stage_grounded( state_context: &mut CollisionContext, - ground_common: &mut GroundedStateCommon, can_walk_off: bool, ) -> GroundedMotionResult { let translation = &mut state_context.translation; let ecb = state_context.ecb; let stage_collision = state_context.stage_collision; - let stage_poly = stage_collision - .stage_polys - .get(ground_common.current_line_id.polygon); + let Some(current_line_id) = state_context.surface.line_id() else { + return GroundedMotionResult::InAir; + }; + + let stage_poly = stage_collision.stage_polys.get(current_line_id.polygon); if let Some(stage_poly) = stage_poly { let ecb_bottom_pos = translation.0 + ecb.get_bottom_point(); match resolve_grounded_position( stage_poly, - ground_common.current_line_id.segment, + current_line_id.segment, ecb_bottom_pos, can_walk_off, ) { @@ -216,30 +211,37 @@ pub fn collide_with_stage_grounded( .0 .project_onto_normalized(current_segment.normal) }; - ground_common.current_line_id.segment = segment_idx; + state_context.surface.land(StageLineID { + polygon: current_line_id.polygon, + segment: segment_idx, + }); GroundedMotionResult::InGround } - GroundedPositionResult::InAir => GroundedMotionResult::InAir, + GroundedPositionResult::InAir => { + state_context.surface.leave_ground(); + GroundedMotionResult::InAir + } } } else { + state_context.surface.leave_ground(); GroundedMotionResult::InAir } } -pub fn collision_interrupt( - ctx: &mut CollisionContext, - common: &mut GroundedStateCommon, - can_walk_off: bool, -) -> Option { - match collide_with_stage_grounded(ctx, common, can_walk_off) { +pub fn collision_interrupt(ctx: &mut CollisionContext, can_walk_off: bool) -> Option { + match collide_with_stage_grounded(ctx, can_walk_off) { GroundedMotionResult::InGround => None, - GroundedMotionResult::InAir => Some(FighterState::Fall(fall::FallState::new(air::AirborneStateCommon::default()))), + GroundedMotionResult::InAir => Some(FighterState::Fall(fall::FallState::new( + air::AirborneStateCommon::default(), + ))), } } #[cfg(test)] mod tests { use super::*; + use crate::fighter::ecb::FighterPreviousECB; + use crate::fighter::{FighterECB, FighterSurfaceState}; use crate::stage::line::StagePolyType; fn two_segment_floor() -> StagePoly { @@ -346,28 +348,110 @@ mod tests { GroundedPositionResult::InAir )); } + + #[test] + fn grounded_collision_updates_surface_segment() { + let stage_collision = crate::stage::line::StageCollision { + stage_polys: vec![two_segment_floor()], + }; + let mut surface = FighterSurfaceState::Grounded { + line_id: StageLineID { + polygon: 0, + segment: 0, + }, + }; + let mut translation = FighterTranslation(FGVec2::lit("11", "0")); + let previous_translation = FighterPreviousTranslation(translation.0); + let mut velocity = FighterVelocity::default(); + let ecb = FighterECB { + vertical_half: FGi32::ONE, + horizontal_half: FGi32::ONE, + }; + let previous_ecb = FighterPreviousECB::from(ecb); + let mut context = CollisionContext { + translation: &mut translation, + prev_translation: &previous_translation, + velocity: &mut velocity, + surface: &mut surface, + ecb: &ecb, + prev_ecb: &previous_ecb, + stage_collision: &stage_collision, + }; + + assert!(matches!( + collide_with_stage_grounded(&mut context, true), + GroundedMotionResult::InGround + )); + assert_eq!( + surface.line_id(), + Some(StageLineID { + polygon: 0, + segment: 1, + }) + ); + } + + #[test] + fn walking_off_edge_clears_surface_contact() { + let stage_collision = crate::stage::line::StageCollision { + stage_polys: vec![StagePoly::build( + StagePolyType::Platform, + &[ + (FGVec2::lit("0", "0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("10", "0"), StagePolyLineSegmentType::Floor), + ], + )], + }; + let mut surface = FighterSurfaceState::Grounded { + line_id: StageLineID { + polygon: 0, + segment: 0, + }, + }; + let mut translation = FighterTranslation(FGVec2::lit("11", "0")); + let previous_translation = FighterPreviousTranslation(translation.0); + let mut velocity = FighterVelocity::default(); + let ecb = FighterECB { + vertical_half: FGi32::ONE, + horizontal_half: FGi32::ONE, + }; + let previous_ecb = FighterPreviousECB::from(ecb); + let mut context = CollisionContext { + translation: &mut translation, + prev_translation: &previous_translation, + velocity: &mut velocity, + surface: &mut surface, + ecb: &ecb, + prev_ecb: &previous_ecb, + stage_collision: &stage_collision, + }; + + assert!(matches!( + collide_with_stage_grounded(&mut context, true), + GroundedMotionResult::InAir + )); + assert_eq!(surface, FighterSurfaceState::Airborne); + } } pub fn grounded_movement_common_interrupts( state_context: &InterruptContext, - ground_common: &GroundedStateCommon, ) -> Option { - super::jump::check_input(state_context, ground_common) + super::jump::check_input(state_context) } pub fn grounded_movement_standstill_common_interrupts( state_context: &InterruptContext, - ground_common: &GroundedStateCommon, ) -> Option { - if let Some(state) = ground_attack::check_interrupt(state_context, &ground_common) { + if let Some(state) = ground_attack::check_interrupt(state_context) { Some(state) - } else if let Some(state) = dash::check_input(state_context, &ground_common) { + } else if let Some(state) = dash::check_input(state_context) { Some(state) - } else if let Some(state) = turn::check_input(state_context, &ground_common) { + } else if let Some(state) = turn::check_input(state_context) { Some(state) - } else if let Some(state) = walk::check_input(state_context, &ground_common) { + } else if let Some(state) = walk::check_input(state_context) { Some(state) - } else if let Some(state) = wait::check_input(state_context, &ground_common) { + } else if let Some(state) = wait::check_input(state_context) { Some(state) } else { None diff --git a/src/fighter/state/ground_attack.rs b/src/fighter/state/ground_attack.rs index 59bfee2..87d1241 100644 --- a/src/fighter/state/ground_attack.rs +++ b/src/fighter/state/ground_attack.rs @@ -2,14 +2,13 @@ use crate::fighter::{ attack::{AttackKind, update_active_hitbox_list}, state::{ CollisionContext, EnterContext, ExitContext, FighterState, FighterStateImpl, - InterruptContext, UpdateContext, ground::GroundedStateCommon, wait::WaitState, + InterruptContext, UpdateContext, wait::WaitState, }, }; #[derive(Clone, Debug, Hash)] pub struct GroundAttackState { pub attack_kind: AttackKind, - pub grounded_common: GroundedStateCommon, } impl FighterStateImpl for GroundAttackState { @@ -17,9 +16,7 @@ impl FighterStateImpl for GroundAttackState { fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.is_current_animation_finished() { - Some(FighterState::Wait(WaitState { - grounded_common: self.grounded_common.clone(), - })) + Some(FighterState::Wait(WaitState)) } else { None } @@ -53,7 +50,7 @@ impl FighterStateImpl for GroundAttackState { active_hitboxes: Vec::new(), solved_hitboxes: Vec::new(), hit_fighters: Vec::new(), - stalled: false + stalled: false, }); } @@ -67,18 +64,13 @@ impl FighterStateImpl for GroundAttackState { ) -> Option { None } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } -pub fn check_interrupt( - state_context: &InterruptContext, - grounded_common: &GroundedStateCommon, -) -> Option { +pub fn check_interrupt(state_context: &InterruptContext) -> Option { if state_context .input .has_command(crate::input::FighterCommands::Attack) @@ -89,7 +81,6 @@ pub fn check_interrupt( .to_attack_kind_grounded(); Some(FighterState::GroundAttack(GroundAttackState { attack_kind, - grounded_common: grounded_common.clone(), })) } else { None diff --git a/src/fighter/state/hitstun.rs b/src/fighter/state/hitstun.rs index 05234f3..545d9d2 100644 --- a/src/fighter/state/hitstun.rs +++ b/src/fighter/state/hitstun.rs @@ -7,7 +7,7 @@ use crate::{ InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, fall::FallState, - ground::{GroundedStateCommon, collision_interrupt}, + ground::collision_interrupt, land::LandingType, wait::WaitState, }, @@ -17,7 +17,7 @@ use crate::{ #[derive(Clone, Debug, Hash)] pub enum HitstopStateKind { - Grounded(GroundedStateCommon), + Grounded, Airborne, } @@ -43,7 +43,7 @@ impl FighterStateImpl for HitstunState { fn name(&self) -> String { let hitstop_kind = match self.hitstop_kind { - HitstopStateKind::Grounded(_) => "Grounded", + HitstopStateKind::Grounded => "Grounded", HitstopStateKind::Airborne => "Airborne", }; @@ -58,14 +58,10 @@ impl FighterStateImpl for HitstunState { fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.get_current_animation_frame() > self.hit_reaction.hitstun_frames { match &self.hitstop_kind { - HitstopStateKind::Grounded(grounded_state_common) => { - Some(FighterState::Wait(WaitState { - grounded_common: grounded_state_common.clone(), - })) - } - HitstopStateKind::Airborne => { - Some(FighterState::Fall(FallState::new(AirborneStateCommon::default()))) - } + HitstopStateKind::Grounded => Some(FighterState::Wait(WaitState)), + HitstopStateKind::Airborne => Some(FighterState::Fall(FallState::new( + AirborneStateCommon::default(), + ))), } } else { None @@ -85,8 +81,9 @@ impl FighterStateImpl for HitstunState { } match self.hitstop_kind { - HitstopStateKind::Grounded(_) => {} + HitstopStateKind::Grounded => {} HitstopStateKind::Airborne => { + ctx.surface.leave_ground(); air::integrate_gravity(ctx.velocity, ctx.fighter_attributes, false); } } @@ -103,7 +100,7 @@ impl FighterStateImpl for HitstunState { fn on_enter(&mut self, state_context: &mut EnterContext) { let animation = match self.hitstop_kind { - HitstopStateKind::Grounded(_) => AnimKind::FlinchGround, + HitstopStateKind::Grounded => AnimKind::FlinchGround, HitstopStateKind::Airborne => AnimKind::FlinchAir, }; state_context.play_animation(animation, false); @@ -116,9 +113,7 @@ impl FighterStateImpl for HitstunState { state_context: &mut CollisionContext, ) -> Option { match &mut self.hitstop_kind { - HitstopStateKind::Grounded(grounded_state_common) => { - collision_interrupt(state_context, grounded_state_common, true) - } + HitstopStateKind::Grounded => collision_interrupt(state_context, true), HitstopStateKind::Airborne => { air::collision_interrupt(state_context, LandingType::Normal) } @@ -127,7 +122,7 @@ impl FighterStateImpl for HitstunState { fn is_visually_grounded<'a>(&'a self) -> bool { match self.hitstop_kind { - HitstopStateKind::Grounded(_) => true, + HitstopStateKind::Grounded => true, HitstopStateKind::Airborne => false, } } diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index 7f43435..8b2b9d7 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -2,10 +2,17 @@ use bevy::prelude::*; use crate::{ fighter::{ - animation::AnimKind, state::{ - CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, UpdateContext, air::{self, AirborneStateCommon}, air_dodge::{self, AirDodgeState}, ground::{self, GroundedStateCommon}, + animation::AnimKind, + state::{ + CollisionContext, EnterContext, FighterState, FighterStateImpl, InterruptContext, + UpdateContext, + air::{self, AirborneStateCommon}, + air_dodge::{self, AirDodgeState}, + ground, }, - }, input::FighterCommands, math::{int::FGi32, vec::FGVec2}, + }, + input::FighterCommands, + math::{int::FGi32, vec::FGVec2}, }; #[derive(Debug, Hash, Clone, Copy)] pub enum JumpType { @@ -22,14 +29,12 @@ pub struct JumpState { #[derive(Debug, Clone, Hash)] pub struct JumpSquatState { - pub grounded_common: GroundedStateCommon, pub duration_counter: u32, } impl JumpSquatState { - pub fn create(grounded_common: GroundedStateCommon) -> Self { + pub fn create() -> Self { Self { - grounded_common, duration_counter: 0, } } @@ -69,7 +74,10 @@ impl FighterStateImpl for JumpSquatState { } else { JumpType::ShortHop }; - Some(FighterState::Jump(JumpState { jump_type, airborne_state_common: AirborneStateCommon::default() })) + Some(FighterState::Jump(JumpState { + jump_type, + airborne_state_common: AirborneStateCommon::default(), + })) } else { None } @@ -84,17 +92,15 @@ impl FighterStateImpl for JumpSquatState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.grounded_common, false) + ground::collision_interrupt(state_context, false) } fn on_enter(&mut self, state_context: &mut EnterContext) { state_context.play_animation(AnimKind::JumpSquat, false); state_context.input.clear_command(FighterCommands::Jump); } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } @@ -105,7 +111,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() + airborne_state_common: AirborneStateCommon::default(), })) } else { air_dodge::check_input(state_context) @@ -176,22 +182,17 @@ impl FighterStateImpl for JumpState { crate::fighter::state::land::LandingType::Normal, ) } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { false } } -pub fn check_input( - state_context: &InterruptContext, - ground_common: &GroundedStateCommon, -) -> Option { +pub fn check_input(state_context: &InterruptContext) -> Option { state_context .input .has_command(FighterCommands::Jump) - .then(|| FighterState::JumpSquat(JumpSquatState::create(ground_common.clone()))) + .then(|| FighterState::JumpSquat(JumpSquatState::create())) } pub fn check_input_double_jump(state_context: &InterruptContext) -> Option { @@ -201,7 +202,7 @@ pub fn check_input_double_jump(state_context: &InterruptContext) -> Option Self { - Self { - grounded_common, - landing_type, - } + pub fn create(landing_type: LandingType) -> Self { + Self { landing_type } } } @@ -34,14 +30,14 @@ impl FighterStateImpl for LandingState { fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { if state_context.is_current_animation_finished() { - grounded_movement_standstill_common_interrupts(state_context, &self.grounded_common) + grounded_movement_standstill_common_interrupts(state_context) } else if state_context.get_current_animation_frame() >= state_context.fighter_attributes.landing_iasa && let LandingType::Normal = self.landing_type { - 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)) + dash::check_input(state_context).or_else(|| { + ground::grounded_movement_common_interrupts(state_context) + .or_else(|| walk::check_input(state_context)) }) } else { None @@ -56,7 +52,7 @@ impl FighterStateImpl for LandingState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.grounded_common, true) + ground::collision_interrupt(state_context, true) } fn on_enter(&mut self, state_context: &mut EnterContext) { @@ -69,10 +65,8 @@ impl FighterStateImpl for LandingState { } }; } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 64a64b7..68a9138 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -7,7 +7,8 @@ use bevy::prelude::*; use crate::fighter::animation::{AnimKind, FighterAnimationFrame}; use crate::fighter::attack::{ - FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, FighterStaleMoveQueue, HitStop, + FighterAttackScriptAssets, FighterKnockbackVelocity, FighterPendingHitReaction, + FighterStaleMoveQueue, HitStop, }; use crate::fighter::attributes::FighterAttributes; use crate::fighter::baked_animation::BakedFighterAnimations; @@ -16,7 +17,9 @@ use crate::fighter::manifest::FighterManifest; use crate::fighter::state::hitstun::HitstunState; use crate::fighter::visual::FighterAnimations; use crate::fighter::{ - Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, FighterPreviousTranslation, FighterTranslation, FighterVelocity, + Fighter, FighterECB, FighterFacingDirection, FighterHitboxes, FighterHitstop, + FighterHitstopShake, FighterHitstopShakeKind, FighterHitstopShakeRole, + FighterPreviousTranslation, FighterSurfaceState, FighterTranslation, FighterVelocity, }; use crate::game_settings::GameSettings; use crate::input::FighterInput; @@ -95,9 +98,7 @@ pub trait FighterStateImpl: Sized { None } - fn is_visually_grounded<'a>( - &'a self, - ) -> bool; + fn is_visually_grounded<'a>(&'a self) -> bool; } pub struct AnimationStatus { @@ -151,6 +152,7 @@ pub struct UpdateContext<'a> { pub input: &'a mut FighterInput, pub hitboxes: &'a mut FighterHitboxes, pub velocity: &'a mut FighterVelocity, + pub surface: &'a mut FighterSurfaceState, pub knockback_velocity: &'a mut FighterKnockbackVelocity, pub game_settings: &'a GameSettings, pub animation_frame: u32, @@ -170,6 +172,7 @@ pub struct CollisionContext<'a> { pub translation: &'a mut FighterTranslation, pub prev_translation: &'a FighterPreviousTranslation, pub velocity: &'a mut FighterVelocity, + pub surface: &'a mut FighterSurfaceState, pub ecb: &'a FighterECB, pub prev_ecb: &'a FighterPreviousECB, pub stage_collision: &'a StageCollision, @@ -212,6 +215,7 @@ pub struct FighterFrameQuery { pub input: &'static mut FighterInput, pub hitboxes: &'static mut FighterHitboxes, pub velocity: &'static mut FighterVelocity, + pub surface: &'static mut FighterSurfaceState, pub knockback_velocity: &'static mut FighterKnockbackVelocity, pub stale_move_queue: &'static mut FighterStaleMoveQueue, pub ecb: &'static FighterECB, @@ -292,13 +296,14 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { input: &mut self.input, hitboxes: &mut self.hitboxes, velocity: &mut self.velocity, + surface: &mut self.surface, knockback_velocity: &mut self.knockback_velocity, game_settings, animation_frame: self.animation_frame.frame, fighter_attributes: attributes, attack_scripts, attack_script_assets, - facing_direction: &self.facing_direction + facing_direction: &self.facing_direction, } } @@ -310,6 +315,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { translation: &mut self.translation, prev_translation: &self.prev_translation, velocity: &mut self.velocity, + surface: &mut self.surface, ecb: &self.ecb, prev_ecb: &self.prev_ecb, stage_collision, @@ -400,7 +406,9 @@ pub fn state_interrupt_system( let new_state = if let Some(reaction) = pending_hit_reaction.take() { hitstop.remaining_frames = *reaction.hitstop_frames; - let shake_frames = reaction.hitstop_frames.calculate_shake_frames(&game_settings); + let shake_frames = reaction + .hitstop_frames + .calculate_shake_frames(&game_settings); hitstop_shake.role = FighterHitstopShakeRole::Receiver; hitstop_shake.total_frames = shake_frames; hitstop_shake.remaining_frames = shake_frames; @@ -409,36 +417,26 @@ pub fn state_interrupt_system( } else { FighterHitstopShakeKind::Vertical }; - - let mut hitstop_kind = match &*state { - FighterState::Wait(value) => { - hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) - } - FighterState::Walk(value) => { - hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) - } - FighterState::Dash(value) => { - hitstun::HitstopStateKind::Grounded(value.ground_common.clone()) - } - FighterState::Run(value) => { - hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) - } - FighterState::Turn(value) => { - hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) - } - FighterState::GroundAttack(value) => { - hitstun::HitstopStateKind::Grounded(value.grounded_common.clone()) - } - _ => hitstun::HitstopStateKind::Airborne, + + let mut hitstop_kind = if frame.surface.is_grounded() { + hitstun::HitstopStateKind::Grounded + } else { + hitstun::HitstopStateKind::Airborne }; - if let hitstun::HitstopStateKind::Grounded(_) = hitstop_kind { + 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) { + 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 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"); @@ -450,6 +448,10 @@ pub fn state_interrupt_system( } } + if matches!(hitstop_kind, hitstun::HitstopStateKind::Airborne) { + frame.surface.leave_ground(); + } + info!("Entering hitstop kind: {:?}", hitstop_kind); Some(FighterState::Hitstun(HitstunState::new( reaction, diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs index 973b633..38414ac 100644 --- a/src/fighter/state/run.rs +++ b/src/fighter/state/run.rs @@ -6,19 +6,14 @@ use super::{ }; use crate::fighter::animation::AnimKind; use crate::fighter::state::FighterState; -use crate::fighter::state::ground::GroundedStateCommon; #[derive(Component, Debug, Clone, Hash)] -pub struct RunState { - pub grounded_common: GroundedStateCommon, -} +pub struct RunState; impl FighterStateImpl for RunState { const NAME: &'static str = "Run"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { - if let Some(state) = - ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) - { + if let Some(state) = ground::grounded_movement_common_interrupts(state_context) { return Some(state); } @@ -28,8 +23,7 @@ impl FighterStateImpl for RunState { { None } else { - walk::check_input(state_context, &self.grounded_common) - .or_else(|| wait::check_input(state_context, &self.grounded_common)) + walk::check_input(state_context).or_else(|| wait::check_input(state_context)) } } @@ -62,26 +56,19 @@ impl FighterStateImpl for RunState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.grounded_common, true) + ground::collision_interrupt(state_context, true) } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } -pub fn check_input( - state_context: &InterruptContext, - ground_state_common: &GroundedStateCommon, -) -> Option { +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.run_stick_threshold { - return Some(FighterState::Run(RunState { - grounded_common: ground_state_common.clone(), - })); + return Some(FighterState::Run(RunState)); } None } diff --git a/src/fighter/state/turn.rs b/src/fighter/state/turn.rs index 0460131..9b40427 100644 --- a/src/fighter/state/turn.rs +++ b/src/fighter/state/turn.rs @@ -1,13 +1,11 @@ use super::{CollisionContext, EnterContext, FighterStateImpl, InterruptContext, UpdateContext}; use crate::fighter::animation::AnimKind; -use crate::fighter::state::ground::{self, GroundedStateCommon}; +use crate::fighter::state::ground; use crate::fighter::state::{FighterState, dash, jump, wait, walk}; use crate::input::FighterCommands; #[derive(Debug, Clone, Hash)] -pub struct TurnState { - pub grounded_common: GroundedStateCommon, -} +pub struct TurnState; impl FighterStateImpl for TurnState { const NAME: &'static str = "Turn"; @@ -19,19 +17,19 @@ impl FighterStateImpl for TurnState { .input_common .turn_into_dashback_window as u32 { - if let Some(state) = dash::check_input(state_context, &self.grounded_common) { + if let Some(state) = dash::check_input(state_context) { return Some(state); } } - if let Some(state) = jump::check_input(state_context, &self.grounded_common) { + if let Some(state) = jump::check_input(state_context) { *state_context.facing_direction = state_context.facing_direction.reverse(); Some(state) } else if state_context.is_current_animation_finished() { - if let Some(state) = dash::check_input(state_context, &self.grounded_common) { + if let Some(state) = dash::check_input(state_context) { Some(state) - } else if let Some(state) = walk::check_input(state_context, &self.grounded_common) { + } else if let Some(state) = walk::check_input(state_context) { Some(state) - } else if let Some(state) = wait::check_input(state_context, &self.grounded_common) { + } else if let Some(state) = wait::check_input(state_context) { Some(state) } else { None @@ -54,20 +52,15 @@ impl FighterStateImpl for TurnState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.grounded_common, false) + ground::collision_interrupt(state_context, false) } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } -pub fn check_input( - state_context: &InterruptContext, - grounded_common: &GroundedStateCommon, -) -> Option { +pub fn check_input(state_context: &InterruptContext) -> Option { let input_frame = state_context.input.get_last_frame(); let has_smash_input = state_context @@ -81,9 +74,7 @@ pub fn check_input( && input_frame.movement.x.signum() != state_context.facing_direction.to_sign() && !has_smash_input { - return Some(FighterState::Turn(TurnState { - grounded_common: grounded_common.clone(), - })); + return Some(FighterState::Turn(TurnState)); } None } diff --git a/src/fighter/state/wait.rs b/src/fighter/state/wait.rs index 962babf..23bf49d 100644 --- a/src/fighter/state/wait.rs +++ b/src/fighter/state/wait.rs @@ -3,24 +3,18 @@ use super::{ }; use crate::fighter::animation::AnimKind; use crate::fighter::state::FighterState; -use crate::fighter::state::ground::GroundedStateCommon; #[derive(Debug, Clone, Hash)] -pub struct WaitState { - pub grounded_common: GroundedStateCommon, -} +pub struct WaitState; impl FighterStateImpl for WaitState { const NAME: &'static str = "Wait"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { - if let Some(state) = - ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) - { + if let Some(state) = ground::grounded_movement_common_interrupts(state_context) { Some(state) - } else if let Some(state) = ground::grounded_movement_standstill_common_interrupts( - state_context, - &self.grounded_common, - ) { + } else if let Some(state) = + ground::grounded_movement_standstill_common_interrupts(state_context) + { // Don't transition into yourself if let FighterState::Wait(_) = state { None @@ -44,26 +38,19 @@ impl FighterStateImpl for WaitState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.grounded_common, false) + ground::collision_interrupt(state_context, false) } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } -pub fn check_input( - state_context: &InterruptContext, - ground_state_common: &GroundedStateCommon, -) -> Option { +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::Wait(WaitState { - grounded_common: ground_state_common.clone(), - })); + return Some(FighterState::Wait(WaitState)); } None } diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index d3ba75d..0403d99 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -4,23 +4,17 @@ use super::{ }; use crate::fighter::animation::AnimKind; use crate::fighter::state::FighterState; -use crate::fighter::state::ground::GroundedStateCommon; #[derive(Debug, Clone, Hash)] -pub struct WalkState { - pub grounded_common: GroundedStateCommon, -} +pub struct WalkState; impl FighterStateImpl for WalkState { const NAME: &'static str = "Walk"; fn check_interrupt(&self, state_context: &mut InterruptContext) -> Option { - if let Some(state) = - ground::grounded_movement_common_interrupts(state_context, &self.grounded_common) - { + if let Some(state) = ground::grounded_movement_common_interrupts(state_context) { return Some(state); } - wait::check_input(state_context, &self.grounded_common) - .or_else(|| dash::check_input(state_context, &self.grounded_common)) + wait::check_input(state_context).or_else(|| dash::check_input(state_context)) } fn on_enter(&mut self, state_context: &mut EnterContext) { @@ -54,26 +48,19 @@ impl FighterStateImpl for WalkState { &mut self, state_context: &mut CollisionContext, ) -> Option { - ground::collision_interrupt(state_context, &mut self.grounded_common, false) + ground::collision_interrupt(state_context, false) } - - fn is_visually_grounded<'a>( - &'a self, - ) -> bool { + + fn is_visually_grounded<'a>(&'a self) -> bool { true } } -pub fn check_input( - state_context: &InterruptContext, - ground_state_common: &GroundedStateCommon, -) -> Option { +pub fn check_input(state_context: &InterruptContext) -> Option { let input_frame = state_context.input.get_last_frame(); if !input_frame.movement.x.is_zero() { - return Some(FighterState::Walk(WalkState { - grounded_common: ground_state_common.clone(), - })); + return Some(FighterState::Walk(WalkState)); } None diff --git a/src/fighter/surface.rs b/src/fighter/surface.rs new file mode 100644 index 0000000..6ee7f6b --- /dev/null +++ b/src/fighter/surface.rs @@ -0,0 +1,62 @@ +use bevy::prelude::*; + +use crate::stage::line::StageLineID; + +/// The stage surface currently supporting a fighter. +/// +/// This is physics state rather than fighter-state-machine state. Keeping it +/// separate means grounded, landing, and hitstun states all share one source +/// of truth for stage contact. +#[derive(Component, Clone, Copy, Debug, Default, Hash, PartialEq, Eq)] +pub enum FighterSurfaceState { + #[default] + Airborne, + Grounded { + line_id: StageLineID, + }, +} + +impl FighterSurfaceState { + pub fn is_grounded(self) -> bool { + matches!(self, Self::Grounded { .. }) + } + + pub fn line_id(self) -> Option { + match self { + Self::Airborne => None, + Self::Grounded { line_id } => Some(line_id), + } + } + + pub(crate) fn land(&mut self, line_id: StageLineID) { + *self = Self::Grounded { line_id }; + } + + pub(crate) fn leave_ground(&mut self) { + *self = Self::Airborne; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn contact_lifecycle_has_one_source_of_truth() { + let mut surface = FighterSurfaceState::default(); + assert!(!surface.is_grounded()); + assert_eq!(surface.line_id(), None); + + let line_id = StageLineID { + polygon: 2, + segment: 3, + }; + surface.land(line_id); + assert!(surface.is_grounded()); + assert_eq!(surface.line_id(), Some(line_id)); + + surface.leave_ground(); + assert!(!surface.is_grounded()); + assert_eq!(surface.line_id(), None); + } +} diff --git a/src/stage/line.rs b/src/stage/line.rs index a0a50fb..665561b 100644 --- a/src/stage/line.rs +++ b/src/stage/line.rs @@ -26,7 +26,7 @@ pub enum StagePolyType { Platform, } -#[derive(Reflect, Hash, Debug, Clone, Copy, Default)] +#[derive(Reflect, Hash, Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct StageLineID { pub polygon: usize, pub segment: usize,