Initial work on rollback and refactor

This commit is contained in:
2026-08-12 12:27:35 +02:00
parent ad6c1e17ac
commit a21f24b2d9
45 changed files with 1451 additions and 717 deletions
+116
View File
@@ -0,0 +1,116 @@
use std::hash::{Hash, Hasher};
use bevy::prelude::*;
use super::{FighterState, FighterStateContext, FighterStateTransition, ground, run, wait, walk};
use crate::fighter::{FighterAttributes, FighterVelocity};
use crate::game_settings::GameSettings;
use crate::input::{
FighterCommands::{SmashMoveLeft, SmashMoveRight},
FighterInput,
};
pub struct DashStateCreator {}
#[derive(Component, Debug, Clone, Copy)]
pub struct DashState {
frames_in_dash: u32,
direction: f32,
}
pub fn hash_dash_state(dash_state: &DashState) -> u64 {
let mut hasher = bevy_ggrs::checksum_hasher();
dash_state.direction.to_bits().hash(&mut hasher);
dash_state.frames_in_dash.hash(&mut hasher);
hasher.finish()
}
impl DashState {
pub fn new(direction: f32) -> Self {
Self {
frames_in_dash: 0,
direction,
}
}
}
impl FighterState for DashState {
fn check_interrupt(
&self,
state_context: &FighterStateContext,
) -> Option<FighterStateTransition> {
if self.frames_in_dash > state_context.fighter_attribs.dash_duration {
// Dash finish shenanigans
run::check_input(state_context)
.or_else(|| walk::check_input(state_context))
.or_else(|| wait::check_input(state_context))
} else {
check_smash_input_with_dir(state_context)
.filter(|dir| *dir != self.direction)
.and_then(|dir| Some(FighterStateTransition::Dash(dir)))
}
}
}
pub fn start(state_context: &mut FighterStateContext) {
state_context.input.clear_buffer();
}
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<f32> {
if state_context.input.has_command(SmashMoveLeft) {
return Some(-1.0);
} else if state_context.input.has_command(SmashMoveRight) {
return Some(1.0);
} else {
None
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
check_smash_input_with_dir(state_context).map(|dir| FighterStateTransition::Dash(dir))
}
pub fn dash_update(
dash_query: Query<(
&mut DashState,
&FighterAttributes,
&FighterInput,
&mut FighterVelocity,
)>,
game_settings: Res<GameSettings>,
) {
for (mut dash, attributes, input, mut velocity) in dash_query {
dash.frames_in_dash += 1;
let last_frame = input.get_last_frame();
let (accel, target_vel) = attributes.get_accel_and_target_dashrun(&last_frame);
let accel = ground::compute_ground_accel(
accel,
target_vel,
velocity.x,
attributes,
game_settings.as_ref(),
);
velocity.x += accel;
}
}
pub fn on_insert_dash(
add: On<Insert, DashState>,
query: Query<
(
Entity,
&mut FighterInput,
&FighterAttributes,
&mut FighterVelocity,
),
With<DashState>,
>,
) {
for (entity, mut input, attribs, mut velocity) in query {
if entity == add.entity {
velocity.x = attribs.dash_initial_velocity * input.get_last_frame().movement.x.signum();
input.clear_buffer();
}
}
}
+55
View File
@@ -0,0 +1,55 @@
//! Ground movement math shared by every grounded state. Deliberately free of
//! ECS types so it stays cheap to reason about and to unit-test — it has to be
//! bit-for-bit deterministic for rollback.
use crate::fighter::FighterAttributes;
use crate::game_settings::GameSettings;
pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 {
if friction.abs() > ground_vel.abs() {
-ground_vel
} else {
(-ground_vel.signum()) * friction.abs()
}
}
pub fn move_accelerate() {}
pub fn compute_ground_accel(
accel: f32,
target_vel: f32,
gr_vel: f32,
attribs: &FighterAttributes,
game_settings: &GameSettings,
) -> f32 {
if target_vel == 0.0 {
apply_grounded_friction(attribs.ground_friction, gr_vel)
} else {
let mut accel = accel;
let ground_max_horizontal_velocity =
game_settings.figher_common.ground_max_horizontal_velocity;
if !(gr_vel * accel < 0.0) {
// accelerating, not reversing
if accel > 0.0 {
if gr_vel + accel > target_vel {
accel = -attribs.ground_friction;
if gr_vel + accel < target_vel {
accel = target_vel - gr_vel;
}
if gr_vel + accel > ground_max_horizontal_velocity {
accel = ground_max_horizontal_velocity - gr_vel;
}
}
} else if gr_vel + accel < target_vel {
accel = attribs.ground_friction;
if gr_vel + accel > target_vel {
accel = target_vel - gr_vel;
}
if gr_vel + accel < -ground_max_horizontal_velocity {
accel = -ground_max_horizontal_velocity - gr_vel;
}
}
}
accel
}
}
+94
View File
@@ -0,0 +1,94 @@
//! The fighter state machine: the [`FighterState`] trait every state
//! implements, the context they inspect, and the generic system that runs the
//! interrupt check. One state per submodule.
use bevy::prelude::*;
use crate::fighter::FighterAttributes;
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
pub mod dash;
pub mod ground;
pub mod run;
pub mod wait;
pub mod walk;
pub enum FighterStateTransition {
Wait,
Walk,
Dash(f32),
Run,
}
pub trait FighterState: Component + Sized {
fn remove(&self, state_context: &FighterStateContext, commands: &mut Commands) {
commands.entity(state_context.entity).remove::<Self>();
}
fn check_interrupt(
&self,
state_context: &FighterStateContext,
) -> Option<FighterStateTransition>;
}
pub struct FighterStateContext<'a> {
pub input: &'a mut FighterInput,
pub fighter_attribs: &'a FighterAttributes,
pub game_settings: &'a GameSettings,
pub entity: Entity,
}
fn create_state(
transition: FighterStateTransition,
state_context: FighterStateContext,
commands: &mut Commands,
) {
let mut ent_cmd = commands.entity(state_context.entity);
match transition {
FighterStateTransition::Wait => ent_cmd.insert(wait::WaitState),
FighterStateTransition::Walk => ent_cmd.insert(walk::WalkState),
FighterStateTransition::Dash(dir) => ent_cmd.insert(dash::DashState::new(dir)),
FighterStateTransition::Run => ent_cmd.insert(run::RunState),
};
}
fn get_state_debug_name(transition: &FighterStateTransition) -> &'static str {
match transition {
FighterStateTransition::Wait => "wait",
FighterStateTransition::Walk => "walk",
FighterStateTransition::Dash(_) => "dash",
FighterStateTransition::Run => "run",
}
}
#[derive(Component)]
pub struct StateNameDebug(pub &'static str);
impl std::fmt::Display for StateNameDebug {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
pub fn state_interrupt_system<T: FighterState + Component + std::fmt::Debug>(
query: Query<(Entity, &T, &mut FighterInput, &FighterAttributes)>,
game_settings: Res<GameSettings>,
mut commands: Commands,
) {
for (ent, state, mut input, attribs) in query {
let state_context = FighterStateContext {
input: &mut input,
fighter_attribs: &attribs,
game_settings: &game_settings,
entity: ent,
};
if let Some(new_state) = state.check_interrupt(&state_context) {
commands.entity(ent).remove::<T>();
info!("State {:?} -> {}", state, get_state_debug_name(&new_state));
commands
.entity(ent)
.insert(StateNameDebug(get_state_debug_name(&new_state)));
create_state(new_state, state_context, &mut commands);
}
}
}
+49
View File
@@ -0,0 +1,49 @@
use bevy::prelude::*;
use super::{FighterState, FighterStateContext, FighterStateTransition, ground, wait, walk};
use crate::fighter::{FighterAttributes, FighterVelocity};
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
#[derive(Component, Debug, Clone, Copy)]
pub struct RunState;
impl FighterState for RunState {
fn check_interrupt(
&self,
state_context: &FighterStateContext,
) -> Option<FighterStateTransition> {
if let Some(_) = check_input(state_context) {
None
} else {
walk::check_input(state_context).or_else(|| wait::check_input(state_context))
}
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
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(FighterStateTransition::Run);
}
None
}
pub fn run_update(
fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With<RunState>>,
game_settings: Res<GameSettings>,
) {
for (mut velocity, attributes, input) in fighters {
let last_frame = input.get_last_frame();
let (accel, target_vel) = attributes.get_accel_and_target_dashrun(&last_frame);
let accel = ground::compute_ground_accel(
accel,
target_vel,
velocity.x,
attributes,
game_settings.as_ref(),
);
velocity.x += accel;
}
}
+43
View File
@@ -0,0 +1,43 @@
use bevy::prelude::*;
use super::{FighterState, FighterStateContext, FighterStateTransition, ground, walk};
use crate::fighter::{FighterAttributes, FighterVelocity};
use crate::game_settings::GameSettings;
#[derive(Component, Debug, Clone, Copy)]
pub struct WaitState;
impl FighterState for WaitState {
fn check_interrupt(
&self,
state_context: &FighterStateContext,
) -> Option<FighterStateTransition> {
walk::check_input(state_context)
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
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(FighterStateTransition::Wait);
}
None
}
pub fn wait_update(
query: Query<(&FighterAttributes, &mut FighterVelocity), With<WaitState>>,
game_settings: Res<GameSettings>,
) {
for (attribs, mut velocity) in query {
let friction = if velocity.x.abs() > attribs.max_walk_vel {
attribs.ground_friction
* game_settings
.figher_common
.ground_friction_over_walk_speed_multiplier
} else {
attribs.ground_friction
};
velocity.x += ground::apply_grounded_friction(friction, velocity.x);
}
}
+43
View File
@@ -0,0 +1,43 @@
use bevy::prelude::*;
use super::{FighterState, FighterStateContext, FighterStateTransition, dash, ground, wait};
use crate::fighter::{FighterAttributes, FighterVelocity};
use crate::game_settings::GameSettings;
use crate::input::FighterInput;
#[derive(Component, Debug, Clone, Copy)]
pub struct WalkState;
impl FighterState for WalkState {
fn check_interrupt(
&self,
state_context: &FighterStateContext,
) -> Option<FighterStateTransition> {
wait::check_input(state_context).or_else(|| dash::check_input(state_context))
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
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(FighterStateTransition::Walk);
}
None
}
pub fn walk_update(
fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With<WalkState>>,
game_settings: Res<GameSettings>,
) {
for (mut velocity, attributes, input) in fighters {
let last_frame = input.get_last_frame();
let (accel, target_vel) = attributes.get_accel_and_target_walk(&last_frame);
let accel =
ground::compute_ground_accel(accel, target_vel, velocity.x, attributes, &game_settings);
velocity.x += accel;
}
}