From 91c5c4e05f68638045754836290c355a805dbcc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=80lex=20Rom=C3=A0n?= Date: Mon, 24 Aug 2026 18:24:20 +0200 Subject: [PATCH] Initial work on proper character movement --- Cargo.lock | 28 ++++++ Cargo.toml | 2 + src/fighter/animation.rs | 48 +++++++++ src/fighter/attributes.rs | 4 + src/fighter/collision.rs | 44 +++++++- src/fighter/debug.rs | 13 +++ src/fighter/ecb.rs | 26 ++++- src/fighter/mod.rs | 58 ++++++----- src/fighter/motion.rs | 17 +++- src/fighter/state/air.rs | 18 ++++ src/fighter/state/dash.rs | 132 ++++++++++++------------ src/fighter/state/fall.rs | 55 ++++++++++ src/fighter/state/ground.rs | 90 ++++++++++++++++- src/fighter/state/jump.rs | 41 ++++++++ src/fighter/state/mod.rs | 194 ++++++++++++++++++++++++++++-------- src/fighter/state/run.rs | 81 +++++++++------ src/fighter/state/wait.rs | 74 +++++++++----- src/fighter/state/walk.rs | 70 ++++++++----- src/lib.rs | 117 ++++++++++++---------- src/math.rs | 33 ------ src/math/int.rs | 5 + src/math/mod.rs | 3 + src/math/segment.rs | 92 +++++++++++++++++ src/math/vec.rs | 115 +++++++++++++++++++++ src/player.rs | 2 +- src/schedule.rs | 2 + src/stage/debug.rs | 25 +++-- src/stage/line.rs | 137 ++++++++++++++++++++++--- src/stage/mod.rs | 2 +- src/stage/scene.rs | 40 +++++--- 30 files changed, 1221 insertions(+), 347 deletions(-) create mode 100644 src/fighter/animation.rs create mode 100644 src/fighter/state/air.rs create mode 100644 src/fighter/state/fall.rs create mode 100644 src/fighter/state/jump.rs delete mode 100644 src/math.rs create mode 100644 src/math/int.rs create mode 100644 src/math/mod.rs create mode 100644 src/math/segment.rs create mode 100644 src/math/vec.rs diff --git a/Cargo.lock b/Cargo.lock index 8643555..56ab845 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,6 +479,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "az" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be5eb007b7cacc6c660343e96f650fedf4b5a77512399eb952ca6642cf8d13f7" + [[package]] name = "base64" version = "0.22.1" @@ -2055,6 +2061,14 @@ dependencies = [ "thread_local", ] +[[package]] +name = "bevy_wind_waker_shader" +version = "0.6.0" +source = "git+https://github.com/lavaeater/bevy_wind_waker_shader.git#1b017c3ef6a4fbdaff115df9ec5ae4954b2fb6ba" +dependencies = [ + "bevy", +] + [[package]] name = "bevy_window" version = "0.19.0" @@ -3211,6 +3225,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixed" +version = "1.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af2cbf772fa6d1c11358f92ef554cb6b386201210bcf0e91fb7fba8a907fb40" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -3405,8 +3431,10 @@ dependencies = [ "bevy-inspector-egui", "bevy_egui", "bevy_ggrs", + "bevy_wind_waker_shader", "bitfields", "clap", + "fixed", "jackdaw_runtime", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index bf73fac..8c7d84a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,8 @@ bevy_ggrs = { git = "https://github.com/gschup/bevy_ggrs" } serde = "1.0.229" bitfields = "3.0.0" clap = { version = "4.6.6", features = ["derive"] } +bevy_wind_waker_shader = { git = "https://github.com/lavaeater/bevy_wind_waker_shader.git" } +fixed = "1.31.0" # Enable a small amount of optimization in the dev profile. [profile.dev] diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs new file mode 100644 index 0000000..9912e40 --- /dev/null +++ b/src/fighter/animation.rs @@ -0,0 +1,48 @@ +use bevy::{mesh::skinning::SkinnedMesh, prelude::*}; + +use crate::{fighter::{FighterECB, FighterTranslation, FighterVisual}, player::Player}; + +#[derive(Component)] +pub struct FighterAnimations { + pub wait: Handle +} + +#[derive(Component)] +pub struct FighterAnimationPlayerLink(Entity); + +pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) { + for (translation, ecb, mut transform) in query { + let translation = translation.0 + ecb.get_bottom_point(); + transform.translation = Vec3::new(translation.x, translation.y, 0.0); + } +} + +pub fn setup_fighter_animation_player( + mut commands: Commands, + animated: Query>, + parents: Query<&ChildOf>, + fighters: Query>, +) { + for child in &animated { + let Some(parent) = parents + .iter_ancestors(child) + .filter_map(|parent| fighters.get(parent).ok()) + .next() + else { + continue; + }; + commands.entity(parent).insert( + FighterAnimationPlayerLink(child) + ); + } +} + +pub fn apply_animation( + children: Query<&mut Transform, With>, +) { + for mut trf in children { + // Melee model scale is in decimeters + trf.scale = Vec3::splat(0.1); + } + +} \ No newline at end of file diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index 6a9cf86..56bb11c 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -17,7 +17,11 @@ pub struct FighterAttributes { pub stick_dash_accel: f32, pub max_dash_vel: f32, + pub jump_vertical_velocity: f32, + pub ground_friction: f32, + pub terminal_velocity: f32, + pub gravity: f32, } impl FighterAttributes { diff --git a/src/fighter/collision.rs b/src/fighter/collision.rs index 04f6819..fb10e2b 100644 --- a/src/fighter/collision.rs +++ b/src/fighter/collision.rs @@ -2,12 +2,15 @@ use bevy::{color::palettes::css::HOT_PINK, prelude::*}; -use crate::fighter::{FighterECB, FighterTranslation, FighterVelocity}; -use crate::stage::StageLine; - +use crate::fighter::ecb::FighterPreviousECB; +use crate::fighter::state::FighterStateContext; +use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; +use crate::stage::StagePoly; +use crate::stage::line::{StageCollision, StageLineID}; +/* pub fn collide_fighter_with_scene( fighters: Query<(&FighterECB, &mut FighterVelocity, &mut FighterTranslation)>, - stage_planes: Query<&StageLine>, + stage_planes: Query<&StagePoly>, mut gizmos: Gizmos, ) { for (ecb, mut velocity, mut translation) in fighters { @@ -29,3 +32,36 @@ pub fn collide_fighter_with_scene( } } } +*/ + +pub struct AirCollisionWithStageResult { + pub line_id: StageLineID, + pub hit_position: Vec2, + pub hit_normal: Vec2, +} + +pub fn air_collide_with_stage( + state_context: &FighterStateContext, +) -> Option { + let ray_segment = Segment2d::new( + state_context.prev_translation.0 + state_context.prev_ecb.get_bottom_point(), + state_context.translation.0 + state_context.ecb.get_bottom_point(), + ); + + for (poly_idx, poly) in state_context.stage_collision.stage_polys.iter().enumerate() { + if let Some(intersect_result) = poly.intersect_ray(ray_segment) { + return Some(AirCollisionWithStageResult { + line_id: { + StageLineID { + polygon: poly_idx, + segment: intersect_result.segment_idx, + } + }, + hit_position: intersect_result.position, + hit_normal: intersect_result.normal, + }); + } + } + + None +} diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index e82f11a..04541a0 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -28,3 +28,16 @@ pub fn fighter_debug( Ok(()) } + +pub fn update_config( + mut config_store: ResMut, + keyboard: Res>, + real_time: Res>, + mut virtual_time: ResMut>, +) { + if keyboard.just_pressed(KeyCode::KeyT) { + for (_, config, _) in config_store.iter_mut() { + config.depth_bias = if config.depth_bias == 0. { -1. } else { 0. }; + } + } +} \ No newline at end of file diff --git a/src/fighter/ecb.rs b/src/fighter/ecb.rs index 77842a6..4349a5e 100644 --- a/src/fighter/ecb.rs +++ b/src/fighter/ecb.rs @@ -4,12 +4,18 @@ use bevy::prelude::*; /// the stage. Purely geometry — the collision response lives in /// [`super::collision`]. #[derive(Component, Debug, Clone, Copy)] -#[require(Transform)] +#[require(FighterPreviousECB)] pub struct FighterECB { pub vertical_half: f32, pub horizontal_half: f32, } +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct FighterPreviousECB { + 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)); @@ -18,4 +24,22 @@ impl FighterECB { 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 get_bottom_point(&self) -> Vec2 { + Vec2::new(0.0, -self.vertical_half) + } +} + +impl FighterPreviousECB { + 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 get_bottom_point(&self) -> Vec2 { + Vec2::new(0.0, -self.vertical_half) + } } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 79a3bf4..9179205 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -5,6 +5,7 @@ use bevy::prelude::*; use bevy_egui::EguiPrimaryContextPass; use bevy_ggrs::prelude::*; +pub mod animation; pub mod attributes; pub mod collision; pub mod debug; @@ -20,7 +21,7 @@ pub use attributes::FighterAttributes; pub use ecb::FighterECB; pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded}; -use crate::schedule::GameplaySet; +use crate::{fighter::state::FighterState, schedule::GameplaySet, stage::line::StageCollision}; use state::state_interrupt_system; /// Owns fighter simulation, the rollback registrations for fighter @@ -28,43 +29,47 @@ use state::state_interrupt_system; #[derive(Default)] pub struct FighterPlugin; +#[derive(Component, Copy, Clone, Hash)] +pub enum FighterFacingDirection { + Left, + Right +} + +#[derive(Component)] +#[require(Transform)] +pub struct FighterVisual; + 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_interrupt_system::, - state_interrupt_system::, - state_interrupt_system::, - ) - .chain() - .in_set(GameplaySet::Interrupt), + (animation::setup_fighter_animation_player, state::state_interrupt_system,).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), + (state::state_update_system,).in_set(GameplaySet::StateUpdate), ) - .add_systems( + /*.add_systems( GgrsSchedule, - (motion::integrate_gravity, motion::apply_motion) + (motion::integrate_gravity, motion::apply_grounded_motion) .chain() .in_set(GameplaySet::Physics), + )*/ + .add_systems( + GgrsSchedule, + state::state_collision_interrupt_system.in_set(GameplaySet::Collision), ) .add_systems( GgrsSchedule, - collision::collide_fighter_with_scene.in_set(GameplaySet::Collision), + ( + animation::apply_fighter_translation_to_visuals, + animation::apply_animation, + ) + .chain() + .in_set(GameplaySet::Animation), ) - .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. @@ -74,13 +79,12 @@ impl Plugin for FighterPlugin { .rollback_component_with_copy::() .rollback_component_with_copy::() .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .rollback_component_with_copy::() - .checksum_component::(state::dash::hash_dash_state) + .rollback_resource_with_reflect::() // Debug views. .add_systems(FixedPostUpdate, debug::debug_draw_ecb) - .add_systems(EguiPrimaryContextPass, debug::fighter_debug); + .add_systems(EguiPrimaryContextPass, debug::fighter_debug) + .add_systems(EguiPrimaryContextPass, debug::update_config) + .rollback_component_with_clone::() + .checksum_component_with_hash::(); } } diff --git a/src/fighter/motion.rs b/src/fighter/motion.rs index 78ae375..7fb4512 100644 --- a/src/fighter/motion.rs +++ b/src/fighter/motion.rs @@ -3,6 +3,8 @@ use bevy::prelude::*; +use crate::fighter::FighterAttributes; + #[derive(Component, Clone, Copy)] pub struct Grounded; @@ -16,20 +18,27 @@ pub struct FighterPreviousTranslation(pub Vec2); #[require(FighterPreviousTranslation)] pub struct FighterTranslation(pub Vec2); -pub fn integrate_gravity(_fighters: Query<&mut FighterVelocity>, _time: Res>) {} - -pub fn apply_motion( +pub fn apply_air_motion( fighters: Query<( &FighterVelocity, &mut FighterTranslation, &mut FighterPreviousTranslation, + &mut Transform, )>, _time: Res>, ) { - for (velocity, mut translation, mut prev_translation) in fighters { + for (velocity, mut translation, mut prev_translation, mut transform) in fighters { prev_translation.0 = translation.0; translation.0 += velocity.0; } } +pub fn copy_fighter_transform_to_visuals( + fighters: Query<(&FighterTranslation, &mut Transform)>, +) { + for (translation, mut transform) in fighters { + transform.translation = Vec3::new(translation.x, translation.y, 0.0); + } +} + pub fn fighter_movement() {} diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs new file mode 100644 index 0000000..f09ffca --- /dev/null +++ b/src/fighter/state/air.rs @@ -0,0 +1,18 @@ +use bevy::prelude::*; + +use crate::fighter::{*}; + +pub fn integrate_gravity(velocity: &mut FighterVelocity, attributes: &FighterAttributes) { + velocity.y -= attributes.gravity; + velocity.y = velocity.y.max(-attributes.terminal_velocity); +} + +pub fn apply_air_motion( + velocity: &FighterVelocity, + translation: &mut FighterTranslation, + prev_translation: &mut FighterPreviousTranslation, +) { + // Vertical velocity should be 0 on the ground + prev_translation.0 = translation.0; + translation.0 += velocity.0; +} \ No newline at end of file diff --git a/src/fighter/state/dash.rs b/src/fighter/state/dash.rs index 9786870..e1467b5 100644 --- a/src/fighter/state/dash.rs +++ b/src/fighter/state/dash.rs @@ -2,58 +2,100 @@ use std::hash::{Hash, Hasher}; use bevy::prelude::*; -use super::{FighterState, FighterStateContext, FighterStateTransition, ground, run, wait, walk}; -use crate::fighter::{FighterAttributes, FighterVelocity}; +use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, run, wait, walk}; +use crate::fighter::state::ground::GroundedStateCommon; +use crate::fighter::state::wait::WaitState; +use crate::fighter::state::{FighterState}; +use crate::fighter::{FighterAttributes, FighterVelocity, collision}; use crate::game_settings::GameSettings; use crate::input::{ FighterCommands::{SmashMoveLeft, SmashMoveRight}, FighterInput, }; -pub struct DashStateCreator {} - -#[derive(Component, Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct DashState { frames_in_dash: u32, direction: f32, + ground_common: GroundedStateCommon } -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 std::hash::Hash for DashState { + fn hash(&self, state: &mut H) { + self.direction.to_bits().hash(state); + self.frames_in_dash.hash(state); + } } impl DashState { - pub fn new(direction: f32) -> Self { + pub fn new(direction: f32, ground_common: GroundedStateCommon) -> Self { Self { frames_in_dash: 0, direction, + ground_common } } } -impl FighterState for DashState { +impl FighterStateImpl for DashState { + const NAME: &'static str = "Dash"; fn check_interrupt( &self, state_context: &FighterStateContext, - ) -> Option { + ) -> Option { 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)) + run::check_input(state_context, &self.ground_common) + .or_else(|| walk::check_input(state_context, &self.ground_common)) + .or_else(|| wait::check_input(state_context, &self.ground_common)) } else { check_smash_input_with_dir(state_context) .filter(|dir| *dir != self.direction) - .and_then(|dir| Some(FighterStateTransition::Dash(dir))) + .and_then(|dir| Some(FighterState::Dash(DashState::new(dir, self.ground_common.clone())))) } } -} + + fn on_enter(&mut self, state_context: &mut FighterStateContext) { + state_context.input.clear_buffer(); + state_context.velocity.x = state_context.fighter_attribs.dash_initial_velocity * self.direction; + } + + fn update(&mut self, state_context: &mut FighterStateContext) { + self.frames_in_dash += 1; -pub fn start(state_context: &mut FighterStateContext) { - state_context.input.clear_buffer(); + let last_frame = state_context.input.get_last_frame(); + let (accel, target_vel) = state_context.fighter_attribs.get_accel_and_target_dashrun(&last_frame); + let accel = ground::compute_ground_accel( + accel, + target_vel, + state_context.velocity.x, + state_context.fighter_attribs, + state_context.game_settings, + ); + state_context.velocity.x += accel; + + ground::apply_grounded_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let Some(res) = collision::air_collide_with_stage(state_context) { + // Adjust translation + state_context.translation.0 = res.hit_position - state_context.ecb.get_bottom_point(); + let grounded_common = GroundedStateCommon { + current_line_id: res.line_id + }; + Some(FighterState::Wait(WaitState { grounded_common } )) + } else { + None + } + } } fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { @@ -66,51 +108,7 @@ fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option Option { - 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, -) { - 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, - query: Query< - ( - Entity, - &mut FighterInput, - &FighterAttributes, - &mut FighterVelocity, - ), - With, - >, -) { - 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(); - } - } +pub fn check_input(state_context: &FighterStateContext, ground_common: &GroundedStateCommon) -> Option { + check_smash_input_with_dir(state_context).map(|dir| FighterState::Dash(DashState::new(dir, ground_common.clone()))); + None } diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs new file mode 100644 index 0000000..422aa0e --- /dev/null +++ b/src/fighter/state/fall.rs @@ -0,0 +1,55 @@ +use bevy::prelude::*; + +use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, walk}; +use crate::fighter::state::{FighterState, air}; +use crate::fighter::state::ground::GroundedStateCommon; +use crate::fighter::state::wait::WaitState; +use crate::fighter::{FighterAttributes, FighterVelocity, collision, motion}; +use crate::game_settings::GameSettings; +use crate::stage::line::StageCollision; + +#[derive(Debug, Clone, Copy, Hash)] +pub struct FallState; + +impl FighterStateImpl for FallState { + const NAME: &'static str = "Fall"; + fn check_interrupt( + &self, + state_context: &FighterStateContext, + ) -> Option { + None + } + + fn on_enter(&mut self, _state_context: &mut super::FighterStateContext) {} + + fn update(&mut self, state_context: &mut super::FighterStateContext) { + air::integrate_gravity(state_context.velocity, state_context.fighter_attribs); + air::apply_air_motion(state_context.velocity, state_context.translation, state_context.prev_translation); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let Some(res) = collision::air_collide_with_stage(state_context) { + // Adjust translation + state_context.translation.0 = res.hit_position - state_context.ecb.get_bottom_point(); + let grounded_common = GroundedStateCommon { + current_line_id: res.line_id + }; + Some(FighterState::Wait(WaitState { grounded_common } )) + } else { + None + } + } +} + +pub fn check_input(state_context: &FighterStateContext) -> 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::Fall(FallState)); + } + None +} + diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index d1cfeff..d2fb2d2 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -2,8 +2,20 @@ //! 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::fighter::state::FighterStateContext; +use crate::fighter::{ + FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity, +}; use crate::game_settings::GameSettings; +use crate::stage::StagePoly; +use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType}; + +use bevy::prelude::*; + +#[derive(Reflect, Hash, Clone, Debug)] +pub struct GroundedStateCommon { + pub current_line_id: StageLineID, +} pub fn apply_grounded_friction(friction: f32, ground_vel: f32) -> f32 { if friction.abs() > ground_vel.abs() { @@ -53,3 +65,79 @@ pub fn compute_ground_accel( accel } } + +pub enum GroundedMotionResult { + InGround, + InAir, +} + +pub fn apply_grounded_motion( + velocity: &FighterVelocity, + translation: &mut FighterTranslation, + prev_translation: &mut FighterPreviousTranslation, +) { + // Vertical velocity should be 0 on the ground + prev_translation.0 = translation.0; + translation.0 += Vec2::new(velocity.0.x ,0.0); +} + +pub fn collide_with_stage_grounded( + state_context: &mut FighterStateContext, + 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); + + if let Some(stage_poly) = stage_poly { + let ecb_bottom_pos = translation.0 + ecb.get_bottom_point(); + let left_segment_idx = ground_common + .current_line_id + .segment + .checked_sub(1) + .unwrap_or(0); + let right_segment_idx = + (ground_common.current_line_id.segment + 1) % stage_poly.segments.len(); + + // First, we check the current segment, if that fails, we check the surrounding ones + let grounded_ecb_bottom_pos = [ + ground_common.current_line_id.segment.clone(), + left_segment_idx, + right_segment_idx, + ] + .iter() + .find_map(|segment_idx| { + let segment = stage_poly.get_segment(*segment_idx)?; + if segment.segment_type != StagePolyLineSegmentType::Floor { + return None; + } + + let line_dir = segment.segment.direction(); + let projected_point = segment.segment.point1() + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir.as_vec2()); + let closest_point = segment.segment.closest_point(ecb_bottom_pos); + let distance = closest_point.distance(projected_point); + + if distance < 0.01 { + return Some((projected_point, *segment_idx, segment.normal)); + } + + None + }); + + if let Some((bp, new_segment_idx, normal)) = grounded_ecb_bottom_pos { + translation.0 = bp - ecb.get_bottom_point(); + state_context.velocity.0 = state_context.velocity.0 - state_context.velocity.0.project_onto_normalized(normal); + ground_common.current_line_id.segment = new_segment_idx; + GroundedMotionResult::InGround + } else { + GroundedMotionResult::InAir + } + } else { + GroundedMotionResult::InAir + } +} diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs new file mode 100644 index 0000000..2075985 --- /dev/null +++ b/src/fighter/state/jump.rs @@ -0,0 +1,41 @@ +use bevy::prelude::*; + +use crate::fighter::state::{FighterState, FighterStateImpl}; + +pub struct JumpState { + pub counter: u32 +} + +pub struct JumpSquatState { + pub counter: u32 +} + + +impl FighterStateImpl for JumpSquatState { + const NAME: &'static str = "JumpSquat"; + fn check_interrupt( + &self, + state_context: &super::FighterStateContext, + ) -> Option { + None + } + + fn update(&mut self, _state_context: &mut super::FighterStateContext) { + todo!() + } + +} + +impl FighterStateImpl for JumpState { + const NAME: &'static str = "Jump"; + fn check_interrupt( + &self, + state_context: &super::FighterStateContext, + ) -> Option { + None + } + + fn update(&mut self, _state_context: &mut super::FighterStateContext) { + todo!() + } +} \ No newline at end of file diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index f607078..0361e89 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -2,65 +2,105 @@ //! implements, the context they inspect, and the generic system that runs the //! interrupt check. One state per submodule. +use bevy::ecs::component::Mutable; +use bevy::ecs::system::ScheduleSystem; use bevy::prelude::*; +use bevy_ggrs::{GgrsSchedule, RollbackApp}; -use crate::fighter::FighterAttributes; +use crate::fighter::ecb::FighterPreviousECB; +use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; use crate::game_settings::GameSettings; use crate::input::FighterInput; +use crate::schedule::GameplaySet; +use crate::stage::StagePoly; +use crate::stage::line::StageCollision; pub mod dash; pub mod ground; pub mod run; pub mod wait; pub mod walk; +pub mod fall; +pub mod air; +pub mod jump; pub enum FighterStateTransition { Wait, Walk, Dash(f32), Run, + Fall, } -pub trait FighterState: Component + Sized { - fn remove(&self, state_context: &FighterStateContext, commands: &mut Commands) { - commands.entity(state_context.entity).remove::(); - } - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option; -} +macro_rules! fighter_states { + ($($variant:ident => $ty:ty),* $(,)?) => { + #[derive(Component, Clone, Debug, Hash)] + pub enum FighterState { $($variant($ty)),* } -pub struct FighterStateContext<'a> { - pub input: &'a mut FighterInput, - pub fighter_attribs: &'a FighterAttributes, - pub game_settings: &'a GameSettings, - pub entity: Entity, -} + impl FighterState { + pub fn name(&self) -> &'static str { + match self { $(Self::$variant(_) => <$ty>::NAME),* } + } + pub fn check_interrupt(&self, ctx: &FighterStateContext) -> Option { + match self { $(Self::$variant(s) => s.check_interrupt(ctx)),* } + } + pub fn update(&mut self, ctx: &mut FighterStateContext) { + match self { $(Self::$variant(s) => s.update(ctx)),* } + } + pub fn on_enter(&mut self, ctx: &mut FighterStateContext) { + match self { $(Self::$variant(s) => s.on_enter(ctx)),* } + } -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), + pub fn check_collision_interrupt(&mut self, ctx: &mut FighterStateContext) -> Option { + match self { $(Self::$variant(s) => s.check_collision_interrupt(ctx)),* } + } + } + + $(impl From<$ty> for FighterState { + fn from(s: $ty) -> Self { Self::$variant(s) } + })* }; } -fn get_state_debug_name(transition: &FighterStateTransition) -> &'static str { - match transition { - FighterStateTransition::Wait => "wait", - FighterStateTransition::Walk => "walk", - FighterStateTransition::Dash(_) => "dash", - FighterStateTransition::Run => "run", +pub trait FighterStateImpl: Sized { + const NAME: &'static str; + fn check_interrupt( + &self, + state_context: &FighterStateContext, + ) -> Option; + + fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} + fn update(&mut self, _state_context: &mut FighterStateContext); + + fn check_collision_interrupt( + &mut self, + _state_context: &mut FighterStateContext, + ) -> Option { + None } } +pub struct FighterStateContext<'a> { + pub translation: &'a mut FighterTranslation, + pub prev_translation: &'a mut FighterPreviousTranslation, + pub input: &'a mut FighterInput, + pub velocity: &'a mut FighterVelocity, + pub ecb: &'a mut FighterECB, + pub prev_ecb: &'a mut FighterPreviousECB, + pub fighter_attribs: &'a FighterAttributes, + pub game_settings: &'a GameSettings, + pub entity: Entity, + pub stage_collision: &'a StageCollision, +} + +fighter_states! { + Wait => wait::WaitState, + Walk => walk::WalkState, + Dash => dash::DashState, + Run => run::RunState, + Fall => fall::FallState +} + #[derive(Component)] pub struct StateNameDebug(pub &'static str); @@ -70,25 +110,91 @@ impl std::fmt::Display for StateNameDebug { } } -pub fn state_interrupt_system( - query: Query<(Entity, &T, &mut FighterInput, &FighterAttributes)>, +pub fn state_update_system( + query: Query<(Entity, &mut FighterState, &mut FighterTranslation, &mut FighterPreviousTranslation, &mut FighterInput, &mut FighterVelocity, &mut FighterECB, &mut FighterPreviousECB, &FighterAttributes)>, + stage_collision: Res, game_settings: Res, - mut commands: Commands, + mut gizmos: Gizmos, ) { - for (ent, state, mut input, attribs) in query { - let state_context = FighterStateContext { + for (ent, mut state, mut translation, mut prev_translation, mut input, mut velocity, mut ecb, mut prev_ecb, attribs) in query { + let mut update_ctx = FighterStateContext { + translation: &mut translation, + prev_translation: &mut prev_translation, input: &mut input, + velocity: &mut velocity, + ecb: &mut ecb, + prev_ecb: &mut prev_ecb, fighter_attribs: &attribs, game_settings: &game_settings, entity: ent, + stage_collision: &stage_collision, + }; + state.update(&mut update_ctx); + } +} + +pub fn state_interrupt_system ( + query: Query<(Entity, &FighterState, &mut FighterTranslation, &mut FighterPreviousTranslation, &mut FighterInput, &mut FighterVelocity, &mut FighterECB, &mut FighterPreviousECB, &FighterAttributes)>, + stage_collision: Res, + game_settings: Res, + mut commands: Commands, +) { + for (ent, state, mut translation, mut prev_translation, mut input, mut velocity, mut ecb, mut prev_ecb, attribs) in query { + let mut state_context = FighterStateContext { + translation: &mut translation, + prev_translation: &mut prev_translation, + input: &mut input, + velocity: &mut velocity, + ecb: &mut ecb, + prev_ecb: &mut prev_ecb, + fighter_attribs: &attribs, + game_settings: &game_settings, + entity: ent, + stage_collision: &stage_collision }; if let Some(new_state) = state.check_interrupt(&state_context) { - commands.entity(ent).remove::(); - 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); + let mut new_state = new_state; + let old_state_name = state.name(); + let new_state_name = new_state.name(); + new_state.on_enter(&mut state_context); + commands.entity(ent).insert(( + new_state, + StateNameDebug(new_state_name) + )); + info!("State {} -> {}", old_state_name, new_state_name); } } } + +pub fn state_collision_interrupt_system( + query: Query<(Entity, &mut FighterState, &mut FighterTranslation, &mut FighterPreviousTranslation, &mut FighterInput, &mut FighterVelocity, &mut FighterECB, &mut FighterPreviousECB, &FighterAttributes)>, + stage_collision: Res, + game_settings: Res, + mut commands: Commands, +) { + for (ent, mut state, mut translation, mut prev_translation, mut input, mut velocity, mut ecb, mut prev_ecb, attribs) in query { + let mut state_context = FighterStateContext { + translation: &mut translation, + prev_translation: &mut prev_translation, + input: &mut input, + velocity: &mut velocity, + ecb: &mut ecb, + prev_ecb: &mut prev_ecb, + fighter_attribs: &attribs, + game_settings: &game_settings, + entity: ent, + stage_collision: &stage_collision + }; + if let Some(new_state) = state.check_collision_interrupt(&mut state_context) { + let mut new_state = new_state; + let old_state_name = state.name(); + let new_state_name = new_state.name(); + new_state.on_enter(&mut state_context); + commands.entity(ent).insert(( + new_state, + StateNameDebug(new_state_name) + )); + info!("State {} -> {}", old_state_name, new_state_name); + } + } +} \ No newline at end of file diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs index 244ffec..917c57e 100644 --- a/src/fighter/state/run.rs +++ b/src/fighter/state/run.rs @@ -1,49 +1,74 @@ use bevy::prelude::*; -use super::{FighterState, FighterStateContext, FighterStateTransition, ground, wait, walk}; +use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, wait, walk}; +use crate::fighter::state::FighterState; +use crate::fighter::state::fall::FallState; +use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::{FighterAttributes, FighterVelocity}; use crate::game_settings::GameSettings; use crate::input::FighterInput; -#[derive(Component, Debug, Clone, Copy)] -pub struct RunState; +#[derive(Component, Debug, Clone, Hash)] +pub struct RunState { + pub grounded_common: GroundedStateCommon +} -impl FighterState for RunState { +impl FighterStateImpl for RunState { + const NAME: &'static str = "Run"; fn check_interrupt( &self, state_context: &FighterStateContext, - ) -> Option { - if let Some(_) = check_input(state_context) { + ) -> 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 { None } else { - walk::check_input(state_context).or_else(|| wait::check_input(state_context)) + walk::check_input(state_context, &self.grounded_common).or_else(|| wait::check_input(state_context, &self.grounded_common)) + } + } + + fn on_enter(&mut self, _state_context: &mut super::FighterStateContext) {} + + fn update(&mut self, state_context: &mut super::FighterStateContext) { + let last_frame = state_context.input.get_last_frame(); + let (accel, target_vel) = state_context.fighter_attribs.get_accel_and_target_dashrun(&last_frame); + let accel = ground::compute_ground_accel( + accel, + target_vel, + state_context.velocity.x, + state_context.fighter_attribs, + state_context.game_settings, + ); + state_context.velocity.x += accel; + + ground::apply_grounded_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let GroundedMotionResult::InAir = ground::collide_with_stage_grounded( + state_context, + &mut self.grounded_common, + false + ) { + Some(FighterState::Fall(FallState)) + } else { + None } } } -pub fn check_input(state_context: &FighterStateContext) -> Option { +pub fn check_input(state_context: &FighterStateContext, ground_state_common: &GroundedStateCommon) -> 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(FighterStateTransition::Run); + return Some(FighterState::Run(RunState { grounded_common: ground_state_common.clone() })); } None -} - -pub fn run_update( - fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With>, - game_settings: Res, -) { - 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; - } -} +} \ No newline at end of file diff --git a/src/fighter/state/wait.rs b/src/fighter/state/wait.rs index abdafa6..433365b 100644 --- a/src/fighter/state/wait.rs +++ b/src/fighter/state/wait.rs @@ -1,43 +1,69 @@ use bevy::prelude::*; -use super::{FighterState, FighterStateContext, FighterStateTransition, ground, walk}; +use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, ground, walk}; +use crate::fighter::state::FighterState; +use crate::fighter::state::fall::FallState; +use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::{FighterAttributes, FighterVelocity}; use crate::game_settings::GameSettings; +use crate::stage::line::StageLineID; -#[derive(Component, Debug, Clone, Copy)] -pub struct WaitState; +#[derive(Debug, Clone, Hash)] +pub struct WaitState { + pub grounded_common: GroundedStateCommon +} -impl FighterState for WaitState { +impl FighterStateImpl for WaitState { + const NAME: &'static str = "Wait"; fn check_interrupt( &self, state_context: &FighterStateContext, - ) -> Option { - walk::check_input(state_context) + ) -> Option { + walk::check_input(state_context, &self.grounded_common) + } + + fn on_enter(&mut self, _state_context: &mut super::FighterStateContext) {} + + fn update(&mut self, state_context: &mut super::FighterStateContext) { + let friction = if state_context.velocity.x.abs() > state_context.fighter_attribs.max_walk_vel { + state_context.fighter_attribs.ground_friction + * state_context.game_settings + .figher_common + .ground_friction_over_walk_speed_multiplier + } else { + state_context.fighter_attribs.ground_friction + }; + state_context.velocity.x += ground::apply_grounded_friction(friction, state_context.velocity.x); + + ground::apply_grounded_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let GroundedMotionResult::InAir = ground::collide_with_stage_grounded( + state_context, + &mut self.grounded_common, + false + ) { + Some(FighterState::Fall(FallState)) + } else { + None + } } } -pub fn check_input(state_context: &FighterStateContext) -> Option { +pub fn check_input(state_context: &FighterStateContext, ground_state_common: &GroundedStateCommon) -> 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(FighterStateTransition::Wait); + return Some(FighterState::Wait(WaitState { grounded_common: ground_state_common.clone() })); } None } -pub fn wait_update( - query: Query<(&FighterAttributes, &mut FighterVelocity), With>, - game_settings: Res, -) { - 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); - } -} diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index 09a48df..0239468 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -1,43 +1,67 @@ use bevy::prelude::*; -use super::{FighterState, FighterStateContext, FighterStateTransition, dash, ground, wait}; +use super::{FighterStateImpl, FighterStateContext, FighterStateTransition, dash, ground, wait}; +use crate::fighter::state::fall::FallState; +use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; +use crate::fighter::state::{self, FighterState}; use crate::fighter::{FighterAttributes, FighterVelocity}; use crate::game_settings::GameSettings; use crate::input::FighterInput; -#[derive(Component, Debug, Clone, Copy)] -pub struct WalkState; +#[derive(Debug, Clone, Hash)] +pub struct WalkState { + pub grounded_common: GroundedStateCommon +} -impl FighterState for WalkState { +impl FighterStateImpl for WalkState { + const NAME: &'static str = "Walk"; fn check_interrupt( &self, state_context: &FighterStateContext, - ) -> Option { - wait::check_input(state_context).or_else(|| dash::check_input(state_context)) + ) -> Option { + wait::check_input(state_context, &self.grounded_common).or_else(|| dash::check_input(state_context, &self.grounded_common)) + } + + fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} + + fn update(&mut self, state_context: &mut FighterStateContext) { + let last_frame = state_context.input.get_last_frame(); + let (accel, target_vel) = state_context.fighter_attribs.get_accel_and_target_walk(&last_frame); + + let accel = + ground::compute_ground_accel(accel, target_vel, state_context.velocity.x, state_context.fighter_attribs, &state_context.game_settings); + + state_context.velocity.x += accel; + + ground::apply_grounded_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); + } + + fn check_collision_interrupt( + &mut self, + state_context: &mut FighterStateContext, + ) -> Option { + if let GroundedMotionResult::InAir = ground::collide_with_stage_grounded( + state_context, + &mut self.grounded_common, + false + ) { + Some(FighterState::Fall(FallState)) + } else { + None + } } } -pub fn check_input(state_context: &FighterStateContext) -> Option { +pub fn check_input(state_context: &FighterStateContext, ground_state_common: &GroundedStateCommon) -> 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(FighterStateTransition::Walk); + return Some(FighterState::Walk(WalkState { grounded_common: ground_state_common.clone() })); } None } - -pub fn walk_update( - fighters: Query<(&mut FighterVelocity, &FighterAttributes, &FighterInput), With>, - game_settings: Res, -) { - 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; - } -} diff --git a/src/lib.rs b/src/lib.rs index 3466281..28e2e97 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,30 +21,26 @@ //! save, click Rebuild in jackdaw (or run `jd build`) and it //! appears in `Add Component`. No registration code is needed. -use bevy::prelude::*; +use bevy::{mesh::SphereMeshBuilder, prelude::*}; +use bevy_wind_waker_shader::prelude::*; use clap::Parser; use jackdaw_runtime::prelude::*; use crate::{ fighter::{ - FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, - state::wait::WaitState, - }, - game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, - input::FighterInput, - player::Player, - schedule::GameplaySchedulePlugin, + FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::FighterAnimations, state::{FighterState, fall::FallState, wait::WaitState}, + }, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, input::FighterInput, player::Player, schedule::GameplaySchedulePlugin, }; +mod args; pub mod fighter; -pub mod input; -pub mod stage; pub mod game_settings; +pub mod input; +mod math; pub mod netcode; pub mod player; pub mod schedule; -mod math; -mod args; +pub mod stage; /// Your game's Bevy plugin. The editor finds it by this name (override /// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the @@ -52,34 +48,52 @@ mod args; #[derive(Default)] pub struct GamePlugin; -pub fn setup(mut commands: Commands) { - let _fighter = commands - .spawn(( - Name("Fighter".into()), - Player { - handle: 0, - }, - FighterECB { - vertical_half: 0.5, - horizontal_half: 0.25, - }, - FighterVelocity::default(), - FighterTranslation(Vec2::new(0.5, 1.25)), - FighterAttributes { - base_walk_accel: 0.01, - stick_walk_accel: 0.02, - max_walk_vel: 0.16, - ground_friction: 0.008, - dash_duration: 15, - base_dash_accel: 0.002, - stick_dash_accel: 0.01, - max_dash_vel: 0.22, - dash_initial_velocity: 0.19, - }, - WaitState {}, - FighterInput::default() - )) - .id(); +pub fn setup(mut commands: Commands, asset_server: Res) { + // debug stage + + let _stage = commands.spawn(( + WorldAssetRoot(asset_server.load("stages/YoshiStory.glb#Scene0")), + Transform::from_scale(Vec3::splat(0.1)), + )); + + let _fighter = + commands + .spawn(( + Name("Fighter".into()), + Player { handle: 0 }, + FighterECB { + vertical_half: 0.5, + horizontal_half: 0.25, + }, + FighterVelocity::default(), + FighterTranslation(Vec2::new(0.5, 1.25)), + FighterAttributes { + base_walk_accel: 0.01, + stick_walk_accel: 0.02, + max_walk_vel: 0.16, + ground_friction: 0.008, + dash_duration: 15, + base_dash_accel: 0.002, + stick_dash_accel: 0.01, + max_dash_vel: 0.22, + jump_vertical_velocity: 0.25, + dash_initial_velocity: 0.19, + terminal_velocity: 0.28, + gravity: 0.023, + }, + FighterState::Fall(FallState), + FighterInput::default(), + FighterAnimations { + wait: asset_server.load("fighters/jiggs/jigglypuff-normal.glb#PlyPurin5K_Share_ACTION_Wait") + } + )) + .with_child(( + FighterVisual, + WorldAssetRoot(asset_server.load( + GltfAssetLabel::Scene(0).from_asset("fighters/jiggs/jigglypuff-normal.glb"), + )), + )) + .id(); } impl Plugin for GamePlugin { @@ -88,6 +102,7 @@ impl Plugin for GamePlugin { // resources; this plugin only wires them together and supplies the // game-level content and configuration. app.add_plugins(( + WindWakerShaderPlugin::default(), // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. netcode::FighterNetcodePlugin::default(), GameplaySchedulePlugin, @@ -95,17 +110,17 @@ impl Plugin for GamePlugin { fighter::FighterPlugin, stage::StagePlugin, )) - .add_systems(Startup, setup) - .add_systems(Update, spin_cubes) - .insert_resource(args::Args::parse()) - .insert_resource(GameSettings { - figher_common: FighterSettingsCommon { - walk_speed_ease: 0.5, - ground_max_horizontal_velocity: 0.3, - ground_friction_over_walk_speed_multiplier: 2.0 - }, - input_common: InputSettingsCommon::default(), - }); + .add_systems(Startup, setup) + .add_systems(Update, spin_cubes) + .insert_resource(args::Args::parse()) + .insert_resource(GameSettings { + figher_common: FighterSettingsCommon { + walk_speed_ease: 0.5, + ground_max_horizontal_velocity: 0.3, + ground_friction_over_walk_speed_multiplier: 2.0, + }, + input_common: InputSettingsCommon::default(), + }); } } diff --git a/src/math.rs b/src/math.rs deleted file mode 100644 index 5ba5423..0000000 --- a/src/math.rs +++ /dev/null @@ -1,33 +0,0 @@ -use bevy::math::{Vec2, primitives::Segment2d}; - -pub trait SegmentIntersection { - fn segment_intersection(&self, rhs: &Segment2d) -> Option; -} - -impl SegmentIntersection for Segment2d { - fn segment_intersection(&self, rhs: &Segment2d) -> Option { - let p0 = self.point1(); - let p1 = self.point2(); - let p2 = rhs.point1(); - let p3 = rhs.point2(); - - let s1_x = p1.x - p0.x; - let s1_y = p1.y - p0.y; - let s2_x = p3.x - p2.x; - let s2_y = p3.y - p2.y; - - let denom = -s2_x * s1_y + s1_x * s2_y; - if denom.abs() < f32::EPSILON { - return None; // Parallel or collinear - } - - let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; - let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; - - if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { - Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) - } else { - None - } - } -} \ No newline at end of file diff --git a/src/math/int.rs b/src/math/int.rs new file mode 100644 index 0000000..a6061a6 --- /dev/null +++ b/src/math/int.rs @@ -0,0 +1,5 @@ +use bevy::math::{Vec2, primitives::Segment2d}; + +use fixed::prelude::*; + +pub type FGi32 = fixed::FixedI32; diff --git a/src/math/mod.rs b/src/math/mod.rs new file mode 100644 index 0000000..b4886ff --- /dev/null +++ b/src/math/mod.rs @@ -0,0 +1,3 @@ +pub mod int; +pub mod segment; +pub mod vec; \ No newline at end of file diff --git a/src/math/segment.rs b/src/math/segment.rs new file mode 100644 index 0000000..24cca3a --- /dev/null +++ b/src/math/segment.rs @@ -0,0 +1,92 @@ +use bevy::math::{Vec2, primitives::Segment2d}; + +use fixed::prelude::*; + +use crate::math::vec::FGVec2; + +type FGi32 = fixed::FixedI32; + +#[derive(Clone, Copy, PartialEq)] +pub struct FGSegment2d { + point1: FGVec2, + point2: FGVec2 +} + +impl FGSegment2d { + #[inline] + pub fn new(point1: FGVec2, point2: FGVec2) -> Self { + Self { + point1, point2 + } + } + + #[inline] + pub fn point1(&self) -> FGVec2 { + self.point1 + } + + #[inline] + pub fn point2(&self) -> FGVec2 { + self.point2 + } +} + +pub trait SegmentIntersection { + fn segment_intersection(&self, rhs: &Segment2d) -> Option; +} + +impl SegmentIntersection for Segment2d { + fn segment_intersection(&self, rhs: &Segment2d) -> Option { + let p0 = self.point1(); + let p1 = self.point2(); + let p2 = rhs.point1(); + let p3 = rhs.point2(); + + let s1_x = p1.x - p0.x; + let s1_y = p1.y - p0.y; + let s2_x = p3.x - p2.x; + let s2_y = p3.y - p2.y; + + let denom = -s2_x * s1_y + s1_x * s2_y; + if denom.abs() < f32::EPSILON { + return None; // Parallel or collinear + } + + let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; + let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; + + if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { + Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) + } else { + None + } + } +} + +impl SegmentIntersection for Segment2d { + fn segment_intersection(&self, rhs: &Segment2d) -> Option { + let p0 = self.point1(); + let p1 = self.point2(); + let p2 = rhs.point1(); + let p3 = rhs.point2(); + + let s1_x = p1.x - p0.x; + let s1_y = p1.y - p0.y; + let s2_x = p3.x - p2.x; + let s2_y = p3.y - p2.y; + + let denom = -s2_x * s1_y + s1_x * s2_y; + if denom.abs() < f32::EPSILON { + return None; // Parallel or collinear + } + + let s = (-s1_y * (p0.x - p2.x) + s1_x * (p0.y - p2.y)) / denom; + let t = ( s2_x * (p0.y - p2.y) - s2_y * (p0.x - p2.x)) / denom; + + if s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0 { + Some(Vec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) + } else { + None + } + } +} \ No newline at end of file diff --git a/src/math/vec.rs b/src/math/vec.rs new file mode 100644 index 0000000..600dff6 --- /dev/null +++ b/src/math/vec.rs @@ -0,0 +1,115 @@ +use bevy::math::Vec2; +use std::ops::{Add, Div, Mul, Sub}; + +use super::int::FGi32; + +#[derive(Clone, Copy, PartialEq)] +pub struct FGVec2 { + pub x: FGi32, + pub y: FGi32, +} + +impl FGVec2 { + pub const ZERO: Self = Self::new(FGi32::ZERO, FGi32::ZERO); + + #[inline(always)] + pub const fn new(x: FGi32, y: FGi32) -> Self { + Self { x, y } + } + + #[inline(always)] + pub fn from_i32(x: i32, y: i32) -> Self { + Self { + x: FGi32::from_num(x), + y: FGi32::from_num(y), + } + } + + #[inline] + pub fn length_squared(&self) -> FGi32 { + self.x * self.x + self.y * self.y + } + + #[inline] + pub fn length(&self) -> FGi32 { + (self.x * self.x + self.y * self.y).sqrt() + } + + #[inline] + pub fn normalized_or_zero(&self) -> Self { + let len = self.length(); + if len.is_zero() { + Self::ZERO + } else { + self / len + } + } + + #[inline] + pub fn to_vec2(&self) -> Vec2 { + Vec2::new(self.x.to_num(), self.y.to_num()) + } +} + +impl Mul for FGVec2 { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self::Output { + Self::new(self.x * rhs.x, self.y * rhs.y) + } +} + +impl Add for FGVec2 { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self::Output { + Self::new(self.x + rhs.x, self.y + rhs.y) + } +} + +impl Sub for FGVec2 { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self::Output { + Self::new(self.x - rhs.x, self.y - rhs.y) + } +} + +impl Div for FGVec2 { + type Output = Self; + + #[inline] + fn div(self, rhs: Self) -> Self::Output { + Self::new(self.x / rhs.x, self.y / rhs.y) + } +} + +impl Mul for FGVec2 { + type Output = Self; + + #[inline] + fn mul(self, rhs: FGi32) -> Self::Output { + Self::new(self.x * rhs, self.y * rhs) + } +} + +impl Div for FGVec2 { + type Output = Self; + + #[inline] + fn div(self, rhs: FGi32) -> Self::Output { + Self::new(self.x / rhs, self.y / rhs) + } +} + +impl Div for &FGVec2 { + type Output = FGVec2; + + #[inline] + fn div(self, rhs: FGi32) -> Self::Output { + (*self).div(rhs) + } +} diff --git a/src/player.rs b/src/player.rs index 981fe8b..6323766 100644 --- a/src/player.rs +++ b/src/player.rs @@ -8,7 +8,7 @@ use bevy_ggrs::prelude::*; /// root rather than in `netcode` because "which player is this" is a game /// concept; `netcode` only decides where the inputs come from. #[derive(Default, Component)] -#[require(Rollback)] +#[require(Rollback, Transform)] pub struct Player { pub handle: usize, } diff --git a/src/schedule.rs b/src/schedule.rs index 20f1427..4758bad 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -21,6 +21,7 @@ pub enum GameplaySet { Physics, /// Resolve the integrated positions against the stage. Collision, + Animation } /// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by @@ -37,6 +38,7 @@ impl Plugin for GameplaySchedulePlugin { GameplaySet::StateUpdate, GameplaySet::Physics, GameplaySet::Collision, + GameplaySet::Animation, ) .chain(), ); diff --git a/src/stage/debug.rs b/src/stage/debug.rs index eddfe0b..2b359c9 100644 --- a/src/stage/debug.rs +++ b/src/stage/debug.rs @@ -1,16 +1,19 @@ use bevy::{color::palettes::css::RED, prelude::*}; -use crate::stage::StageLine; +use crate::stage::{StagePoly, line::StageCollision}; + +pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res) { + for poly in stage_collision.stage_polys.iter() { + for segment in poly.segments.iter() { + let from = segment.segment.point1(); + let to = segment.segment.point2(); + let from_3d = Vec3::new(from.x, from.y, 0.0); + let end_3d = Vec3::new(to.x, to.y, 0.0); + let normal_3d = Vec3::new(segment.normal.x, segment.normal.y, 0.0); + gizmos.line(from_3d, end_3d, RED); + let midpoint = (from_3d + end_3d) * 0.5; + gizmos.arrow(midpoint, midpoint + normal_3d, RED); + } -pub fn debug_draw_scene(mut gizmos: Gizmos, planes: Query<&StageLine>) { - for plane in planes { - let from = plane.segment.point1(); - let to = plane.segment.point2(); - let from_3d = Vec3::new(from.x, from.y, 0.0); - let end_3d = Vec3::new(to.x, to.y, 0.0); - let normal_3d = Vec3::new(plane.normal.x, plane.normal.y, 0.0); - gizmos.line(from_3d, end_3d, RED); - let midpoint = (from_3d + end_3d) * 0.5; - gizmos.arrow(midpoint, midpoint + normal_3d, RED); } } diff --git a/src/stage/line.rs b/src/stage/line.rs index e3a0d3d..d3b53df 100644 --- a/src/stage/line.rs +++ b/src/stage/line.rs @@ -1,34 +1,145 @@ //! Stage collision geometry. +use std::default; + use bevy::{math::InvalidDirectionError, prelude::*}; -use crate::math::SegmentIntersection; +use crate::math::segment::SegmentIntersection; + +#[derive(Reflect, Clone, Copy, PartialEq)] +pub enum StagePolyLineSegmentType { + Floor, Wall, Ceiling +} + + +#[derive(Reflect, Clone)] +pub struct StagePolyLineSegment { + pub segment: Segment2d, + pub normal: Vec2, + pub segment_type: StagePolyLineSegmentType +} + +#[derive(Reflect, Clone, Default)] +pub enum StagePolyType { + #[default] + Closed, + Platform +} + +#[derive(Reflect, Hash, Debug, Clone, Copy, Default)] +pub struct StageLineID { + pub polygon: usize, + pub segment: usize +} #[derive(Component, Reflect, Clone, Default)] #[reflect(Component)] -pub struct StageLine { - pub segment: Segment2d, - pub normal: Vec2, +pub struct StagePoly { + poly_type: StagePolyType, + pub segments: Vec } pub struct StagePlaneIntersectResult { pub position: Vec2, + pub segment_idx: usize, + pub normal: Vec2 } -impl StageLine { - pub fn new(start: Vec2, end: Vec2) -> Result { - let segment = Segment2d::new(start, end); - let normal = segment.left_normal().as_vec2(); - Ok(Self { segment, normal }) +#[derive(Resource, Reflect, Clone, Default)] +#[reflect(Resource)] +pub struct StageCollision { + pub stage_polys: Vec +} + +impl StagePoly { + pub fn intersect_ray(&self, ray_segment: Segment2d) -> Option { + for (i, segment) in self.segments.iter().enumerate() { + if let Some(intersection) = segment.segment.segment_intersection(&ray_segment) { + println!("DIST3 {} {}", intersection, segment.segment.closest_point(intersection)); + return Some(StagePlaneIntersectResult { + position: intersection, + segment_idx: i, + normal: segment.normal + }); + } + } + None } - pub fn intersect_ray(&self, segment: Segment2d) -> Option { - self.segment - .segment_intersection(&segment) - .map(|position| StagePlaneIntersectResult { position }) + pub fn get_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { + self.segments.get(segment_idx) } + pub fn get_next_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { + self.segments.get(segment_idx % segment_idx + 1) + } + + pub fn get_prev_segment(&self, segment_idx: usize) -> Option<&StagePolyLineSegment> { + self.segments.get(segment_idx % segment_idx + 1) + } + + pub fn build(poly_type: StagePolyType, points: &[(Vec2, StagePolyLineSegmentType)]) -> Self { + let windows = points.iter().zip(points.iter().cycle().skip(1)); + + let segments = windows.map(|segment_window| { + let (first_point, segment_type) = segment_window.0; + let (second_point, _) = segment_window.1; + let segment = Segment2d::new(*first_point, *second_point); + StagePolyLineSegment { + segment_type: *segment_type, + segment, + normal: segment.left_normal().as_vec2() + } + }).collect(); + + Self { + poly_type, + segments + } + } +} + +impl StagePolyLineSegment { pub fn project(&self, vec: Vec2) -> Vec2 { vec - vec.project_onto_normalized(self.normal) } } + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn ray_intersection_test() { + let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ + (Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor), + (Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor), + ]); + + let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(0.0, 1.0), Vec2::new(0.0, -1.0))); + assert!(res.is_some()); + assert_eq!(res.unwrap().position, Vec2::ZERO); + } + + #[test] + fn ray_intersection_test_side() { + let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ + (Vec2::new(-4.5, 0.0), StagePolyLineSegmentType::Floor), + (Vec2::new(4.5, 0.0), StagePolyLineSegmentType::Floor), + ]); + + let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(2.0, 1.0), Vec2::new(2.0, -1.0))); + assert!(res.is_some()); + assert_eq!(res.unwrap().position, Vec2::new(2.0, 0.0)); + } + #[test] + fn ray_intersection_test_diagonal() { + let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ + (Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor), + (Vec2::new(2.0, 2.0), StagePolyLineSegmentType::Floor), + ]); + + let res = stage_poly.intersect_ray(Segment2d::new(Vec2::new(1.0, 2.0), Vec2::new(1.0, -2.0))); + assert!(res.is_some()); + assert_eq!(res.unwrap().position, Vec2::new(1.0, 1.0)); + } +} \ No newline at end of file diff --git a/src/stage/mod.rs b/src/stage/mod.rs index 9f74a49..b16a612 100644 --- a/src/stage/mod.rs +++ b/src/stage/mod.rs @@ -7,7 +7,7 @@ pub mod debug; pub mod line; pub mod scene; -pub use line::{StageLine, StagePlaneIntersectResult}; +pub use line::{StagePoly, StagePlaneIntersectResult}; /// Owns the stage geometry: which scene gets spawned and how it is drawn. #[derive(Default)] diff --git a/src/stage/scene.rs b/src/stage/scene.rs index 0e2551c..9a6cd5f 100644 --- a/src/stage/scene.rs +++ b/src/stage/scene.rs @@ -1,37 +1,49 @@ //! The scene content that gets spawned at startup. -use bevy::prelude::*; +use std::f32::consts::PI; -use crate::stage::StageLine; +use bevy::{camera_controller::free_camera::FreeCamera, prelude::*}; + +use crate::stage::{StagePoly, line::StagePolyLineSegmentType, line::StageCollision}; /// set up a simple 3D scene pub fn test_scene() -> impl SceneList { + + let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[ + (Vec2::new(-5.6, -0.35), StagePolyLineSegmentType::Floor), + (Vec2::new(-3.92, 0.0), StagePolyLineSegmentType::Floor), + (Vec2::new(0.0, 0.0), StagePolyLineSegmentType::Floor), + (Vec2::new(3.92, 0.0), StagePolyLineSegmentType::Floor), + (Vec2::new(5.6, -0.35), StagePolyLineSegmentType::Wall), + (Vec2::new(5.6, -20.0), StagePolyLineSegmentType::Ceiling), + (Vec2::new(-5.6, -20.0), StagePolyLineSegmentType::Wall) + ]); + bsn_list! [ ( - #Cube - Mesh3d(asset_value(Cuboid::new(5.0, 1.0, 1.0))) - MeshMaterial3d::(asset_value(Color::srgb_u8(124, 144, 255))) - Transform::from_xyz(0.0, 0.5, 0.0) + #StageCollision + template_value(StageCollision { + stage_polys: vec!(stage_poly) + }) ), ( - #StagePlane - template_value(StageLine::new(Vec2::new(-2.5, 1.0), Vec2::new(2.5, 1.0)).unwrap()) - ), - ( - PointLight { + DirectionalLight { shadow_maps_enabled: true, } - Transform::from_xyz(4.0, 8.0, 4.0) + Transform { + translation: Vec3::new(0.0, 2.0, 0.0), + rotation: Quat::from_rotation_x(-PI / 4.), + } ), ( Camera3d template_value(Transform::from_xyz(0.0, 1.5, 9.0)) - /*FreeCamera { + FreeCamera { sensitivity: 0.2, friction: 25.0, walk_speed: 3.0, run_speed: 9.0, - }*/ + } ) ] }