diff --git a/Cargo.lock b/Cargo.lock index 70e187b..c74f8be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,7 +599,7 @@ dependencies = [ "downcast-rs 2.0.2", "either", "petgraph", - "ron", + "ron 0.12.2", "serde", "smallvec", "thiserror 2.0.19", @@ -696,7 +696,7 @@ dependencies = [ "futures-util", "js-sys", "notify-debouncer-full", - "ron", + "ron 0.12.2", "serde", "stackfuture", "thiserror 2.0.19", @@ -810,6 +810,21 @@ dependencies = [ "wgpu-types", ] +[[package]] +name = "bevy_common_assets" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b9a9509ff29e5db89e1697c047fbbdc55208c8c3d07a494b013cc96814fcddb" +dependencies = [ + "anyhow", + "bevy_app", + "bevy_asset", + "bevy_reflect", + "ron 0.11.0", + "serde", + "thiserror 2.0.19", +] + [[package]] name = "bevy_core_pipeline" version = "0.19.0" @@ -881,7 +896,7 @@ dependencies = [ "bevy_ui_render", "bevy_utils", "bevy_window", - "ron", + "ron 0.12.2", "serde", "thiserror 2.0.19", "tracing", @@ -2137,7 +2152,7 @@ dependencies = [ "bevy_transform", "bevy_utils", "derive_more", - "ron", + "ron 0.12.2", "serde", "thiserror 2.0.19", "uuid", @@ -3430,6 +3445,7 @@ dependencies = [ "avian3d", "bevy", "bevy-inspector-egui", + "bevy_common_assets", "bevy_egui", "bevy_ggrs", "bevy_wind_waker_shader", @@ -3438,6 +3454,8 @@ dependencies = [ "fixed", "jackdaw_runtime", "serde", + "strum", + "strum_macros", ] [[package]] @@ -6035,6 +6053,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "ron" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db09040cc89e461f1a265139777a2bde7f8d8c67c4936f700c63ce3e2904d468" +dependencies = [ + "base64", + "bitflags 2.13.1", + "serde", + "serde_derive", + "unicode-ident", +] + [[package]] name = "ron" version = "0.12.2" diff --git a/Cargo.toml b/Cargo.toml index 98bf2a9..510b83f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,10 +30,9 @@ 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 = { version = "1.31.0", features = ["serde"] } - -# Enable a small amount of optimization in the dev profile. -[profile.dev] -opt-level = 1 +strum = "0.28.0" +strum_macros = "0.28.0" +bevy_common_assets = { version = "0.17.0", features = ["ron"] } # Enable a large amount of optimization in the dev profile for dependencies. [profile.dev.package."*"] diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index f74cf9a..7dab5b4 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -1,16 +1,23 @@ +use std::{collections::HashMap, time::Duration}; + use bevy::{mesh::skinning::SkinnedMesh, prelude::*}; -use crate::{fighter::{FighterECB, FighterTranslation, FighterVisual}, player::Player}; - -#[derive(Component)] -pub struct FighterAnimations { - pub wait: Handle -} +use crate::{ + fighter::{FighterECB, FighterTranslation, FighterVisual, visual::FighterAnimations}, player::Player, +}; #[derive(Component)] pub struct FighterAnimationPlayerLink(Entity); -pub fn apply_fighter_translation_to_visuals(query: Query<(&FighterTranslation, &FighterECB, &mut Transform)>) { +impl FighterAnimationPlayerLink { + pub fn player(&self) -> Entity { + self.0 + } +} + +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.to_num(), translation.y.to_num(), 0.0); @@ -31,18 +38,53 @@ pub fn setup_fighter_animation_player( else { continue; }; - commands.entity(parent).insert( - FighterAnimationPlayerLink(child) - ); + + commands.entity(child).insert(AnimationTransitions::new()); + info!("LINK"); + commands + .entity(parent) + .insert(FighterAnimationPlayerLink(child)); } } -pub fn apply_animation( - children: Query<&mut Transform, With>, -) { +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); + trf.rotation = Quat::IDENTITY; + trf.rotate_local_y(std::f32::consts::PI * 0.5); } +} -} \ No newline at end of file +pub fn animation_init( + fighters: Query< + ( + &FighterAnimations, + &FighterAnimationPlayerLink, + ), + Added + >, + mut commands: Commands, + mut anim_player_query: Query<(&mut AnimationPlayer, &mut AnimationTransitions)> +) { + for (anim, link) in fighters { + commands.entity(link.0).insert( + AnimationGraphHandle(anim.graph.clone()) + ); + info!("BEGIN! {:?}", fighters); + if let Ok((mut player, mut transitions)) = anim_player_query.get_mut(link.0) { + info!("DOS"); + transitions.play(&mut player, anim.clips[&AnimKind::Wait], Duration::ZERO).repeat(); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)] +pub enum AnimKind { + Wait, + Walk, + Run, + Dash, +} + +#[derive(serde::Deserialize, bevy::asset::Asset, bevy::reflect::TypePath)] +pub struct AnimManifest(pub HashMap); diff --git a/src/fighter/attributes.rs b/src/fighter/attributes.rs index bc69e44..f2acbf6 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -11,6 +11,7 @@ pub struct FighterAttributes { pub stick_walk_accel: FGi32, pub max_walk_vel: FGi32, + pub dash_acceleration_duration: u32, pub dash_duration: u32, pub dash_initial_velocity: FGi32, pub base_dash_accel: FGi32, @@ -26,11 +27,13 @@ pub struct FighterAttributes { } impl FighterAttributes { - pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame) -> (FGi32, FGi32) { + pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame, direction: FGi32) -> (FGi32, FGi32) { let accel = input.movement.x * self.stick_dash_accel; - let accel = accel + input.movement.x.signum() * self.base_dash_accel; + let accel = accel + direction * self.base_dash_accel; let target_vel = input.movement.x * self.max_dash_vel; + info!("ACCEL {}", accel); + (accel, target_vel) } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index c489b58..3bcdae0 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -1,7 +1,7 @@ //! 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::{ecs::name::HashedStr, prelude::*}; use bevy_egui::EguiPrimaryContextPass; use bevy_ggrs::prelude::*; @@ -12,6 +12,7 @@ pub mod debug; pub mod ecb; pub mod motion; pub mod state; +pub mod visual; // The fighter components are re-exported so the rest of the crate can say // `fighter::FighterVelocity` without caring which file it lives in. Named @@ -21,7 +22,18 @@ pub use attributes::FighterAttributes; pub use ecb::FighterECB; pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded}; -use crate::{fighter::state::FighterState, schedule::GameplaySet, stage::line::StageCollision}; +use crate::{ + fighter::{ + animation::animation_init, + state::{FighterState, fall::FallState}, + visual::FighterAnimations, + }, + input::FighterInput, + math::{int::FGi32, vec::FGVec2}, + player::Player, + schedule::GameplaySet, + stage::line::StageCollision, +}; use state::state_interrupt_system; /// Owns fighter simulation, the rollback registrations for fighter @@ -32,7 +44,7 @@ pub struct FighterPlugin; #[derive(Component, Copy, Clone, Hash)] pub enum FighterFacingDirection { Left, - Right + Right, } #[derive(Component)] @@ -41,56 +53,97 @@ 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, - ( - ecb::snapshot_fighter_ecb.before(state_interrupt_system), - animation::setup_fighter_animation_player, - state::state_interrupt_system, - ) - .in_set(GameplaySet::Interrupt), + app.add_systems( + Update, + (animation_init, animation::setup_fighter_animation_player), + ) + // Every state gets a chance to interrupt itself, in priority order. + .add_systems( + GgrsSchedule, + ( + ecb::snapshot_fighter_ecb.before(state_interrupt_system), + state::state_interrupt_system, ) - .add_systems( - GgrsSchedule, - (state::state_update_system,).in_set(GameplaySet::StateUpdate), + .in_set(GameplaySet::Interrupt), + ) + .add_systems( + GgrsSchedule, + (state::state_update_system,).in_set(GameplaySet::StateUpdate), + ) + /*.add_systems( + GgrsSchedule, + (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, + ( + animation::apply_fighter_translation_to_visuals, + animation::apply_animation, ) - /*.add_systems( - GgrsSchedule, - (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, - ( - animation::apply_fighter_translation_to_visuals, - animation::apply_animation, - ) - .chain() - .in_set(GameplaySet::Animation), - ) - // 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::() - .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_resource_with_reflect::() - // Debug views. - .add_systems(FixedPostUpdate, debug::debug_draw_ecb) - .add_systems(EguiPrimaryContextPass, debug::fighter_debug) - .add_systems(EguiPrimaryContextPass, debug::update_config) - .rollback_component_with_clone::() - .checksum_component_with_hash::(); + .chain() + .in_set(GameplaySet::Animation), + ) + // 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::() + .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_resource_with_reflect::() + // Debug views. + .add_systems(FixedPostUpdate, debug::debug_draw_ecb) + .add_systems(EguiPrimaryContextPass, debug::fighter_debug) + .add_systems(EguiPrimaryContextPass, debug::update_config) + .rollback_component_with_clone::() + .checksum_component_with_hash::(); } } + +pub fn spawn_fighter( + commands: &mut Commands, + animations: FighterAnimations, + visual_root: WorldAssetRoot, +) { + commands.spawn(( + Name("Fighter".into()), + Player { handle: 0 }, + FighterFacingDirection::Right, + FighterECB { + vertical_half: FGi32::lit("5.0"), + horizontal_half: FGi32::lit("2.5"), + }, + FighterVelocity::default(), + FighterTranslation(FGVec2::lit("5.0", "12.5")), + FighterAttributes { + base_walk_accel: FGi32::lit("0.1"), + stick_walk_accel: FGi32::lit("0.16"), + max_walk_vel: FGi32::lit("0.7"), + ground_friction: FGi32::lit("0.09"), + dash_acceleration_duration: 13, + dash_duration: 24, + base_dash_accel: FGi32::lit("0.02"), + stick_dash_accel: FGi32::lit("0.065"), + max_dash_vel: FGi32::lit("1.1"), + jump_vertical_velocity: FGi32::lit("2.5"), + dash_initial_velocity: FGi32::lit("1.4"), + terminal_velocity: FGi32::lit("2.8"), + gravity: FGi32::lit("0.23"), + jumpsquat_duration: 4, + }, + visual_root, + animations, + FighterState::Fall(FallState), + FighterVisual, + FighterInput::default(), + )); +} diff --git a/src/fighter/state/dash.rs b/src/fighter/state/dash.rs index 2cab4ab..bbd19cd 100644 --- a/src/fighter/state/dash.rs +++ b/src/fighter/state/dash.rs @@ -1,9 +1,12 @@ use std::hash::{Hash, Hasher}; +use std::time::Duration; use bevy::prelude::*; use super::{FighterStateImpl, FighterStateContext, ground, run, wait, walk}; -use crate::fighter::state::ground::GroundedStateCommon; +use crate::fighter::animation::AnimKind; +use crate::fighter::state::fall::FallState; +use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::wait::WaitState; use crate::fighter::state::{FighterState}; use crate::fighter::{FighterAttributes, FighterVelocity, collision}; @@ -45,10 +48,13 @@ impl FighterStateImpl for DashState { state_context: &FighterStateContext, ) -> Option { if self.frames_in_dash > state_context.fighter_attribs.dash_duration { - // Dash finish shenanigans 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 if self.frames_in_dash > state_context.fighter_attribs.dash_acceleration_duration { + // Dash finish shenanigans + run::check_input(state_context, &self.ground_common) + .or_else(|| walk::check_input(state_context, &self.ground_common)) } else { check_smash_input_with_dir(state_context) .filter(|dir| *dir != self.direction) @@ -59,13 +65,21 @@ impl FighterStateImpl for DashState { 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; + + state_context.animation_transitions.play( + &mut state_context.animation_player, + state_context.animations.clips[&AnimKind::Dash], + Duration::ZERO, + ); } fn update(&mut self, state_context: &mut FighterStateContext) { self.frames_in_dash += 1; + info!("{}", self.frames_in_dash); + 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, target_vel) = state_context.fighter_attribs.get_accel_and_target_dashrun(&last_frame, self.direction); let accel = ground::compute_ground_accel( accel, target_vel, @@ -86,13 +100,12 @@ impl FighterStateImpl for DashState { &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 } )) + if let GroundedMotionResult::InAir = ground::collide_with_stage_grounded( + state_context, + &mut self.ground_common, + true + ) { + Some(FighterState::Fall(FallState)) } else { None } @@ -110,6 +123,5 @@ fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option Option { - check_smash_input_with_dir(state_context).map(|dir| FighterState::Dash(DashState::new(dir, ground_common.clone()))); - None + check_smash_input_with_dir(state_context).map(|dir| FighterState::Dash(DashState::new(dir, ground_common.clone()))) } diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 03a54f7..69bb181 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -2,7 +2,7 @@ //! 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::state::FighterStateContext; +use crate::fighter::state::{FighterState, FighterStateContext}; use crate::fighter::{ FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; @@ -10,7 +10,9 @@ use crate::game_settings::GameSettings; use crate::math::int::FGi32; use crate::math::vec::FGVec2; use crate::stage::StagePoly; -use crate::stage::line::{StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType}; +use crate::stage::line::{ + StageCollision, StageLineID, StagePolyLineSegment, StagePolyLineSegmentType, +}; use bevy::prelude::*; use fixed::traits::Fixed; @@ -37,7 +39,7 @@ pub fn compute_ground_accel( attribs: &FighterAttributes, game_settings: &GameSettings, ) -> FGi32 { - if target_vel == 0.0 { + if target_vel == FGi32::ZERO { apply_grounded_friction(attribs.ground_friction, gr_vel) } else { let mut accel = accel; @@ -81,13 +83,13 @@ pub fn apply_grounded_motion( ) { // Vertical velocity should be 0 on the ground prev_translation.0 = translation.0; - translation.0 += FGVec2::new(velocity.0.x ,FGi32::ZERO); + translation.0 += FGVec2::new(velocity.0.x, FGi32::ZERO); } pub fn collide_with_stage_grounded( state_context: &mut FighterStateContext, ground_common: &mut GroundedStateCommon, - can_walk_off: bool + can_walk_off: bool, ) -> GroundedMotionResult { let translation = &mut state_context.translation; let ecb = &state_context.ecb; @@ -116,7 +118,8 @@ pub fn collide_with_stage_grounded( } let line_dir = segment.segment.direction(); - let projected_point = segment.segment.point1() + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir); + let projected_point = segment.segment.point1() + + (ecb_bottom_pos - segment.segment.point1()).project_onto(line_dir); let closest_point = segment.segment.closest_point(ecb_bottom_pos); let distance = closest_point.distance(projected_point); @@ -129,7 +132,8 @@ pub fn collide_with_stage_grounded( 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); + 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 { @@ -139,3 +143,10 @@ pub fn collide_with_stage_grounded( GroundedMotionResult::InAir } } + +pub fn grounded_movement_common_interrupts( + state_context: &FighterStateContext, + ground_common: &GroundedStateCommon +) -> Option { + super::jump::check_input(state_context, ground_common) +} diff --git a/src/fighter/state/jump.rs b/src/fighter/state/jump.rs index d6cd28a..f5c4c60 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -99,8 +99,7 @@ pub fn check_input( state_context .input .has_command(FighterCommands::Jump) - .then_some(FighterState::JumpSquat(JumpSquatState::create( + .then(|| FighterState::JumpSquat(JumpSquatState::create( ground_common.clone(), - ))); - None + ))) } diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index 2aef511..f5737d8 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -2,28 +2,27 @@ //! 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::ecs::query::QueryData; use bevy::prelude::*; -use bevy_ggrs::{GgrsSchedule, RollbackApp}; +use crate::fighter::animation::FighterAnimationPlayerLink; use crate::fighter::ecb::FighterPreviousECB; -use crate::fighter::{FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; +use crate::fighter::visual::FighterAnimations; +use crate::fighter::{ + FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity, +}; use crate::game_settings::GameSettings; use crate::input::FighterInput; -use crate::math::int::FGi32; -use crate::schedule::GameplaySet; -use crate::stage::StagePoly; use crate::stage::line::StageCollision; +pub mod air; pub mod dash; +pub mod fall; pub mod ground; +pub mod jump; pub mod run; pub mod wait; pub mod walk; -pub mod fall; -pub mod air; -pub mod jump; macro_rules! fighter_states { ($($variant:ident => $ty:ty),* $(,)?) => { @@ -37,6 +36,10 @@ macro_rules! fighter_states { pub fn check_interrupt(&self, ctx: &FighterStateContext) -> Option { match self { $(Self::$variant(s) => s.check_interrupt(ctx)),* } } + + pub fn play_animation(&self, transitions: &mut AnimationTransitions, player: &mut AnimationPlayer, anims: &FighterAnimations) { + match self { $(Self::$variant(s) => s.play_animation(transitions, player, anims)),* } + } pub fn update(&mut self, ctx: &mut FighterStateContext) { match self { $(Self::$variant(s) => s.update(ctx)),* } } @@ -57,11 +60,15 @@ macro_rules! fighter_states { pub trait FighterStateImpl: Sized { const NAME: &'static str; - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option; + fn check_interrupt(&self, state_context: &FighterStateContext) -> Option; + fn play_animation( + &self, + _transitions: &mut AnimationTransitions, + _player: &mut AnimationPlayer, + _anims: &FighterAnimations, + ) { + } fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} fn update(&mut self, _state_context: &mut FighterStateContext); @@ -73,7 +80,7 @@ pub trait FighterStateImpl: Sized { } } -pub struct FighterStateContext<'a, 'w, 's> { +pub struct FighterStateContext<'a> { pub translation: &'a mut FighterTranslation, pub prev_translation: &'a mut FighterPreviousTranslation, pub input: &'a mut FighterInput, @@ -82,9 +89,10 @@ pub struct FighterStateContext<'a, 'w, 's> { pub prev_ecb: &'a mut FighterPreviousECB, pub fighter_attribs: &'a FighterAttributes, pub game_settings: &'a GameSettings, - pub entity: Entity, pub stage_collision: &'a StageCollision, - pub gizmos: Option<&'a mut Gizmos<'w, 's>>, + pub animations: &'a FighterAnimations, + pub animation_player: &'a mut AnimationPlayer, + pub animation_transitions: &'a mut AnimationTransitions, } fighter_states! { @@ -97,6 +105,54 @@ fighter_states! { Jump => jump::JumpState } +#[derive(QueryData)] +#[query_data(mutable)] +pub struct FighterFrameQuery { + pub translation: &'static mut FighterTranslation, + pub prev_translation: &'static mut FighterPreviousTranslation, + pub input: &'static mut FighterInput, + pub velocity: &'static mut FighterVelocity, + pub ecb: &'static mut FighterECB, + pub prev_ecb: &'static mut FighterPreviousECB, + pub attributes: &'static FighterAttributes, +} + +impl<'w, 's> FighterFrameQueryItem<'w, 's> { + fn context<'a>( + &'a mut self, + game_settings: &'a GameSettings, + stage_collision: &'a StageCollision, + animations: &'a FighterAnimations, + animation_player: &'a mut AnimationPlayer, + animation_transitions: &'a mut AnimationTransitions, + ) -> FighterStateContext<'a> { + FighterStateContext { + translation: &mut self.translation, + prev_translation: &mut self.prev_translation, + input: &mut self.input, + velocity: &mut self.velocity, + ecb: &mut self.ecb, + prev_ecb: &mut self.prev_ecb, + fighter_attribs: self.attributes, + game_settings, + stage_collision, + animations, + animation_player, + animation_transitions, + } + } +} + +#[derive(QueryData)] +#[query_data(mutable)] +pub struct StatefulFighterQuery { + pub entity: Entity, + pub state: &'static mut FighterState, + pub frame: FighterFrameQuery, + pub animations: &'static FighterAnimations, + pub animation_player_link: &'static FighterAnimationPlayerLink, +} + #[derive(Component)] pub struct StateNameDebug(pub &'static str); @@ -107,93 +163,110 @@ impl std::fmt::Display for StateNameDebug { } pub fn state_update_system( - query: Query<(Entity, &mut FighterState, &mut FighterTranslation, &mut FighterPreviousTranslation, &mut FighterInput, &mut FighterVelocity, &mut FighterECB, &mut FighterPreviousECB, &FighterAttributes)>, + mut query: Query, + mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, stage_collision: Res, game_settings: Res, - mut gizmos: Gizmos, ) { - 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, - gizmos: Some(&mut gizmos), + for fighter in &mut query { + let StatefulFighterQueryItem { + mut state, + mut frame, + animations, + animation_player_link, + .. + } = fighter; + let Ok((mut animation_player, mut animation_transitions)) = + animation_players.get_mut(animation_player_link.player()) + else { + continue; }; + let mut update_ctx = frame.context( + &game_settings, + &stage_collision, + animations, + &mut animation_player, + &mut animation_transitions, + ); 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)>, +pub fn state_interrupt_system( + mut query: Query, + mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, 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, - gizmos: None, + for fighter in &mut query { + let StatefulFighterQueryItem { + entity, + state, + mut frame, + animations, + animation_player_link, + } = fighter; + let Ok((mut animation_player, mut animation_transitions)) = + animation_players.get_mut(animation_player_link.player()) + else { + continue; }; + let mut state_context = frame.context( + &game_settings, + &stage_collision, + animations, + &mut animation_player, + &mut animation_transitions, + ); if let Some(new_state) = state.check_interrupt(&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) - )); + commands + .entity(entity) + .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)>, + mut query: Query, + mut animation_players: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, stage_collision: Res, game_settings: Res, mut commands: Commands, - mut gizmos: Gizmos, ) { - 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, - gizmos: Some(&mut gizmos), + for fighter in &mut query { + let StatefulFighterQueryItem { + entity, + mut state, + mut frame, + animations, + animation_player_link, + } = fighter; + let Ok((mut animation_player, mut animation_transitions)) = + animation_players.get_mut(animation_player_link.player()) + else { + continue; }; + let mut state_context = frame.context( + &game_settings, + &stage_collision, + animations, + &mut animation_player, + &mut animation_transitions, + ); 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) - )); + commands + .entity(entity) + .insert((new_state, StateNameDebug(new_state_name))); info!("State {} -> {}", old_state_name, new_state_name); } } diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs index a2f1a2c..6e55b68 100644 --- a/src/fighter/state/run.rs +++ b/src/fighter/state/run.rs @@ -1,12 +1,16 @@ +use std::time::Duration; + use bevy::prelude::*; use super::{FighterStateImpl, FighterStateContext, ground, wait, walk}; +use crate::fighter::animation::AnimKind; 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; +use crate::math::int::FGi32; #[derive(Component, Debug, Clone, Hash)] pub struct RunState { @@ -27,11 +31,17 @@ impl FighterStateImpl for RunState { } } - fn on_enter(&mut self, _state_context: &mut super::FighterStateContext) {} + fn on_enter(&mut self, state_context: &mut super::FighterStateContext) { + state_context.animation_transitions.play( + &mut state_context.animation_player, + state_context.animations.clips[&AnimKind::Run], + Duration::ZERO, + ).repeat(); + } 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, target_vel) = state_context.fighter_attribs.get_accel_and_target_dashrun(&last_frame, FGi32::ONE); let accel = ground::compute_ground_accel( accel, target_vel, diff --git a/src/fighter/state/wait.rs b/src/fighter/state/wait.rs index b497448..67a9b68 100644 --- a/src/fighter/state/wait.rs +++ b/src/fighter/state/wait.rs @@ -1,38 +1,50 @@ +use std::time::Duration; + use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, ground, walk}; -use crate::fighter::state::FighterState; +use super::{FighterStateContext, FighterStateImpl, ground, walk}; +use crate::fighter::animation::AnimKind; +use crate::fighter::state::{FighterState, dash}; use crate::fighter::state::fall::FallState; -use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; +use crate::fighter::state::ground::{ + GroundedMotionResult, GroundedStateCommon, grounded_movement_common_interrupts, +}; use crate::fighter::{FighterAttributes, FighterVelocity}; use crate::game_settings::GameSettings; use crate::stage::line::StageLineID; #[derive(Debug, Clone, Hash)] pub struct WaitState { - pub grounded_common: GroundedStateCommon + pub grounded_common: GroundedStateCommon, } impl FighterStateImpl for WaitState { const NAME: &'static str = "Wait"; - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { - walk::check_input(state_context, &self.grounded_common) + fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { + dash::check_input(state_context, &self.grounded_common).or_else(|| + grounded_movement_common_interrupts(state_context, &self.grounded_common) + .or_else(|| walk::check_input(state_context, &self.grounded_common))) } - - fn on_enter(&mut self, _state_context: &mut super::FighterStateContext) {} - + + fn on_enter(&mut self, state_context: &mut super::FighterStateContext) { + state_context.animation_transitions.play( + &mut state_context.animation_player, + state_context.animations.clips[&AnimKind::Wait], + Duration::ZERO, + ).repeat(); + } + 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 - }; + 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 + }; let ground_velocity = state_context.velocity.x; @@ -44,16 +56,14 @@ impl FighterStateImpl for WaitState { 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 - ) { + if let GroundedMotionResult::InAir = + ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false) + { Some(FighterState::Fall(FallState)) } else { None @@ -61,12 +71,16 @@ impl FighterStateImpl for WaitState { } } -pub fn check_input(state_context: &FighterStateContext, ground_state_common: &GroundedStateCommon) -> 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(FighterState::Wait(WaitState { grounded_common: ground_state_common.clone() })); + return Some(FighterState::Wait(WaitState { + grounded_common: ground_state_common.clone(), + })); } None } - diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index 6cfb7eb..1570cf4 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -1,6 +1,9 @@ +use std::time::Duration; + use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, dash, ground, wait}; +use super::{FighterStateContext, FighterStateImpl, dash, ground, wait}; +use crate::fighter::animation::AnimKind; use crate::fighter::state::fall::FallState; use crate::fighter::state::ground::{GroundedMotionResult, GroundedStateCommon}; use crate::fighter::state::{self, FighterState}; @@ -10,26 +13,37 @@ use crate::input::FighterInput; #[derive(Debug, Clone, Hash)] pub struct WalkState { - pub grounded_common: GroundedStateCommon + pub grounded_common: GroundedStateCommon, } impl FighterStateImpl for WalkState { const NAME: &'static str = "Walk"; - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { - wait::check_input(state_context, &self.grounded_common).or_else(|| dash::check_input(state_context, &self.grounded_common)) + fn check_interrupt(&self, state_context: &FighterStateContext) -> 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 on_enter(&mut self, state_context: &mut FighterStateContext) { + state_context.animation_transitions.play( + &mut state_context.animation_player, + state_context.animations.clips[&AnimKind::Walk], + Duration::ZERO, + ).repeat(); + } + 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, 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); + 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; @@ -39,16 +53,14 @@ impl FighterStateImpl for WalkState { 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 - ) { + if let GroundedMotionResult::InAir = + ground::collide_with_stage_grounded(state_context, &mut self.grounded_common, false) + { Some(FighterState::Fall(FallState)) } else { None @@ -56,11 +68,16 @@ impl FighterStateImpl for WalkState { } } -pub fn check_input(state_context: &FighterStateContext, ground_state_common: &GroundedStateCommon) -> 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(FighterState::Walk(WalkState { grounded_common: ground_state_common.clone() })); + return Some(FighterState::Walk(WalkState { + grounded_common: ground_state_common.clone(), + })); } None diff --git a/src/fighter/visual.rs b/src/fighter/visual.rs new file mode 100644 index 0000000..b717008 --- /dev/null +++ b/src/fighter/visual.rs @@ -0,0 +1,98 @@ +use bevy::gltf::Gltf; +use bevy::prelude::*; +use std::collections::HashMap; + +use crate::{AppState, fighter::{ + animation::{AnimKind, AnimManifest}, + spawn_fighter, +}}; + +// One entry per character being loaded +pub struct PendingCharacter { + pub id: String, + pub gltf: Handle, + pub manifest: Handle, + pub resolved: bool, +} + +#[derive(Resource, Default)] +pub struct CharacterLoadQueue(pub Vec); + +#[derive(Component)] +pub struct FighterAnimations { + pub graph: Handle, + pub clips: HashMap, +} + +pub fn start_loading(mut commands: Commands, asset_server: Res) { + let queue = vec![PendingCharacter { + id: "jigglypuff".to_owned(), + gltf: asset_server.load(format!("fighters/jiggs/jigglypuff-normal.glb")), + manifest: asset_server.load(format!("fighters/jiggs/jigglypuff-anims.ron")), + resolved: false, + }]; + + commands.insert_resource(CharacterLoadQueue(queue)); +} + +pub fn resolve_character_assets( + mut commands: Commands, + mut queue: ResMut, + gltf_assets: Res>, + manifest_assets: Res>, + mut graphs: ResMut>, + anim_clips: Res>, + mut next_state: ResMut>, +) { + for pending in queue.0.iter_mut().filter(|p| !p.resolved) { + let (Some(gltf), Some(manifest)) = ( + gltf_assets.get(&pending.gltf), + manifest_assets.get(&pending.manifest), + ) else { + continue; // still loading, try again next frame + }; + + let mut graph = AnimationGraph::new(); + let mut clips = HashMap::new(); + let mut missing = Vec::new(); + + for (kind, clip_name) in manifest.0.iter() { + match gltf.named_animations.get(clip_name.as_str()) { + Some(clip_handle) => { + let animation = anim_clips.get(clip_handle); + info!("{clip_name}, {}", animation.unwrap().duration()); + let node = graph.add_clip(clip_handle.clone(), 1.0, graph.root); + clips.insert(*kind, node); + } + None => missing.push(clip_name.clone()), + } + } + + if !missing.is_empty() { + panic!( + "character '{}': manifest references animations not found in glb: {:?}", + pending.id, missing + ); + } + + let graph_handle = graphs.add(graph); + + + spawn_fighter( + &mut commands, + FighterAnimations { + graph: graph_handle, + clips, + }, + WorldAssetRoot(gltf.scenes[0].clone()), + ); + + pending.resolved = true; + } + + // All done? Move on. + if queue.0.iter().all(|p| p.resolved) { + next_state.set(AppState::InGame); + commands.remove_resource::(); + } +} diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 2981a80..493aeab 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -6,14 +6,15 @@ use std::ops::{Index, IndexMut}; use bevy::prelude::*; use bevy_ggrs::{PlayerInputs, Rollback}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; use crate::game_settings::GameSettings; use crate::input::FighterInputFrame; use crate::netcode::GGRSCfg; use crate::player::Player; -struct FighterInputBufferedActions {} - +#[derive(Clone, Copy, EnumIter)] pub enum FighterCommands { SmashMoveLeft, SmashMoveRight, @@ -85,6 +86,10 @@ impl FighterInput { pub fn has_command(&self, command: FighterCommands) -> bool { self.command_lifetimes[command] > 0 } + + pub fn reduce_command_lifetime(&mut self, command: FighterCommands) { + self.command_lifetimes[command] = self.command_lifetimes[command].saturating_sub(1); + } } /// Turns this frame's rolled-back inputs into per-fighter [`FighterInput`] @@ -95,6 +100,11 @@ pub fn postprocess_input( game_settings: Res, ) { for (mut input, player) in query { + + for command in FighterCommands::iter() { + input.reduce_command_lifetime(command); + } + input.push_input_frame(inputs[player.handle].0); // Directional smash detection @@ -117,6 +127,10 @@ pub fn postprocess_input( } } + if !input.prev_frame.jump && input.current_frame.jump { + input.set_lifetime(FighterCommands::Jump, game_settings.input_common.input_buffer_size); + } + if x_abs >= axis_threshold && input.frames_in_smash_move_deadzone <= frame_threshold { input.clear_command(FighterCommands::SmashMoveLeft); input.clear_command(FighterCommands::SmashMoveRight); diff --git a/src/input/frame.rs b/src/input/frame.rs index f9028db..0576282 100644 --- a/src/input/frame.rs +++ b/src/input/frame.rs @@ -1,7 +1,7 @@ use bevy::prelude::*; use serde::{Deserialize, Serialize}; -use crate::math::vec::FGVec2; +use crate::{input::InputActionState, math::vec::FGVec2}; /// One frame of a player's raw input. This is the type GGRS serializes and /// sends over the wire, so keep it small and keep it `PartialEq`. @@ -9,4 +9,5 @@ use crate::math::vec::FGVec2; pub struct FighterInputFrame { pub movement: FGVec2, pub directional_attack: FGVec2, + pub jump: bool } diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index 110e058..5e31380 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -44,7 +44,9 @@ pub fn preprocess_keyboard_input( InputMapAction::MovementYDir(sign) => { input_frame.movement.y += (sign) * action_strength } - InputMapAction::Jump => todo!(), + InputMapAction::Jump => { + input_frame.jump = action_strength != FGi32::ZERO + }, } } diff --git a/src/input/map.rs b/src/input/map.rs index f528d4b..2e521fd 100644 --- a/src/input/map.rs +++ b/src/input/map.rs @@ -3,12 +3,14 @@ //! decides how to read it. use bevy::prelude::*; +use serde::{Deserialize, Serialize}; -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Default)] pub enum InputActionState { JustPressed, Pressed, JustReleased, + #[default] Released, } diff --git a/src/input/mod.rs b/src/input/mod.rs index 0bffea1..d2501aa 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -39,6 +39,11 @@ impl Plugin for FighterInputPlugin { key: KeyCode::KeyA, action: InputMapAction::MovementXDir(-1), }, + // jump + KeyboardInputMapElement { + key: KeyCode::Space, + action: InputMapAction::Jump, + }, ], }) .add_systems(ReadInputs, keyboard::preprocess_keyboard_input) diff --git a/src/lib.rs b/src/lib.rs index 276fa52..7b8a50c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ use jackdaw_runtime::prelude::*; use crate::{ fighter::{ - FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::FighterAnimations, state::{FighterState, fall::FallState, wait::WaitState}, + FighterAttributes, FighterECB, FighterTranslation, FighterVelocity, FighterVisual, animation::{AnimManifest}, state::{FighterState, fall::FallState, wait::WaitState}, visual::resolve_character_assets, }, game_settings::{FighterSettingsCommon, GameSettings, InputSettingsCommon}, input::FighterInput, math::{int::FGi32, vec::FGVec2}, player::Player, schedule::GameplaySchedulePlugin, }; @@ -42,6 +42,13 @@ pub mod player; pub mod schedule; pub mod stage; +#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] +pub enum AppState { + #[default] + LoadingCharacters, + InGame, +} + /// Your game's Bevy plugin. The editor finds it by this name (override /// with `plugin = "..."` in jackdaw.toml) and runs it on Play; the /// standalone binary adds it in `main.rs`. @@ -53,47 +60,7 @@ pub fn setup(mut commands: Commands, asset_server: Res) { 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: FGi32::lit("0.5"), - horizontal_half: FGi32::lit("0.25"), - }, - FighterVelocity::default(), - FighterTranslation(FGVec2::lit("0.5", "1.25")), - FighterAttributes { - base_walk_accel: FGi32::lit("0.01"), - stick_walk_accel: FGi32::lit("0.02"), - max_walk_vel: FGi32::lit("0.16"), - ground_friction: FGi32::lit("0.008"), - dash_duration: 15, - base_dash_accel: FGi32::lit("0.002"), - stick_dash_accel: FGi32::lit("0.01"), - max_dash_vel: FGi32::lit("0.22"), - jump_vertical_velocity: FGi32::lit("0.25"), - dash_initial_velocity: FGi32::lit("0.19"), - terminal_velocity: FGi32::lit("0.28"), - gravity: FGi32::lit("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 { @@ -101,7 +68,9 @@ impl Plugin for GamePlugin { // Each module owns its own systems, rollback registrations and // resources; this plugin only wires them together and supplies the // game-level content and configuration. - app.add_plugins(( + app + .init_state::() + .add_plugins(( WindWakerShaderPlugin::default(), // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. netcode::FighterNetcodePlugin::default(), @@ -109,6 +78,7 @@ impl Plugin for GamePlugin { input::FighterInputPlugin, fighter::FighterPlugin, stage::StagePlugin, + bevy_common_assets::ron::RonAssetPlugin::::new(&[]) )) .add_systems(Startup, setup) .add_systems(Update, spin_cubes) @@ -116,11 +86,16 @@ impl Plugin for GamePlugin { .insert_resource(GameSettings { figher_common: FighterSettingsCommon { walk_speed_ease: FGi32::lit("0.5"), - ground_max_horizontal_velocity: FGi32::lit("0.3"), + ground_max_horizontal_velocity: FGi32::lit("3.0"), ground_friction_over_walk_speed_multiplier: FGi32::lit("2.0"), }, input_common: InputSettingsCommon::default(), - }); + }) + .add_systems(OnEnter(AppState::LoadingCharacters), crate::fighter::visual::start_loading) + .add_systems( + Update, + resolve_character_assets.run_if(in_state(AppState::LoadingCharacters)), + ); } } diff --git a/src/math/int.rs b/src/math/int.rs index a6061a6..23c6b73 100644 --- a/src/math/int.rs +++ b/src/math/int.rs @@ -3,3 +3,4 @@ use bevy::math::{Vec2, primitives::Segment2d}; use fixed::prelude::*; pub type FGi32 = fixed::FixedI32; +pub type FGWide = fixed::FixedI128; diff --git a/src/math/segment.rs b/src/math/segment.rs index 6b42b55..90c522c 100644 --- a/src/math/segment.rs +++ b/src/math/segment.rs @@ -3,9 +3,10 @@ use bevy::math::{Vec2, primitives::Segment2d}; use bevy::prelude::*; use fixed::prelude::*; -use crate::math::vec::FGVec2; - -type FGi32 = fixed::FixedI32; +use crate::math::{ + int::{FGWide, FGi32}, + vec::FGVec2, +}; #[derive(Clone, Copy, PartialEq, Reflect)] pub struct FGSegment2d { @@ -81,7 +82,14 @@ impl FGSegment2d { // Point lies somewhere in the middle, we compute the closest point by finding the parameter along the line. let t = projection_scaled / length_squared; - self.point1 + segment_vector * t + FGVec2::new( + FGi32::from_num( + FGWide::from_num(self.point1.x) + FGWide::from_num(segment_vector.x) * t, + ), + FGi32::from_num( + FGWide::from_num(self.point1.y) + FGWide::from_num(segment_vector.y) * t, + ), + ) } /// Compute the normalized counterclockwise normal on the left-hand side of the line segment. @@ -100,22 +108,34 @@ impl FGSegment2d { 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 p0_x = FGWide::from_num(p0.x); + let p0_y = FGWide::from_num(p0.y); + let p1_x = FGWide::from_num(p1.x); + let p1_y = FGWide::from_num(p1.y); + let p2_x = FGWide::from_num(p2.x); + let p2_y = FGWide::from_num(p2.y); + let p3_x = FGWide::from_num(p3.x); + let p3_y = FGWide::from_num(p3.y); + + 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 == FGi32::ZERO { + if denom == FGWide::ZERO { return None; // Parallel or collinear } + info!("{p0}, {p1}, {p2}, {p3}"); + 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; - 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 >= FGi32::ZERO && s <= FGi32::ONE && t >= FGi32::ZERO && t <= FGi32::ONE { - Some(FGVec2::new(p0.x + (t * s1_x), p0.y + (t * s1_y))) + if s >= FGWide::ZERO && s <= FGWide::ONE && t >= FGWide::ZERO && t <= FGWide::ONE { + Some(FGVec2::new( + FGi32::from_num(p0_x + t * s1_x), + FGi32::from_num(p0_y + t * s1_y), + )) } else { None } @@ -134,4 +154,26 @@ mod tests { assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1()); assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2()); } + + #[test] + fn segment_intersection_avoids_cross_product_overflow() { + let segment = FGSegment2d::new(FGVec2::lit("56", "-3.5"), FGVec2::lit("56", "-200")); + let rhs = FGSegment2d::new( + FGVec2::lit("-111.26334", "-130.22586"), + FGVec2::lit("-112.36334", "-133.02586"), + ); + + assert_eq!(segment.segment_intersection(&rhs), None); + } + + #[test] + fn segment_intersection_handles_large_crossing_segments() { + let horizontal = FGSegment2d::new(FGVec2::lit("-200", "0"), FGVec2::lit("200", "0")); + let vertical = FGSegment2d::new(FGVec2::lit("0", "-200"), FGVec2::lit("0", "200")); + + assert_eq!( + horizontal.segment_intersection(&vertical), + Some(FGVec2::ZERO) + ); + } } diff --git a/src/math/vec.rs b/src/math/vec.rs index cbcaa09..b5d21fb 100644 --- a/src/math/vec.rs +++ b/src/math/vec.rs @@ -4,7 +4,7 @@ use fixed::types::I32F32; use serde::{Deserialize, Serialize}; use std::ops::{AddAssign, Add, Div, Mul, Sub}; -use super::int::FGi32; +use super::int::{FGWide, FGi32}; #[derive(Clone, Copy, PartialEq, Default, Debug, Reflect, Serialize, Deserialize)] #[reflect(opaque)] @@ -41,13 +41,15 @@ impl FGVec2 { } #[inline] - pub fn length_squared(&self) -> FGi32 { - self.x * self.x + self.y * self.y + pub fn length_squared(&self) -> FGWide { + let x = FGWide::from_num(self.x); + let y = FGWide::from_num(self.y); + x * x + y * y } #[inline] pub fn length(self) -> FGi32 { - (self.x * self.x + self.y * self.y).sqrt() + FGi32::from_num(self.length_squared().sqrt()) } /// Returns the vector projection of `self` onto `rhs`. @@ -62,7 +64,11 @@ impl FGVec2 { pub fn project_onto_normalized(self, rhs: Self) -> Self { assert!(rhs.is_normalized()); - rhs * self.dot(rhs) + let scale = self.dot(rhs); + Self::new( + FGi32::from_num(FGWide::from_num(rhs.x) * scale), + FGi32::from_num(FGWide::from_num(rhs.y) * scale), + ) } const EPS: FGi32 = FGi32::from_bits(6); @@ -76,16 +82,21 @@ impl FGVec2 { #[inline] #[must_use] - pub fn dot(self, rhs: Self) -> FGi32 { - (self.x * rhs.x) + (self.y * rhs.y) + pub fn dot(self, rhs: Self) -> FGWide { + FGWide::from_num(self.x) * FGWide::from_num(rhs.x) + + FGWide::from_num(self.y) * FGWide::from_num(rhs.y) } #[inline] #[must_use] pub fn project_onto(self, rhs: Self) -> Self { let other_len_sq = rhs.dot(rhs); - assert!(other_len_sq != FGi32::ZERO); - rhs * (self.dot(rhs) / other_len_sq) + assert!(other_len_sq != FGWide::ZERO); + let scale = self.dot(rhs) / other_len_sq; + Self::new( + FGi32::from_num(FGWide::from_num(rhs.x) * scale), + FGi32::from_num(FGWide::from_num(rhs.y) * scale), + ) } #[inline] @@ -246,7 +257,7 @@ mod is_normalized_tests { let v = FGVec2 { x: fx, y: fy }; if let Some(normalized) = v.normalize() { - let ls = normalized.length_squared(); + let ls = FGi32::from_num(normalized.length_squared()); let diff_bits = ls.to_bits() - FGi32::ONE.to_bits(); let abs_diff_bits = diff_bits.abs(); @@ -310,4 +321,12 @@ mod is_normalized_tests { let non_unit_small = FGVec2 { x: FGi32::from_num(0.5), y: FGi32::from_num(0.0) }; assert!(!non_unit_small.is_normalized()); } + + #[test] + fn dot_and_length_squared_use_wide_intermediates() { + let vector = FGVec2::lit("200", "200"); + + assert_eq!(vector.dot(vector), FGWide::from_num(80_000)); + assert_eq!(vector.length_squared(), FGWide::from_num(80_000)); + } } diff --git a/src/stage/scene.rs b/src/stage/scene.rs index cc0527f..921aa66 100644 --- a/src/stage/scene.rs +++ b/src/stage/scene.rs @@ -10,13 +10,13 @@ use crate::{math::vec::FGVec2, stage::{StagePoly, line::{StageCollision, StagePo pub fn test_scene() -> impl SceneList { let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[ - (FGVec2::lit("-5.6", "-0.35"), StagePolyLineSegmentType::Floor), - (FGVec2::lit("-3.92", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("-56.0", "-3.5"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("-39.2", "0.0"), StagePolyLineSegmentType::Floor), (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor), - (FGVec2::lit("3.92", "0.0"), StagePolyLineSegmentType::Floor), - (FGVec2::lit("5.6", "-0.35"), StagePolyLineSegmentType::Wall), - (FGVec2::lit("5.6", "-20.0"), StagePolyLineSegmentType::Ceiling), - (FGVec2::lit("-5.6", "-20.0"), StagePolyLineSegmentType::Wall) + (FGVec2::lit("39.2", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("56.0", "-3.5"), StagePolyLineSegmentType::Wall), + (FGVec2::lit("56.0", "-200.0"), StagePolyLineSegmentType::Ceiling), + (FGVec2::lit("-56", "-200.0"), StagePolyLineSegmentType::Wall) ]); bsn_list! [