Initial commit

This commit is contained in:
2026-08-06 18:53:46 +02:00
commit 03cbb1657d
24 changed files with 8773 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
use bevy::prelude::*;
#[derive(Component)]
pub struct FighterAttributes {
pub base_walk_accel: f32,
pub stick_walk_accel: f32,
pub max_walk_vel: f32,
pub ground_friction: f32,
pub dash_duration: u32
}
+100
View File
@@ -0,0 +1,100 @@
use bevy::{
color::palettes::css::{GREEN, HOT_PINK, RED, YELLOW},
ecs::relationship::Relationship,
prelude::*,
};
use crate::{
game_settings::GameSettings,
input::player::{ControlledBy, FighterInput},
stage::StagePlane,
};
#[derive(Component)]
pub struct Grounded;
#[derive(Component, Deref, DerefMut, Default, Debug)]
pub struct FighterVelocity(pub Vec2);
#[derive(Component, Deref, DerefMut, Default)]
pub struct FighterPreviousTranslation(pub Vec2);
#[derive(Component, Deref, DerefMut, Default)]
#[require(FighterPreviousTranslation)]
pub struct FighterTranslation(pub Vec2);
#[derive(Component, Debug)]
#[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]
}
}
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 integrate_gravity(
fighters: Query<(&mut FighterVelocity)>,
time: Res<Time<Fixed>>,
) {
/*for (movement, mut velocity) in fighters {
//velocity.y -= movement.gravity;
}*/
}
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 collide_fighter_with_scene(
fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>,
stage_planes: Query<&StagePlane>,
mut gizmos: Gizmos,
) {
for (ecb, mut velocity, mut translation) in fighters {
for plane in stage_planes {
let dist_sq = ecb.vertical_half * ecb.vertical_half;
let ray = Ray2d::new(translation.0, Dir2::NEG_Y);
let intersection_result = plane
.intersect_ray(ray)
.filter(|result| result.position.distance_squared(translation.0) <= dist_sq);
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);
}
}
}
}
pub fn fighter_movement() {}
+8
View File
@@ -0,0 +1,8 @@
pub mod plugin;
pub mod fighter;
pub mod data;
pub mod state;
pub mod states;
pub use fighter::*;
pub use plugin::*;
pub use data::*;
+29
View File
@@ -0,0 +1,29 @@
use bevy::prelude::*;
use crate::fighter::states;
use crate::fighter::{
FighterECB, FighterTranslation, FighterVelocity, apply_motion, collide_fighter_with_scene, integrate_gravity, state::state_interrupt_system,
};
#[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>,
states::walk::walk_update,
states::dash::dash_update,
integrate_gravity,
apply_motion,
collide_fighter_with_scene,
)
.chain(),
)
.add_observer(states::dash::on_insert_dash);
}
}
+57
View File
@@ -0,0 +1,57 @@
use bevy::{ecs::relationship::RelationshipSourceCollection, prelude::*};
use crate::{fighter::{FighterAttributes, states}, game_settings::GameSettings, input::player::FighterInput};
pub enum FighterStateTransition {
Wait,
Walk,
Dash
}
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 Res<'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(states::wait::WaitState),
FighterStateTransition::Walk => ent_cmd.insert(states::walk::WalkState),
FighterStateTransition::Dash => ent_cmd.insert(states::dash::DashState::default()),
};
}
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 {
info!("{:?}", state);
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>();
create_state(new_state, state_context, &mut commands);
}
}
}
+79
View File
@@ -0,0 +1,79 @@
use crate::{
fighter::state::{FighterState, FighterStateContext, FighterStateTransition},
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::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>) {
for mut dash in dash_query {
dash.frames_in_dash += 1;
}
}
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), With<DashState>>,
) {
for (entity, mut input) in query {
if entity == add.entity {
input.clear_buffer();
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod wait;
pub mod walk;
pub mod dash;
+23
View File
@@ -0,0 +1,23 @@
use bevy::prelude::*;
use crate::fighter::{state::{FighterState, FighterStateContext, FighterStateTransition}, states::walk};
#[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
}
+94
View File
@@ -0,0 +1,94 @@
use crate::{
fighter::{
FighterAttributes, FighterVelocity,
state::{FighterState, FighterStateContext, FighterStateTransition},
states::{dash, 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_mul = 1.0;
let mut accel = last_frame.movement.x * attributes.stick_walk_accel * accel_mul; // stick-proportional
accel += (last_frame.movement.x.signum() * attributes.base_walk_accel) * accel_mul; // constant
let target_vel = last_frame.movement.x * attributes.max_walk_vel * accel_mul;
if target_vel != 0.0 {
let mult = velocity.x / target_vel;
if mult > 0.0 && mult < 1.0 {
accel *= (1.0 - mult) * game_settings.figher_common.walk_speed_ease; // 0.5 — linear ease-out toward target
}
}
// Friction mode
if target_vel == 0.0 {
let mut out_friction = velocity.x.signum() * (-attributes.ground_friction);
// apply friction
if attributes.ground_friction.abs() > velocity.x.abs() {
out_friction = -velocity.x;
}
accel = out_friction;
} else {
let ground_max_horizontal_velocity =
game_settings.figher_common.ground_max_horizontal_velocity;
if !(velocity.x * accel < 0.0) {
// accelerating, not reversing
if accel > 0.0 {
if velocity.x + accel > target_vel {
accel = -attributes.ground_friction;
if velocity.x + accel < target_vel {
accel = target_vel - velocity.x;
}
if velocity.x + accel > ground_max_horizontal_velocity {
accel = ground_max_horizontal_velocity - velocity.x;
}
}
} else if velocity.x + accel < target_vel {
accel = attributes.ground_friction;
if velocity.x + accel > target_vel {
accel = target_vel - velocity.x;
}
if velocity.x + accel < -ground_max_horizontal_velocity {
accel = -ground_max_horizontal_velocity - velocity.x;
}
}
}
}
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
}