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
+39
View File
@@ -0,0 +1,39 @@
use bevy::prelude::*;
use crate::input::FighterInputFrame;
/// Per-fighter tuning values. These are character data, not global rules —
/// anything that applies to every fighter belongs in
/// [`crate::game_settings::FighterSettingsCommon`] instead.
#[derive(Component, Clone, Copy)]
pub struct FighterAttributes {
pub base_walk_accel: f32,
pub stick_walk_accel: f32,
pub max_walk_vel: f32,
pub dash_duration: u32,
pub dash_initial_velocity: f32,
pub base_dash_accel: f32,
pub stick_dash_accel: f32,
pub max_dash_vel: f32,
pub ground_friction: f32,
}
impl FighterAttributes {
pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (f32, f32) {
let accel = input.movement.x * self.stick_dash_accel;
let accel = accel + input.movement.x.signum() * self.base_dash_accel;
let target_vel = input.movement.x * self.max_dash_vel;
(accel, target_vel)
}
pub fn get_accel_and_target_walk(&self, input: &FighterInputFrame) -> (f32, f32) {
let accel = input.movement.x * self.stick_walk_accel;
let accel = accel + input.movement.x.signum() * self.base_walk_accel;
let target_vel = input.movement.x * self.max_walk_vel;
(accel, target_vel)
}
}
+31
View File
@@ -0,0 +1,31 @@
//! Resolving an integrated fighter position against the stage.
use bevy::{color::palettes::css::HOT_PINK, prelude::*};
use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity};
use crate::stage::StageLine;
pub fn collide_fighter_with_scene(
fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>,
stage_planes: Query<&StageLine>,
mut gizmos: Gizmos,
) {
for (ecb, mut velocity, mut translation) in fighters {
for plane in stage_planes {
let ray_from = translation.0;
let ray_to = translation.0 + Vec2::new(0.0, -ecb.vertical_half);
let intersection_result = plane.intersect_ray(Segment2d::new(ray_from, ray_to));
if let Some(result) = intersection_result {
gizmos.sphere(
Isometry3d::from(Vec3::new(result.position.x, result.position.y, 0.0)),
0.1f32,
HOT_PINK,
);
velocity.0 = plane.project(velocity.0);
translation.0 = result.position + Vec2::new(0.0, ecb.vertical_half);
}
}
}
}
-16
View File
@@ -1,16 +0,0 @@
use bevy::prelude::*;
#[derive(Component)]
pub struct FighterAttributes {
pub base_walk_accel: f32,
pub stick_walk_accel: f32,
pub max_walk_vel: f32,
pub dash_duration: u32,
pub dash_initial_velocity: f32,
pub base_dash_accel: f32,
pub stick_dash_accel: f32,
pub max_dash_vel: f32,
pub ground_friction: f32,
}
+30
View File
@@ -0,0 +1,30 @@
//! Fighter debug views: gizmos and the egui inspector window.
use bevy::{color::palettes::css::YELLOW, prelude::*};
use bevy_egui::{EguiContexts, egui};
use crate::fighter::state::StateNameDebug;
use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity};
use crate::input::FighterInput;
pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation)>, mut gizmos: Gizmos) {
for (ecb, trf) in ecbs {
let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x, trf.y, 0.0));
gizmos.lineloop(transformed, YELLOW);
}
}
pub fn fighter_debug(
mut contexts: EguiContexts,
query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>,
) -> Result {
for (vel, input, name) in query {
egui::Window::new("Hello").show(contexts.ctx_mut()?, |ui| {
ui.label(format!("State: {}", name.0));
ui.label(format!("{:?}", vel));
ui.label(format!("{:?}", input.get_last_frame()));
});
}
Ok(())
}
+21
View File
@@ -0,0 +1,21 @@
use bevy::prelude::*;
/// Environment collision box: the diamond used to resolve a fighter against
/// the stage. Purely geometry — the collision response lives in
/// [`super::collision`].
#[derive(Component, Debug, Clone, Copy)]
#[require(Transform)]
pub struct FighterECB {
pub vertical_half: f32,
pub horizontal_half: f32,
}
impl FighterECB {
pub fn to_3d_lineloop(&self, trf: Transform) -> [Vec3; 4] {
let rel_up = trf.transform_point(Vec3::new(0.0, self.vertical_half, 0.0));
let rel_down = trf.transform_point(Vec3::new(0.0, -self.vertical_half, 0.0));
let rel_left = trf.transform_point(Vec3::new(-self.horizontal_half, 0.0, 0.0));
let rel_right = trf.transform_point(Vec3::new(self.horizontal_half, 0.0, 0.0));
[rel_up, rel_right, rel_down, rel_left]
}
}
+86
View File
@@ -0,0 +1,86 @@
//! Everything fighter-shaped: the components that describe a fighter, the
//! state machine that drives it, and the plugin that schedules it all.
use bevy::prelude::*;
use bevy_egui::EguiPrimaryContextPass;
use bevy_ggrs::prelude::*;
pub mod attributes;
pub mod collision;
pub mod debug;
pub mod ecb;
pub mod motion;
pub mod state;
// The fighter components are re-exported so the rest of the crate can say
// `fighter::FighterVelocity` without caring which file it lives in. Named
// explicitly rather than globbed, so moving a type between submodules can't
// silently change what this module exports.
pub use attributes::FighterAttributes;
pub use ecb::FighterECB;
pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded};
use crate::schedule::GameplaySet;
use state::state_interrupt_system;
/// Owns fighter simulation, the rollback registrations for fighter
/// components, and the fighter debug overlays.
#[derive(Default)]
pub struct FighterPlugin;
impl Plugin for FighterPlugin {
fn build(&self, app: &mut App) {
app
// Every state gets a chance to interrupt itself, in priority order.
.add_systems(
GgrsSchedule,
(
state_interrupt_system::<state::wait::WaitState>,
state_interrupt_system::<state::walk::WalkState>,
state_interrupt_system::<state::dash::DashState>,
state_interrupt_system::<state::run::RunState>,
)
.chain()
.in_set(GameplaySet::Interrupt),
)
.add_systems(
GgrsSchedule,
(
state::wait::wait_update,
state::walk::walk_update,
state::dash::dash_update,
state::run::run_update,
)
.chain()
.in_set(GameplaySet::StateUpdate),
)
.add_systems(
GgrsSchedule,
(motion::integrate_gravity, motion::apply_motion)
.chain()
.in_set(GameplaySet::Physics),
)
.add_systems(
GgrsSchedule,
collision::collide_fighter_with_scene.in_set(GameplaySet::Collision),
)
.add_observer(state::dash::on_insert_dash)
// Rollback registration lives next to the systems that write these
// components; a component simulated here but missing from this list
// is a desync waiting to happen.
.rollback_component_with_copy::<FighterECB>()
.rollback_component_with_copy::<FighterVelocity>()
.rollback_component_with_copy::<FighterTranslation>()
.rollback_component_with_copy::<FighterPreviousTranslation>()
.rollback_component_with_copy::<FighterAttributes>()
.rollback_component_with_copy::<Grounded>()
.rollback_component_with_copy::<state::wait::WaitState>()
.rollback_component_with_copy::<state::walk::WalkState>()
.rollback_component_with_copy::<state::dash::DashState>()
.rollback_component_with_copy::<state::run::RunState>()
.checksum_component::<state::dash::DashState>(state::dash::hash_dash_state)
// Debug views.
.add_systems(FixedPostUpdate, debug::debug_draw_ecb)
.add_systems(EguiPrimaryContextPass, debug::fighter_debug);
}
}
+35
View File
@@ -0,0 +1,35 @@
//! Where a fighter is and how it moves. States write [`FighterVelocity`];
//! this module is what turns that into a position.
use bevy::prelude::*;
#[derive(Component, Clone, Copy)]
pub struct Grounded;
#[derive(Component, Deref, DerefMut, Default, Debug, Clone, Copy)]
pub struct FighterVelocity(pub Vec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
pub struct FighterPreviousTranslation(pub Vec2);
#[derive(Component, Deref, DerefMut, Default, Clone, Copy)]
#[require(FighterPreviousTranslation)]
pub struct FighterTranslation(pub Vec2);
pub fn integrate_gravity(_fighters: Query<&mut FighterVelocity>, _time: Res<Time<Fixed>>) {}
pub fn apply_motion(
fighters: Query<(
&FighterVelocity,
&mut FighterTranslation,
&mut FighterPreviousTranslation,
)>,
_time: Res<Time<Fixed>>,
) {
for (velocity, mut translation, mut prev_translation) in fighters {
prev_translation.0 = translation.0;
translation.0 += velocity.0;
}
}
pub fn fighter_movement() {}
-48
View File
@@ -1,48 +0,0 @@
use bevy::prelude::*;
use bevy_egui::{EguiContexts, EguiPrimaryContextPass, egui};
use crate::fighter::state::StateNameDebug;
use crate::fighter::{FighterVelocity, states};
use crate::fighter::{
apply_motion, collide_fighter_with_scene, integrate_gravity, state::state_interrupt_system,
};
use crate::input::player::FighterInput;
#[derive(Default)]
pub struct FighterPlugin;
impl Plugin for FighterPlugin {
fn build(&self, app: &mut App) {
app.add_systems(FixedPostUpdate, super::debug_draw_ecb)
.add_systems(
FixedUpdate,
(
state_interrupt_system::<states::wait::WaitState>,
state_interrupt_system::<states::walk::WalkState>,
state_interrupt_system::<states::dash::DashState>,
state_interrupt_system::<states::run::RunState>,
states::wait::wait_update,
states::walk::walk_update,
states::dash::dash_update,
states::run::run_update,
integrate_gravity,
apply_motion,
collide_fighter_with_scene,
)
.chain(),
)
.add_observer(states::dash::on_insert_dash)
.add_systems(EguiPrimaryContextPass, fighter_debug);
}
}
fn fighter_debug(mut contexts: EguiContexts, query: Query<(&FighterVelocity, &FighterInput, &StateNameDebug)>) -> Result {
for (vel, input, name) in query {
egui::Window::new("Hello").show(contexts.ctx_mut()?, |ui| {
ui.label(format!("State: {}", name.0));
ui.label(format!("{:?}", vel));
ui.label(format!("{:?}", input.get_last_frame()));
});
}
Ok(())
}
+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();
}
}
}
@@ -1,6 +1,9 @@
use bevy::log::info;
//! 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, game_settings::GameSettings, input::player::InputFrame};
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() {
@@ -10,11 +13,15 @@ pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 {
}
}
pub fn move_accelerate() {
pub fn move_accelerate() {}
}
pub fn compute_ground_accel(accel: f32, target_vel: f32, gr_vel: f32, attribs: &FighterAttributes, game_settings: &GameSettings) -> f32 {
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 {
@@ -46,24 +53,3 @@ pub fn compute_ground_accel(accel: f32, target_vel: f32, gr_vel: f32, attribs: &
accel
}
}
impl FighterAttributes {
pub fn get_accel_and_target_dashrun(&self, input: &InputFrame) -> (f32, f32) {
let accel = input.movement.x * self.stick_dash_accel;
let accel = accel + input.movement.x.signum() * self.base_dash_accel;
let target_vel = input.movement.x * self.max_dash_vel;
info!("accel {:?} target_vel {:?}", accel, target_vel);
(accel, target_vel)
}
pub fn get_accel_and_target_walk(&self, input: &InputFrame) -> (f32, f32) {
let accel = input.movement.x * self.stick_walk_accel;
let accel = accel + input.movement.x.signum() * self.base_walk_accel;
let target_vel = input.movement.x * self.max_walk_vel;
(accel, target_vel)
}
}
@@ -1,18 +1,28 @@
use bevy::{prelude::*};
//! 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 crate::{fighter::{FighterAttributes, states}, game_settings::GameSettings, input::player::FighterInput};
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,
Dash(f32),
Run,
}
pub trait FighterState : Component + Sized {
fn remove(&self,
state_context: &FighterStateContext,
commands: &mut Commands) {
pub trait FighterState: Component + Sized {
fn remove(&self, state_context: &FighterStateContext, commands: &mut Commands) {
commands.entity(state_context.entity).remove::<Self>();
}
fn check_interrupt(
@@ -24,17 +34,21 @@ pub trait FighterState : Component + Sized {
pub struct FighterStateContext<'a> {
pub input: &'a mut FighterInput,
pub fighter_attribs: &'a FighterAttributes,
pub game_settings: &'a Res<'a, GameSettings>,
pub game_settings: &'a GameSettings,
pub entity: Entity,
}
fn create_state(transition: FighterStateTransition, state_context: FighterStateContext, commands: &mut Commands) {
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(states::wait::WaitState),
FighterStateTransition::Walk => ent_cmd.insert(states::walk::WalkState),
FighterStateTransition::Dash => ent_cmd.insert(states::dash::DashState::default()),
FighterStateTransition::Run => ent_cmd.insert(states::run::RunState),
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),
};
}
@@ -42,7 +56,7 @@ fn get_state_debug_name(transition: &FighterStateTransition) -> &'static str {
match transition {
FighterStateTransition::Wait => "wait",
FighterStateTransition::Walk => "walk",
FighterStateTransition::Dash => "dash",
FighterStateTransition::Dash(_) => "dash",
FighterStateTransition::Run => "run",
}
}
@@ -62,7 +76,6 @@ pub fn state_interrupt_system<T: FighterState + Component + std::fmt::Debug>(
mut commands: Commands,
) {
for (ent, state, mut input, attribs) in query {
info!("{:?}", state);
let state_context = FighterStateContext {
input: &mut input,
fighter_attribs: &attribs,
@@ -71,7 +84,10 @@ pub fn state_interrupt_system<T: FighterState + Component + std::fmt::Debug>(
};
if let Some(new_state) = state.check_interrupt(&state_context) {
commands.entity(ent).remove::<T>();
commands.entity(ent).insert(StateNameDebug(get_state_debug_name(&new_state)));
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;
}
}
-5
View File
@@ -1,5 +0,0 @@
pub mod wait;
pub mod walk;
pub mod dash;
pub mod run;
pub mod grounded;
-116
View File
@@ -1,116 +0,0 @@
use crate::{
fighter::{
FighterAttributes, FighterVelocity,
state::{FighterState, FighterStateContext, FighterStateTransition},
states::grounded,
},
game_settings::GameSettings,
input::player::FighterInput,
};
use bevy::prelude::*;
pub struct DashStateCreator {}
#[derive(Component, Debug)]
pub struct DashState {
frames_in_dash: u32,
}
impl Default for DashState {
fn default() -> Self {
Self { frames_in_dash: 0 }
}
}
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
super::run::check_input(state_context)
.or_else(|| super::walk::check_input(state_context))
.or_else(|| super::wait::check_input(state_context))
} else if let Some(last_frame) = state_context.input.get_last_frame() {
check_smash_input_with_dir(state_context)
.filter(|dir| *dir != last_frame.movement.x.signum())
.and_then(|_| super::dash::check_input(state_context))
} else {
None
}
}
}
pub fn start(state_context: &mut FighterStateContext) {
state_context.input.clear_buffer();
}
fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option<f32> {
state_context.input.has_smash_x_movement(
state_context
.game_settings
.input_common
.smash_input_reset_axis_threshold,
state_context
.game_settings
.input_common
.smash_input_axis_threshold,
state_context
.game_settings
.input_common
.smash_input_frame_threshold,
)
}
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;
if let Some(last_frame) = input.get_last_frame() {
let (accel, target_vel) = attributes.get_accel_and_target_dashrun(last_frame);
let accel = grounded::compute_ground_accel(
accel,
target_vel,
velocity.x,
attributes,
game_settings.as_ref(),
);
velocity.x += accel;
}
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
check_smash_input_with_dir(state_context).map(|_| FighterStateTransition::Dash)
}
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 {
if let Some(last_frame) = input.get_last_frame() {
velocity.x = attribs.dash_initial_velocity * last_frame.movement.x.signum();
}
input.clear_buffer();
}
}
}
-44
View File
@@ -1,44 +0,0 @@
use crate::{
fighter::{FighterAttributes, FighterVelocity, state::{FighterStateContext, FighterStateTransition}, states::grounded}, game_settings::GameSettings, input::player::FighterInput, state::FighterState,
};
use bevy::prelude::*;
#[derive(Component, Debug)]
pub struct RunState;
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
if let Some(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
}
impl FighterState for RunState {
fn check_interrupt(
&self,
state_context: &crate::fighter::state::FighterStateContext,
) -> Option<crate::fighter::state::FighterStateTransition> {
use super::*;
if let Some(_) = check_input(state_context) {
None
} else {
walk::check_input(state_context).or_else(|| wait::check_input(state_context))
}
}
}
pub fn run_update(
fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With<RunState>>,
game_settings: Res<GameSettings>,
) {
for ( mut velocity, attributes, input) in fighters {
if let Some(last_frame) = input.get_last_frame() {
let (accel, target_vel) = attributes.get_accel_and_target_dashrun(last_frame);
let accel = grounded::compute_ground_accel(accel, target_vel, velocity.x, attributes, game_settings.as_ref());
velocity.x += accel;
}
}
}
-34
View File
@@ -1,34 +0,0 @@
use crate::{fighter::{state::{FighterState, FighterStateContext, FighterStateTransition}, states::{grounded, walk}}, game_settings::GameSettings};
use crate::fighter::*;
#[derive(Component, Debug)]
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> {
if let Some(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 += grounded::apply_grounded_friction(friction, velocity.x);
}
}
-46
View File
@@ -1,46 +0,0 @@
use crate::{
fighter::{
FighterAttributes, FighterVelocity, state::{FighterState, FighterStateContext, FighterStateTransition}, states::{dash, grounded, wait},
}, game_settings::GameSettings, input::player::FighterInput,
};
use bevy::prelude::*;
#[derive(Component, Debug)]
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 walk_update(
fighters: Query<(
&mut FighterVelocity,
&FighterAttributes,
&FighterInput,
), With<WalkState>>,
game_settings: Res<GameSettings>,
) {
for (mut velocity, attributes, input) in fighters {
if let Some(last_frame) = input.get_last_frame() {
let (accel, target_vel) = attributes.get_accel_and_target_walk(last_frame);
let accel = grounded::compute_ground_accel(accel, target_vel, velocity.x, attributes, &game_settings);
velocity.x += accel;
}
}
}
pub fn check_input(state_context: &FighterStateContext) -> Option<FighterStateTransition> {
if let Some(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
}