diff --git a/Cargo.lock b/Cargo.lock index c74f8be..f23f59c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,36 @@ dependencies = [ "web-sys", ] +[[package]] +name = "bevy_asset_loader" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef4cba5fb56abd9842dece351e61f9c4f6beaa08a9e92760e5cd9472c43e14d" +dependencies = [ + "anyhow", + "bevy_app", + "bevy_asset", + "bevy_asset_loader_derive", + "bevy_ecs", + "bevy_log", + "bevy_platform", + "bevy_reflect", + "bevy_state", + "bevy_utils", + "path-slash", +] + +[[package]] +name = "bevy_asset_loader_derive" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c0596de511aeb25cd2d9e0680a67a9e51ce37962806a1c7aaa4b9ad2c6fd4ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "bevy_asset_macros" version = "0.19.0" @@ -3445,6 +3475,7 @@ dependencies = [ "avian3d", "bevy", "bevy-inspector-egui", + "bevy_asset_loader", "bevy_common_assets", "bevy_egui", "bevy_ggrs", @@ -5475,6 +5506,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "pathdiff" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 510b83f..980d001 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,11 @@ 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 = { version = "1.31.0", features = ["serde"] } +fixed = { version = "1.31.0", features = ["serde", "serde-str"] } strum = "0.28.0" strum_macros = "0.28.0" bevy_common_assets = { version = "0.17.0", features = ["ron"] } +bevy_asset_loader = "0.27.0" # Enable a large amount of optimization in the dev profile for dependencies. [profile.dev.package."*"] diff --git a/assets/fighters/test/test.fighter.ron b/assets/fighters/test/test.fighter.ron new file mode 100644 index 0000000..b6df1c8 --- /dev/null +++ b/assets/fighters/test/test.fighter.ron @@ -0,0 +1,31 @@ +( + id: TestFighter, + model_path: "fighters/test/test-fighter-normal.glb", + attributes: ( + base_walk_accel: "0.1", + stick_walk_accel: "0.16", + max_walk_vel: "0.7", + ground_friction: "0.09", + dash_acceleration_duration: 13, + dash_duration: 24, + base_dash_accel: "0.02", + stick_dash_accel: "0.065", + max_dash_vel: "1.1", + short_hop_vertical_velocity: "1.05", + full_jump_vertical_velocity: "1.6", + jump_horizontal_velocity: "0.7", + dash_initial_velocity: "1.4", + terminal_velocity: "2.8", + gravity: "0.23", + jumpsquat_duration: 4, + ), + animations: { + Wait: "PlyPurin5K_Share_ACTION_Wait", + Walk: "PlyPurin5K_Share_ACTION_WalkFast", + Run: "PlyPurin5K_Share_ACTION_Run", + Dash: "PlyPurin5K_Share_ACTION_Dash", + JumpSquat: "PlyPurin5K_Share_ACTION_Landing" + JumpForward: "PlyPurin5K_Share_ACTION_JumpF" + JumpBack: "PlyPurin5K_Share_ACTION_JumpB" + } +) diff --git a/assets/game_settings.ron b/assets/game_settings.ron new file mode 100644 index 0000000..6d99ce0 --- /dev/null +++ b/assets/game_settings.ron @@ -0,0 +1,20 @@ +( + input_common: ( + stick_deadzone: "0.28", + trigger_deadzone: "0.3", + smash_input_reset_axis_threshold: "0.25", + smash_input_axis_threshold: "0.8", + run_stick_threshold: "0.625", + smash_input_frame_threshold: 2, + input_buffer_size: 5 + + ), + fighter_common: ( + walk_speed_ease: "0.5", + ground_max_horizontal_velocity: "3.0", + ground_friction_over_walk_speed_multiplier: "2.0" + ), + fighter_manifest_paths: [ + "fighters/test/test.fighter.ron" + ] +) diff --git a/src/args.rs b/src/args.rs index e6e122d..465df30 100644 --- a/src/args.rs +++ b/src/args.rs @@ -11,5 +11,5 @@ pub struct Args { #[clap(long, default_value_t = 2)] pub check_distance: usize, #[clap(long, default_value_t = 1)] - pub players: usize + pub players: usize, } diff --git a/src/fighter/animation.rs b/src/fighter/animation.rs index 7dab5b4..8ecf825 100644 --- a/src/fighter/animation.rs +++ b/src/fighter/animation.rs @@ -1,9 +1,9 @@ -use std::{collections::HashMap, time::Duration}; +use std::time::Duration; use bevy::{mesh::skinning::SkinnedMesh, prelude::*}; use crate::{ - fighter::{FighterECB, FighterTranslation, FighterVisual, visual::FighterAnimations}, player::Player, + fighter::{FighterECB, FighterFacingDirection, FighterTranslation, FighterVisual, visual::FighterAnimations}, player::Player, }; #[derive(Component)] @@ -47,44 +47,43 @@ pub fn setup_fighter_animation_player( } } -pub fn apply_animation(children: Query<&mut Transform, With>) { - for mut trf in children { +pub fn apply_animation(children: Query<(&mut Transform, &FighterFacingDirection), With>) { + for (mut trf, facing_direction) in children { // Melee model scale is in decimeters trf.rotation = Quat::IDENTITY; - trf.rotate_local_y(std::f32::consts::PI * 0.5); + trf.rotate_local_y(std::f32::consts::PI * 0.5 * facing_direction.to_sign().to_num::()); } } pub fn animation_init( fighters: Query< - ( - &FighterAnimations, - &FighterAnimationPlayerLink, - ), - Added + (&FighterAnimations, &FighterAnimationPlayerLink), + Added, >, mut commands: Commands, - mut anim_player_query: Query<(&mut AnimationPlayer, &mut AnimationTransitions)> + mut anim_player_query: Query<(&mut AnimationPlayer, &mut AnimationTransitions)>, ) { for (anim, link) in fighters { - commands.entity(link.0).insert( - AnimationGraphHandle(anim.graph.clone()) - ); + 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(); + transitions + .play(&mut player, anim.clips[&AnimKind::Wait], Duration::ZERO) + .repeat(); } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)] +#[derive( + Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize, serde::Serialize, Reflect, +)] pub enum AnimKind { Wait, Walk, Run, Dash, + JumpSquat, } - -#[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 f2acbf6..b3eed2e 100644 --- a/src/fighter/attributes.rs +++ b/src/fighter/attributes.rs @@ -1,11 +1,13 @@ use bevy::prelude::*; +use serde::{Deserialize, Serialize}; use crate::{input::FighterInputFrame, math::int::FGi32}; /// Per-fighter tuning values. These are character data, not global rules — /// anything that applies to every fighter belongs in /// [`crate::game_settings::FighterSettingsCommon`] instead. -#[derive(Component, Clone, Copy)] +#[derive(Asset, Component, Clone, Copy, Reflect, Serialize, Deserialize)] +#[reflect(opaque)] pub struct FighterAttributes { pub base_walk_accel: FGi32, pub stick_walk_accel: FGi32, @@ -19,7 +21,9 @@ pub struct FighterAttributes { pub max_dash_vel: FGi32, pub jumpsquat_duration: u32, - pub jump_vertical_velocity: FGi32, + pub short_hop_vertical_velocity: FGi32, + pub full_jump_vertical_velocity: FGi32, + pub jump_horizontal_velocity: FGi32, pub ground_friction: FGi32, pub terminal_velocity: FGi32, @@ -27,7 +31,11 @@ pub struct FighterAttributes { } impl FighterAttributes { - pub fn get_accel_and_target_dashrun(&self, input: &FighterInputFrame, direction: FGi32) -> (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 + direction * self.base_dash_accel; let target_vel = input.movement.x * self.max_dash_vel; @@ -45,3 +53,7 @@ impl FighterAttributes { (accel, target_vel) } } + +pub struct FighterAttributesLink { + attributes: Handle, +} diff --git a/src/fighter/debug.rs b/src/fighter/debug.rs index c5b4f41..5f778b5 100644 --- a/src/fighter/debug.rs +++ b/src/fighter/debug.rs @@ -9,11 +9,24 @@ use crate::fighter::state::StateNameDebug; use crate::fighter::{FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity}; use crate::input::FighterInput; -pub fn debug_draw_ecb(ecbs: Query<(&FighterECB, &FighterTranslation, &FighterPreviousECB, &FighterPreviousTranslation)>, mut gizmos: Gizmos) { +pub fn debug_draw_ecb( + ecbs: Query<( + &FighterECB, + &FighterTranslation, + &FighterPreviousECB, + &FighterPreviousTranslation, + )>, + mut gizmos: Gizmos, +) { for (ecb, trf, ecb_prev, trf_prev) in ecbs { - let transformed = ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0)); + let transformed = + ecb.to_3d_lineloop(Transform::from_xyz(trf.x.to_num(), trf.y.to_num(), 0.0)); gizmos.lineloop(transformed, YELLOW); - let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz(trf_prev.x.to_num(), trf_prev.y.to_num(), -0.1)); + let prev_transformed = ecb_prev.to_3d_lineloop(Transform::from_xyz( + trf_prev.x.to_num(), + trf_prev.y.to_num(), + -0.1, + )); gizmos.lineloop(prev_transformed, ORANGE); } } @@ -44,4 +57,4 @@ pub fn update_config( 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 b4ba3b5..219140d 100644 --- a/src/fighter/ecb.rs +++ b/src/fighter/ecb.rs @@ -28,9 +28,7 @@ impl From for FighterPreviousECB { } } -pub fn snapshot_fighter_ecb( - fighters: Query<(&FighterECB, &mut FighterPreviousECB)>, -) { +pub fn snapshot_fighter_ecb(fighters: Query<(&FighterECB, &mut FighterPreviousECB)>) { for (ecb, mut previous_ecb) in fighters { *previous_ecb = (*ecb).into(); } diff --git a/src/fighter/manifest.rs b/src/fighter/manifest.rs new file mode 100644 index 0000000..cd08dbf --- /dev/null +++ b/src/fighter/manifest.rs @@ -0,0 +1,122 @@ +use std::collections::HashMap; + +use bevy::{asset::UntypedHandle, prelude::*}; +use bevy_asset_loader::asset_collection::AssetCollection; +use serde::{Deserialize, Serialize}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +use crate::{ + AppState, + fighter::{FighterAttributes, animation::AnimKind}, + game_settings::GameSettings, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect, EnumIter)] +pub enum FighterId { + TestFighter, +} + +#[derive(Serialize, Deserialize, Clone, Asset, Reflect)] +pub struct FighterManifest { + pub id: FighterId, + pub model_path: String, + pub attributes: FighterAttributes, + pub animations: HashMap, +} + +#[derive(Resource)] +pub struct FighterManifestAssets(pub Vec>); + +impl FighterManifestAssets { + fn requested(world: &World) -> Self { + let settings = world.resource::(); + let asset_server = world.resource::(); + + Self( + settings + .fighter_manifest_paths + .iter() + .map(|path| asset_server.load(path.clone())) + .collect(), + ) + } +} + +impl AssetCollection for FighterManifestAssets { + fn create(world: &mut World) -> Self { + Self::requested(world) + } + + fn load(world: &mut World) -> Vec { + Self::requested(world) + .0 + .into_iter() + .map(Handle::untyped) + .collect() + } +} + +#[derive(Resource)] +pub struct FighterManifestRegistry(HashMap>); + +impl FighterManifestRegistry { + pub fn get(&self, fighter: FighterId) -> Option<&Handle> { + self.0.get(&fighter) + } +} + +pub fn prepare_fighter_manifests( + mut commands: Commands, + handles: Res, + manifests: Res>, + mut next_state: ResMut>, +) { + let mut registry = HashMap::with_capacity(handles.0.len()); + + for handle in &handles.0 { + let Some(manifest) = manifests.get(handle) else { + fail(&mut next_state, "a loaded fighter manifest is unavailable"); + return; + }; + if manifest.model_path.is_empty() { + fail( + &mut next_state, + &format!("{:?} has an empty model path", manifest.id), + ); + return; + } + if !manifest.animations.contains_key(&AnimKind::Wait) { + fail( + &mut next_state, + &format!("{:?} has no Wait animation", manifest.id), + ); + return; + } + if registry.insert(manifest.id, handle.clone()).is_some() { + fail( + &mut next_state, + &format!("duplicate manifest for {:?}", manifest.id), + ); + return; + } + } + + for fighter in FighterId::iter() { + if !registry.contains_key(&fighter) { + fail( + &mut next_state, + &format!("no manifest configured for {fighter:?}"), + ); + return; + } + } + + commands.insert_resource(FighterManifestRegistry(registry)); + next_state.set(AppState::Idle); +} + +fn fail(next_state: &mut NextState, message: &str) { + error!("Fighter manifest preparation failed: {message}"); + next_state.set(AppState::CommonAssetLoadFailed); +} diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index 3bcdae0..7d14f11 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -10,6 +10,7 @@ pub mod attributes; pub mod collision; pub mod debug; pub mod ecb; +pub mod manifest; pub mod motion; pub mod state; pub mod visual; @@ -20,6 +21,7 @@ pub mod visual; // silently change what this module exports. pub use attributes::FighterAttributes; pub use ecb::FighterECB; +pub use manifest::FighterId; pub use motion::{FighterPreviousTranslation, FighterTranslation, FighterVelocity, Grounded}; use crate::{ @@ -41,12 +43,21 @@ use state::state_interrupt_system; #[derive(Default)] pub struct FighterPlugin; -#[derive(Component, Copy, Clone, Hash)] +#[derive(Component, Copy, Clone, Hash, Debug, PartialEq, Reflect)] pub enum FighterFacingDirection { Left, Right, } +impl FighterFacingDirection { + pub fn to_sign(&self) -> FGi32 { + match self { + FighterFacingDirection::Left => FGi32::NEG_ONE, + FighterFacingDirection::Right => FGi32::ONE, + } + } +} + #[derive(Component)] #[require(Transform)] pub struct FighterVisual; @@ -105,41 +116,32 @@ impl Plugin for FighterPlugin { .add_systems(EguiPrimaryContextPass, debug::fighter_debug) .add_systems(EguiPrimaryContextPass, debug::update_config) .rollback_component_with_clone::() - .checksum_component_with_hash::(); + .checksum_component_with_hash::() + .register_type::(); } } pub fn spawn_fighter( commands: &mut Commands, + player_handle: usize, + spawn_position: FGVec2, + attributes: FighterAttributes, animations: FighterAnimations, visual_root: WorldAssetRoot, ) { commands.spawn(( - Name("Fighter".into()), - Player { handle: 0 }, + Name(format!("Fighter {player_handle}").into()), + Player { + handle: player_handle, + }, 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, - }, + FighterTranslation(spawn_position), + attributes, visual_root, animations, FighterState::Fall(FallState), diff --git a/src/fighter/motion.rs b/src/fighter/motion.rs index 0985765..962ed5b 100644 --- a/src/fighter/motion.rs +++ b/src/fighter/motion.rs @@ -33,9 +33,7 @@ pub fn apply_air_motion( } } -pub fn copy_fighter_transform_to_visuals( - fighters: Query<(&FighterTranslation, &mut Transform)>, -) { +pub fn copy_fighter_transform_to_visuals(fighters: Query<(&FighterTranslation, &mut Transform)>) { for (translation, mut transform) in fighters { transform.translation = Vec3::new(translation.x.to_num(), translation.y.to_num(), 0.0); } diff --git a/src/fighter/state/air.rs b/src/fighter/state/air.rs index f09ffca..c08cf60 100644 --- a/src/fighter/state/air.rs +++ b/src/fighter/state/air.rs @@ -1,6 +1,6 @@ use bevy::prelude::*; -use crate::fighter::{*}; +use crate::fighter::*; pub fn integrate_gravity(velocity: &mut FighterVelocity, attributes: &FighterAttributes) { velocity.y -= attributes.gravity; @@ -15,4 +15,4 @@ pub fn apply_air_motion( // 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 bbd19cd..27e2469 100644 --- a/src/fighter/state/dash.rs +++ b/src/fighter/state/dash.rs @@ -3,13 +3,13 @@ use std::time::Duration; use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, ground, run, wait, walk}; +use super::{FighterStateContext, FighterStateImpl, ground, run, 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::state::wait::WaitState; -use crate::fighter::state::{FighterState}; -use crate::fighter::{FighterAttributes, FighterVelocity, collision}; +use crate::fighter::{FighterAttributes, FighterFacingDirection, FighterVelocity, collision}; use crate::game_settings::GameSettings; use crate::input::{ FighterCommands::{SmashMoveLeft, SmashMoveRight}, @@ -20,33 +20,30 @@ use crate::math::int::FGi32; #[derive(Debug, Clone)] pub struct DashState { frames_in_dash: u32, - direction: FGi32, - ground_common: GroundedStateCommon + direction: FighterFacingDirection, + ground_common: GroundedStateCommon, } impl std::hash::Hash for DashState { fn hash(&self, state: &mut H) { - self.direction.to_bits().hash(state); + self.direction.hash(state); self.frames_in_dash.hash(state); } } impl DashState { - pub fn new(direction: FGi32, ground_common: GroundedStateCommon) -> Self { + pub fn new(direction: FighterFacingDirection, ground_common: GroundedStateCommon) -> Self { Self { frames_in_dash: 0, direction, - ground_common + ground_common, } } } impl FighterStateImpl for DashState { const NAME: &'static str = "Dash"; - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { + fn check_interrupt(&self, state_context: &FighterStateContext) -> Option { if self.frames_in_dash > state_context.fighter_attribs.dash_duration { run::check_input(state_context, &self.ground_common) .or_else(|| walk::check_input(state_context, &self.ground_common)) @@ -58,13 +55,20 @@ impl FighterStateImpl for DashState { } else { check_smash_input_with_dir(state_context) .filter(|dir| *dir != self.direction) - .and_then(|dir| Some(FighterState::Dash(DashState::new(dir, self.ground_common.clone())))) + .and_then(|dir| { + Some(FighterState::Dash(DashState::new( + dir, + self.ground_common.clone(), + ))) + }) } } - + fn on_enter(&mut self, state_context: &mut FighterStateContext) { + *state_context.facing_direction = self.direction; state_context.input.clear_buffer(); - state_context.velocity.x = state_context.fighter_attribs.dash_initial_velocity * self.direction; + state_context.velocity.x = + state_context.fighter_attribs.dash_initial_velocity * self.direction.to_sign(); state_context.animation_transitions.play( &mut state_context.animation_player, @@ -72,14 +76,16 @@ impl FighterStateImpl for DashState { 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, self.direction); + let (accel, target_vel) = state_context + .fighter_attribs + .get_accel_and_target_dashrun(&last_frame, self.direction.to_sign()); let accel = ground::compute_ground_accel( accel, target_vel, @@ -100,11 +106,9 @@ impl FighterStateImpl for DashState { &mut self, state_context: &mut FighterStateContext, ) -> Option { - if let GroundedMotionResult::InAir = ground::collide_with_stage_grounded( - state_context, - &mut self.ground_common, - true - ) { + if let GroundedMotionResult::InAir = + ground::collide_with_stage_grounded(state_context, &mut self.ground_common, true) + { Some(FighterState::Fall(FallState)) } else { None @@ -112,16 +116,20 @@ impl FighterStateImpl for DashState { } } -fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { +fn check_smash_input_with_dir(state_context: &FighterStateContext) -> Option { if state_context.input.has_command(SmashMoveLeft) { - return Some(FGi32::NEG_ONE); + return Some(FighterFacingDirection::Left); } else if state_context.input.has_command(SmashMoveRight) { - return Some(FGi32::ONE); + return Some(FighterFacingDirection::Right); } else { None } } -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()))) +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()))) } diff --git a/src/fighter/state/fall.rs b/src/fighter/state/fall.rs index b0f4825..5c38c9a 100644 --- a/src/fighter/state/fall.rs +++ b/src/fighter/state/fall.rs @@ -1,9 +1,9 @@ use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, ground, walk}; -use crate::fighter::state::{FighterState, air}; +use super::{FighterStateContext, FighterStateImpl, ground, walk}; use crate::fighter::state::ground::GroundedStateCommon; use crate::fighter::state::wait::WaitState; +use crate::fighter::state::{FighterState, air}; use crate::fighter::{FighterAttributes, FighterVelocity, collision, motion}; use crate::game_settings::GameSettings; use crate::stage::line::StageCollision; @@ -13,18 +13,19 @@ pub struct FallState; impl FighterStateImpl for FallState { const NAME: &'static str = "Fall"; - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { + 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); + air::apply_air_motion( + state_context.velocity, + state_context.translation, + state_context.prev_translation, + ); } fn check_collision_interrupt( @@ -35,9 +36,9 @@ impl FighterStateImpl for FallState { // 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 + current_line_id: res.line_id, }; - Some(FighterState::Wait(WaitState { grounded_common } )) + Some(FighterState::Wait(WaitState { grounded_common })) } else { None } @@ -52,4 +53,3 @@ pub fn check_input(state_context: &FighterStateContext) -> Option } None } - diff --git a/src/fighter/state/ground.rs b/src/fighter/state/ground.rs index 69bb181..e292d5c 100644 --- a/src/fighter/state/ground.rs +++ b/src/fighter/state/ground.rs @@ -44,7 +44,7 @@ pub fn compute_ground_accel( } else { let mut accel = accel; let ground_max_horizontal_velocity = - game_settings.figher_common.ground_max_horizontal_velocity; + game_settings.fighter_common.ground_max_horizontal_velocity; if !(gr_vel * accel < FGi32::ZERO) { // accelerating, not reversing if accel > FGi32::ZERO { @@ -146,7 +146,7 @@ pub fn collide_with_stage_grounded( pub fn grounded_movement_common_interrupts( state_context: &FighterStateContext, - ground_common: &GroundedStateCommon + 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 f5c4c60..27fb9cb 100644 --- a/src/fighter/state/jump.rs +++ b/src/fighter/state/jump.rs @@ -1,12 +1,13 @@ +use std::time::Duration; + use bevy::prelude::*; use crate::{ - fighter::state::{ + fighter::{animation::AnimKind, state::{ FighterState, FighterStateContext, FighterStateImpl, fall::FallState, ground::{self, GroundedMotionResult, GroundedStateCommon}, - }, - input::FighterCommands, + }}, input::FighterCommands, }; pub enum JumpType { @@ -52,7 +53,7 @@ impl FighterStateImpl for JumpSquatState { state_context.fighter_attribs.ground_friction * state_context .game_settings - .figher_common + .fighter_common .ground_friction_over_walk_speed_multiplier } else { state_context.fighter_attribs.ground_friction @@ -81,6 +82,17 @@ impl FighterStateImpl for JumpSquatState { None } } + + fn play_animation( + &self, + transitions: &mut AnimationTransitions, + player: &mut AnimationPlayer, + anims: &crate::fighter::visual::FighterAnimations, + ) { + transitions.play(player, anims.clips[&AnimKind::JumpSquat], Duration::ZERO); + } + + fn on_enter(&mut self, _state_context: &mut FighterStateContext) {} } impl FighterStateImpl for JumpState { @@ -99,7 +111,5 @@ pub fn check_input( state_context .input .has_command(FighterCommands::Jump) - .then(|| FighterState::JumpSquat(JumpSquatState::create( - ground_common.clone(), - ))) + .then(|| FighterState::JumpSquat(JumpSquatState::create(ground_common.clone()))) } diff --git a/src/fighter/state/mod.rs b/src/fighter/state/mod.rs index f5737d8..e6cf6fa 100644 --- a/src/fighter/state/mod.rs +++ b/src/fighter/state/mod.rs @@ -9,7 +9,7 @@ use crate::fighter::animation::FighterAnimationPlayerLink; use crate::fighter::ecb::FighterPreviousECB; use crate::fighter::visual::FighterAnimations; use crate::fighter::{ - FighterAttributes, FighterECB, FighterPreviousTranslation, FighterTranslation, FighterVelocity, + FighterAttributes, FighterECB, FighterFacingDirection, FighterPreviousTranslation, FighterTranslation, FighterVelocity, }; use crate::game_settings::GameSettings; use crate::input::FighterInput; @@ -88,6 +88,7 @@ pub struct FighterStateContext<'a> { pub ecb: &'a mut FighterECB, pub prev_ecb: &'a mut FighterPreviousECB, pub fighter_attribs: &'a FighterAttributes, + pub facing_direction: &'a mut FighterFacingDirection, pub game_settings: &'a GameSettings, pub stage_collision: &'a StageCollision, pub animations: &'a FighterAnimations, @@ -114,6 +115,7 @@ pub struct FighterFrameQuery { pub velocity: &'static mut FighterVelocity, pub ecb: &'static mut FighterECB, pub prev_ecb: &'static mut FighterPreviousECB, + pub facing_direction: &'static mut FighterFacingDirection, pub attributes: &'static FighterAttributes, } @@ -133,6 +135,7 @@ impl<'w, 's> FighterFrameQueryItem<'w, 's> { velocity: &mut self.velocity, ecb: &mut self.ecb, prev_ecb: &mut self.prev_ecb, + facing_direction: &mut self.facing_direction, fighter_attribs: self.attributes, game_settings, stage_collision, diff --git a/src/fighter/state/run.rs b/src/fighter/state/run.rs index 6e55b68..bea19fe 100644 --- a/src/fighter/state/run.rs +++ b/src/fighter/state/run.rs @@ -2,7 +2,7 @@ use std::time::Duration; use bevy::prelude::*; -use super::{FighterStateImpl, FighterStateContext, ground, wait, walk}; +use super::{FighterStateContext, FighterStateImpl, ground, wait, walk}; use crate::fighter::animation::AnimKind; use crate::fighter::state::FighterState; use crate::fighter::state::fall::FallState; @@ -14,34 +14,39 @@ use crate::math::int::FGi32; #[derive(Component, Debug, Clone, Hash)] pub struct RunState { - pub grounded_common: GroundedStateCommon + pub grounded_common: GroundedStateCommon, } impl FighterStateImpl for RunState { const NAME: &'static str = "Run"; - fn check_interrupt( - &self, - state_context: &FighterStateContext, - ) -> Option { + fn check_interrupt(&self, 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.run_stick_threshold { + if input_frame.movement.x.abs() + >= state_context.game_settings.input_common.run_stick_threshold + { None } else { - walk::check_input(state_context, &self.grounded_common).or_else(|| wait::check_input(state_context, &self.grounded_common)) + 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) { - state_context.animation_transitions.play( - &mut state_context.animation_player, - state_context.animations.clips[&AnimKind::Run], - Duration::ZERO, - ).repeat(); + 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, FGi32::ONE); + 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, @@ -62,11 +67,9 @@ impl FighterStateImpl for RunState { &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 @@ -74,11 +77,16 @@ impl FighterStateImpl for RunState { } } -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.run_stick_threshold { - return Some(FighterState::Run(RunState { grounded_common: ground_state_common.clone() })); + return Some(FighterState::Run(RunState { + grounded_common: ground_state_common.clone(), + })); } None -} \ No newline at end of file +} diff --git a/src/fighter/state/wait.rs b/src/fighter/state/wait.rs index 67a9b68..7f72f23 100644 --- a/src/fighter/state/wait.rs +++ b/src/fighter/state/wait.rs @@ -4,11 +4,11 @@ use bevy::prelude::*; 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, grounded_movement_common_interrupts, }; +use crate::fighter::state::{FighterState, dash}; use crate::fighter::{FighterAttributes, FighterVelocity}; use crate::game_settings::GameSettings; use crate::stage::line::StageLineID; @@ -21,17 +21,21 @@ pub struct WaitState { impl FighterStateImpl for WaitState { const NAME: &'static str = "Wait"; 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))) + 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) { - state_context.animation_transitions.play( - &mut state_context.animation_player, - state_context.animations.clips[&AnimKind::Wait], - Duration::ZERO, - ).repeat(); + 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) { @@ -40,7 +44,7 @@ impl FighterStateImpl for WaitState { state_context.fighter_attribs.ground_friction * state_context .game_settings - .figher_common + .fighter_common .ground_friction_over_walk_speed_multiplier } else { state_context.fighter_attribs.ground_friction diff --git a/src/fighter/state/walk.rs b/src/fighter/state/walk.rs index 1570cf4..17dfea8 100644 --- a/src/fighter/state/walk.rs +++ b/src/fighter/state/walk.rs @@ -24,11 +24,14 @@ impl FighterStateImpl for WalkState { } 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(); + 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) { diff --git a/src/fighter/visual.rs b/src/fighter/visual.rs index b717008..1e80b86 100644 --- a/src/fighter/visual.rs +++ b/src/fighter/visual.rs @@ -1,98 +1,10 @@ -use bevy::gltf::Gltf; use bevy::prelude::*; use std::collections::HashMap; -use crate::{AppState, fighter::{ - animation::{AnimKind, AnimManifest}, - spawn_fighter, -}}; +use crate::fighter::animation::AnimKind; -// 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)] +#[derive(Component, Clone)] 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/game_settings.rs b/src/game_settings.rs index dd7e300..90ddf22 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -1,38 +1,48 @@ use bevy::prelude::*; +use bevy_asset_loader::asset_collection::AssetCollection; +use serde::{Deserialize, Serialize}; use crate::math::int::FGi32; +#[derive(Reflect, Serialize, Deserialize, Clone)] +#[reflect(opaque)] pub struct FighterSettingsCommon { pub walk_speed_ease: FGi32, pub ground_max_horizontal_velocity: FGi32, - pub ground_friction_over_walk_speed_multiplier: FGi32 + pub ground_friction_over_walk_speed_multiplier: FGi32, } +#[derive(Reflect, Serialize, Deserialize, Clone)] +#[reflect(opaque)] pub struct InputSettingsCommon { - pub stick_deadzone: f32, - pub trigger_deadzone: f32, - pub smash_input_reset_axis_threshold: f32, - pub smash_input_axis_threshold: f32, - pub run_stick_threshold: f32, + pub stick_deadzone: FGi32, + pub trigger_deadzone: FGi32, + pub smash_input_reset_axis_threshold: FGi32, + pub smash_input_axis_threshold: FGi32, + pub run_stick_threshold: FGi32, pub smash_input_frame_threshold: u8, pub input_buffer_size: u8, } -impl Default for InputSettingsCommon { - fn default() -> Self { - Self { - stick_deadzone: 0.28f32, - trigger_deadzone: 0.3f32, - smash_input_reset_axis_threshold: 0.25, - smash_input_axis_threshold: 0.8, - run_stick_threshold: 0.625, - smash_input_frame_threshold: 2, - input_buffer_size: 5 - } - } +#[derive(Reflect, Serialize, Deserialize, Resource, Asset, Clone)] +pub struct GameSettings { + pub fighter_common: FighterSettingsCommon, + pub input_common: InputSettingsCommon, + pub fighter_manifest_paths: Vec, } -#[derive(Resource)] -pub struct GameSettings { - pub figher_common: FighterSettingsCommon, - pub input_common: InputSettingsCommon -} \ No newline at end of file +#[derive(AssetCollection, Resource)] +pub struct CommonAssets { + #[asset(path = "game_settings.ron")] + game_settings: Handle, +} + +impl FromWorld for GameSettings { + fn from_world(world: &mut World) -> Self { + let game_settings_handle = world.resource::().game_settings.clone(); + world + .resource::>() + .get(&game_settings_handle) + .expect("GameSettings should already be loaded! Bug?") + .clone() + } +} diff --git a/src/input/buffer.rs b/src/input/buffer.rs index 493aeab..00baf39 100644 --- a/src/input/buffer.rs +++ b/src/input/buffer.rs @@ -100,7 +100,6 @@ pub fn postprocess_input( game_settings: Res, ) { for (mut input, player) in query { - for command in FighterCommands::iter() { input.reduce_command_lifetime(command); } @@ -128,7 +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); + 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 { diff --git a/src/input/frame.rs b/src/input/frame.rs index 0576282..825397a 100644 --- a/src/input/frame.rs +++ b/src/input/frame.rs @@ -9,5 +9,5 @@ use crate::{input::InputActionState, math::vec::FGVec2}; pub struct FighterInputFrame { pub movement: FGVec2, pub directional_attack: FGVec2, - pub jump: bool + pub jump: bool, } diff --git a/src/input/keyboard.rs b/src/input/keyboard.rs index 5e31380..d8220ec 100644 --- a/src/input/keyboard.rs +++ b/src/input/keyboard.rs @@ -44,9 +44,7 @@ pub fn preprocess_keyboard_input( InputMapAction::MovementYDir(sign) => { input_frame.movement.y += (sign) * action_strength } - InputMapAction::Jump => { - input_frame.jump = action_strength != FGi32::ZERO - }, + InputMapAction::Jump => input_frame.jump = action_strength != FGi32::ZERO, } } diff --git a/src/lib.rs b/src/lib.rs index 7b8a50c..dbe7803 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,21 +21,22 @@ //! save, click Rebuild in jackdaw (or run `jd build`) and it //! appears in `Add Component`. No registration code is needed. -use bevy::{mesh::SphereMeshBuilder, prelude::*}; +use bevy::prelude::*; +use bevy_asset_loader::prelude::*; use bevy_wind_waker_shader::prelude::*; use clap::Parser; -use jackdaw_runtime::prelude::*; +use jackdaw_runtime::EditorCategory; use crate::{ - fighter::{ - 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, + fighter::manifest::FighterManifest, game_settings::GameSettings, + schedule::GameplaySchedulePlugin, }; mod args; pub mod fighter; pub mod game_settings; pub mod input; +pub mod match_loading; mod math; pub mod netcode; pub mod player; @@ -45,8 +46,15 @@ pub mod stage; #[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)] pub enum AppState { #[default] - LoadingCharacters, - InGame, + LoadCommonAssets, + LoadFighterManifests, + PrepareFighterManifests, + CommonAssetLoadFailed, + Idle, + LoadingMatch, + PreparingMatch, + InMatch, + MatchLoadFailed, } /// Your game's Bevy plugin. The editor finds it by this name (override @@ -55,47 +63,57 @@ pub enum AppState { #[derive(Default)] pub struct GamePlugin; -pub fn setup(mut commands: Commands, asset_server: Res) { - // debug stage - - let _stage = commands.spawn(( - WorldAssetRoot(asset_server.load("stages/YoshiStory.glb#Scene0")), - )); -} - impl Plugin for GamePlugin { fn build(&self, app: &mut App) { // 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 - .init_state::() - .add_plugins(( - WindWakerShaderPlugin::default(), - // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. - netcode::FighterNetcodePlugin::default(), - GameplaySchedulePlugin, - input::FighterInputPlugin, - fighter::FighterPlugin, - stage::StagePlugin, - bevy_common_assets::ron::RonAssetPlugin::::new(&[]) - )) - .add_systems(Startup, setup) - .add_systems(Update, spin_cubes) - .insert_resource(args::Args::parse()) - .insert_resource(GameSettings { - figher_common: FighterSettingsCommon { - walk_speed_ease: FGi32::lit("0.5"), - 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)), - ); + app.init_state::() + .add_plugins(( + WindWakerShaderPlugin::default(), + // Brings up GGRS (and with it `GgrsSchedule`), so it goes first. + netcode::FighterNetcodePlugin::default(), + GameplaySchedulePlugin, + input::FighterInputPlugin, + fighter::FighterPlugin, + stage::StagePlugin, + bevy_common_assets::ron::RonAssetPlugin::::new(&["fighter.ron"]), + bevy_common_assets::ron::RonAssetPlugin::::new(&["ron"]), + )) + .add_loading_state( + LoadingState::new(AppState::LoadingMatch) + .continue_to_state(AppState::PreparingMatch) + .on_failure_continue_to_state(AppState::MatchLoadFailed) + .load_collection::(), + ) + .add_loading_state( + LoadingState::new(AppState::LoadCommonAssets) + .continue_to_state(AppState::LoadFighterManifests) + .on_failure_continue_to_state(AppState::CommonAssetLoadFailed) + .load_collection::() + .finally_init_resource::(), + ) + .add_loading_state( + LoadingState::new(AppState::LoadFighterManifests) + .continue_to_state(AppState::PrepareFighterManifests) + .on_failure_continue_to_state(AppState::CommonAssetLoadFailed) + .load_collection::(), + ) + .add_systems( + OnEnter(AppState::PrepareFighterManifests), + fighter::manifest::prepare_fighter_manifests, + ) + .add_systems( + OnEnter(AppState::Idle), + match_loading::initiate_default_match, + ) + .add_systems( + OnEnter(AppState::PreparingMatch), + match_loading::prepare_match, + ) + .add_systems(OnExit(AppState::InMatch), match_loading::cleanup_match) + .add_systems(Update, spin_cubes) + .insert_resource(args::Args::parse()); } } diff --git a/src/main.rs b/src/main.rs index d05ec75..b964ca8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,12 +5,10 @@ use bevy::camera_controller::free_camera::FreeCameraPlugin; use bevy::image::{ImageAddressMode, ImagePlugin, ImageSamplerDescriptor}; use bevy::prelude::*; +use bevy::remote::RemotePlugin; use bevy::remote::http::RemoteHttpPlugin; -use bevy::remote::{RemotePlugin}; use bevy_egui::EguiPlugin; -use bevy_inspector_egui::{ - quick::WorldInspectorPlugin -}; +use bevy_inspector_egui::quick::WorldInspectorPlugin; fn main() -> AppExit { // A `Repeat` sampler so the tileable materials authored in the diff --git a/src/match_loading.rs b/src/match_loading.rs new file mode 100644 index 0000000..bb7ee36 --- /dev/null +++ b/src/match_loading.rs @@ -0,0 +1,326 @@ +//! Match selection, asset loading, preparation, and cleanup. + +use std::collections::HashMap; + +use bevy::{asset::UntypedHandle, gltf::Gltf, prelude::*}; +use bevy_asset_loader::prelude::*; +use bevy_ggrs::prelude::Session; + +use crate::{ + AppState, + args::Args, + fighter::{ + FighterId, + animation::AnimKind, + manifest::{FighterManifest, FighterManifestRegistry}, + spawn_fighter, + visual::FighterAnimations, + }, + math::{int::FGi32, vec::FGVec2}, + netcode::{GGRSCfg, session::create_session}, + stage, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum StageId { + Battlefield, +} + +impl StageId { + fn scene_path(self) -> &'static str { + match self { + Self::Battlefield => "stages/battlefield.glb#Scene0", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MatchPlayer { + pub handle: usize, + pub fighter: FighterId, +} + +#[derive(Resource, Debug, Clone, PartialEq, Eq)] +pub struct PendingMatch { + pub stage: StageId, + pub players: Vec, +} + +pub struct SelectedFighterAssets { + pub fighter: FighterId, + pub model: Handle, +} + +#[derive(Resource)] +pub struct MatchAssets { + pub stage: Handle, + pub fighters: Vec, +} + +impl MatchAssets { + fn requested(world: &World) -> Self { + let request = world.resource::(); + let asset_server = world.resource::(); + let registry = world.resource::(); + let manifests = world.resource::>(); + + Self { + stage: asset_server.load(request.stage.scene_path()), + fighters: request + .players + .iter() + .map(|player| { + let handle = registry + .get(player.fighter) + .expect("startup validation guarantees every fighter has a manifest"); + let manifest = manifests + .get(handle) + .expect("startup-loaded fighter manifest should remain available"); + + SelectedFighterAssets { + fighter: player.fighter, + model: asset_server.load(manifest.model_path.clone()), + } + }) + .collect(), + } + } +} + +impl AssetCollection for MatchAssets { + fn create(world: &mut World) -> Self { + Self::requested(world) + } + + fn load(world: &mut World) -> Vec { + let assets = Self::requested(world); + let mut handles = Vec::with_capacity(1 + assets.fighters.len()); + handles.push(assets.stage.clone().untyped()); + for character in assets.fighters { + handles.push(character.model.untyped()); + } + handles + } +} + +#[derive(Component)] +pub struct MatchRoot; + +pub fn initiate_match( + commands: &mut Commands, + next_state: &mut NextState, + request: PendingMatch, +) { + commands.insert_resource(request); + next_state.set(AppState::LoadingMatch); +} + +pub fn initiate_default_match( + mut commands: Commands, + args: Res, + mut next_state: ResMut>, +) { + let players = (0..args.players) + .map(|handle| MatchPlayer { + handle, + fighter: FighterId::TestFighter, + }) + .collect(); + + initiate_match( + &mut commands, + &mut next_state, + PendingMatch { + stage: StageId::Battlefield, + players, + }, + ); +} + +pub fn prepare_match( + mut commands: Commands, + request: Res, + assets: Res, + gltfs: Res>, + registry: Res, + manifests: Res>, + mut graphs: ResMut>, + args: Res, + mut next_state: ResMut>, +) { + if !valid_player_handles(&request.players) { + fail_match( + &mut next_state, + "player handles must be unique and cover 0..player_count", + ); + return; + } + + if assets.fighters.len() != request.players.len() { + fail_match( + &mut next_state, + "loaded character assets do not match the requested player slots", + ); + return; + } + + let mut prepared = Vec::with_capacity(assets.fighters.len()); + let mut shared_animations = HashMap::::new(); + + for character in &assets.fighters { + let manifest_handle = registry + .get(character.fighter) + .expect("startup validation guarantees every fighter has a manifest"); + let Some(manifest) = manifests.get(manifest_handle) else { + fail_match(&mut next_state, "a fighter manifest is unavailable"); + return; + }; + let Some(gltf) = gltfs.get(&character.model) else { + fail_match(&mut next_state, "a loaded character GLTF is unavailable"); + return; + }; + let Some(scene) = gltf.scenes.first().cloned() else { + fail_match(&mut next_state, "a character GLTF contains no scene"); + return; + }; + + if let Some(animations) = shared_animations.get(&character.fighter) { + prepared.push((scene, animations.clone(), manifest.attributes)); + continue; + } + + let mut graph = AnimationGraph::new(); + let mut clips = HashMap::::new(); + + for (kind, clip_name) in &manifest.animations { + let Some(clip) = gltf.named_animations.get(clip_name.as_str()) else { + fail_match( + &mut next_state, + &format!( + "{:?} animation manifest references missing clip '{clip_name}'", + character.fighter + ), + ); + return; + }; + clips.insert(*kind, graph.add_clip(clip.clone(), 1.0, graph.root)); + } + + if !clips.contains_key(&AnimKind::Wait) { + fail_match(&mut next_state, "a character has no Wait animation"); + return; + } + + let animations = FighterAnimations { + graph: graphs.add(graph), + clips, + }; + shared_animations.insert(character.fighter, animations.clone()); + prepared.push((scene, animations, manifest.attributes)); + } + + let session = match create_session(&args, request.players.len()) { + Ok(session) => session, + Err(error) => { + fail_match( + &mut next_state, + &format!("failed to create GGRS session: {error}"), + ); + return; + } + }; + + let root = commands.spawn((Name::new("Match"), MatchRoot)).id(); + stage::spawn_stage(&mut commands, root, assets.stage.clone()); + + for (index, (player, (scene, animations, attributes))) in + request.players.iter().zip(prepared).enumerate() + { + let spawn_x = (index as i32 * 2 + 1 - request.players.len() as i32) * 5; + spawn_fighter( + &mut commands, + player.handle, + FGVec2::new(FGi32::from_num(spawn_x), FGi32::lit("12.5")), + attributes, + animations, + WorldAssetRoot(scene), + ); + } + + commands.insert_resource::>(session); + next_state.set(AppState::InMatch); +} + +fn fail_match(next_state: &mut NextState, message: &str) { + error!("Match preparation failed: {message}"); + next_state.set(AppState::MatchLoadFailed); +} + +fn valid_player_handles(players: &[MatchPlayer]) -> bool { + let mut handles = players + .iter() + .map(|player| player.handle) + .collect::>(); + handles.sort_unstable(); + handles == (0..players.len()).collect::>() +} + +pub fn cleanup_match( + mut commands: Commands, + roots: Query>, + fighters: Query>, +) { + for root in &roots { + commands.entity(root).despawn(); + } + for fighter in &fighters { + commands.entity(fighter).despawn(); + } + commands.remove_resource::>(); + commands.remove_resource::(); + commands.remove_resource::(); + commands.remove_resource::(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn match_request_keeps_duplicate_fighter_slots() { + let request = PendingMatch { + stage: StageId::Battlefield, + players: vec![ + MatchPlayer { + handle: 0, + fighter: FighterId::TestFighter, + }, + MatchPlayer { + handle: 1, + fighter: FighterId::TestFighter, + }, + ], + }; + + assert_eq!(request.players.len(), 2); + assert_eq!(request.players[0].handle, 0); + assert_eq!(request.players[1].handle, 1); + assert!(valid_player_handles(&request.players)); + } + + #[test] + fn player_handles_must_match_the_session_range() { + let players = [ + MatchPlayer { + handle: 0, + fighter: FighterId::TestFighter, + }, + MatchPlayer { + handle: 0, + fighter: FighterId::TestFighter, + }, + ]; + + assert!(!valid_player_handles(&players)); + } +} diff --git a/src/math/mod.rs b/src/math/mod.rs index b4886ff..3d47442 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -1,3 +1,3 @@ pub mod int; pub mod segment; -pub mod vec; \ No newline at end of file +pub mod vec; diff --git a/src/math/segment.rs b/src/math/segment.rs index 90c522c..6cf2b25 100644 --- a/src/math/segment.rs +++ b/src/math/segment.rs @@ -11,15 +11,13 @@ use crate::math::{ #[derive(Clone, Copy, PartialEq, Reflect)] pub struct FGSegment2d { point1: FGVec2, - point2: FGVec2 + point2: FGVec2, } impl FGSegment2d { #[inline] pub fn new(point1: FGVec2, point2: FGVec2) -> Self { - Self { - point1, point2 - } + Self { point1, point2 } } #[inline] @@ -41,7 +39,8 @@ impl FGSegment2d { /// Panics if a valid direction could not be computed, for example when the endpoints are coincident, NaN, or infinite. #[inline] pub fn direction(&self) -> FGVec2 { - self.try_direction().expect("Failed to compute the direction of a line segment") + self.try_direction() + .expect("Failed to compute the direction of a line segment") } /// Try to compute the normalized direction pointing from the first endpoint to the second endpoint. @@ -100,7 +99,10 @@ impl FGSegment2d { /// /// Panics if a valid normal could not be computed, for example when the endpoints are coincident pub fn left_normal(&self) -> FGVec2 { - self.point1.direction_to(self.point2).map(|o| { FGVec2::new(-o.y, o.x)}).expect("a valid normal could not be computed") + self.point1 + .direction_to(self.point2) + .map(|o| FGVec2::new(-o.y, o.x)) + .expect("a valid normal could not be computed") } pub fn segment_intersection(&self, rhs: &FGSegment2d) -> Option { @@ -150,9 +152,18 @@ mod tests { fn closest_point_projects_and_clamps_to_segment() { let segment = FGSegment2d::new(FGVec2::lit("-4", "0"), FGVec2::lit("4", "0")); - assert_eq!(segment.closest_point(FGVec2::lit("2", "1")), FGVec2::lit("2", "0")); - assert_eq!(segment.closest_point(FGVec2::lit("-6", "1")), segment.point1()); - assert_eq!(segment.closest_point(FGVec2::lit("6", "1")), segment.point2()); + assert_eq!( + segment.closest_point(FGVec2::lit("2", "1")), + FGVec2::lit("2", "0") + ); + assert_eq!( + segment.closest_point(FGVec2::lit("-6", "1")), + segment.point1() + ); + assert_eq!( + segment.closest_point(FGVec2::lit("6", "1")), + segment.point2() + ); } #[test] diff --git a/src/math/vec.rs b/src/math/vec.rs index b5d21fb..84d483a 100644 --- a/src/math/vec.rs +++ b/src/math/vec.rs @@ -2,7 +2,7 @@ use bevy::math::Vec2; use bevy::prelude::*; use fixed::types::I32F32; use serde::{Deserialize, Serialize}; -use std::ops::{AddAssign, Add, Div, Mul, Sub}; +use std::ops::{Add, AddAssign, Div, Mul, Sub}; use super::int::{FGWide, FGi32}; @@ -29,7 +29,10 @@ impl FGVec2 { #[inline(always)] pub const fn lit(x: &str, y: &str) -> Self { - Self { x: FGi32::lit(x), y: FGi32::lit(y) } + Self { + x: FGi32::lit(x), + y: FGi32::lit(y), + } } #[inline(always)] @@ -213,7 +216,7 @@ impl AddAssign for FGVec2 { fn add_assign(&mut self, rhs: Self) { *self = Self { x: self.x + rhs.x, - y: self.y + rhs.y + y: self.y + rhs.y, } } } @@ -251,8 +254,12 @@ mod is_normalized_tests { let x = mag * theta.cos(); let y = mag * theta.sin(); - let Some(fx) = FGi32::checked_from_num(x) else { continue }; - let Some(fy) = FGi32::checked_from_num(y) else { continue }; + let Some(fx) = FGi32::checked_from_num(x) else { + continue; + }; + let Some(fy) = FGi32::checked_from_num(y) else { + continue; + }; let v = FGVec2 { x: fx, y: fy }; @@ -301,24 +308,36 @@ mod is_normalized_tests { for &mag in &[0.01, 1.0, 100.0, 10000.0] { let x = mag * theta.cos(); let y = mag * theta.sin(); - let Some(fx) = FGi32::checked_from_num(x) else { continue }; - let Some(fy) = FGi32::checked_from_num(y) else { continue }; + let Some(fx) = FGi32::checked_from_num(x) else { + continue; + }; + let Some(fy) = FGi32::checked_from_num(y) else { + continue; + }; let v = FGVec2 { x: fx, y: fy }; if let Some(normalized) = v.normalize() { assert!( normalized.is_normalized(), "normalized({}, {}) failed is_normalized(): length_squared = {:?}", - x, y, normalized.length_squared() + x, + y, + normalized.length_squared() ); } } } - let non_unit = FGVec2 { x: FGi32::from_num(2.0), y: FGi32::from_num(0.0) }; + let non_unit = FGVec2 { + x: FGi32::from_num(2.0), + y: FGi32::from_num(0.0), + }; assert!(!non_unit.is_normalized()); - let non_unit_small = FGVec2 { x: FGi32::from_num(0.5), y: FGi32::from_num(0.0) }; + let non_unit_small = FGVec2 { + x: FGi32::from_num(0.5), + y: FGi32::from_num(0.0), + }; assert!(!non_unit_small.is_normalized()); } diff --git a/src/netcode/debug.rs b/src/netcode/debug.rs index bd9f095..bb63bfa 100644 --- a/src/netcode/debug.rs +++ b/src/netcode/debug.rs @@ -6,10 +6,7 @@ use bevy_ggrs::prelude::*; use crate::netcode::GGRSCfg; -pub fn network_debug( - mut contexts: EguiContexts, - session: Option>>, -) -> Result { +pub fn network_debug(mut contexts: EguiContexts, session: Option>>) -> Result { if let Some(sess) = session { match sess.as_ref() { Session::P2P(s) => { diff --git a/src/netcode/mod.rs b/src/netcode/mod.rs index e146860..5265bb6 100644 --- a/src/netcode/mod.rs +++ b/src/netcode/mod.rs @@ -19,7 +19,6 @@ impl Plugin for FighterNetcodePlugin { fn build(&self, app: &mut App) { let plugin = GgrsPlugin::::default(); app.add_plugins(plugin) - .add_systems(Startup, session::netcode_setup) // Engine-level components that every rolled-back entity may carry. // Gameplay components are registered by the plugin that owns them. .rollback_component_with_clone::() diff --git a/src/netcode/session.rs b/src/netcode/session.rs index 1c0bb28..e85e15f 100644 --- a/src/netcode/session.rs +++ b/src/netcode/session.rs @@ -6,31 +6,32 @@ use bevy_ggrs::{ggrs::UdpNonBlockingSocket, prelude::*}; use crate::args::Args; use crate::netcode::GGRSCfg; -pub fn netcode_setup(mut commands: Commands, args: Res) -> Result { +pub fn create_session(args: &Args, player_count: usize) -> Result> { let mut sess_build = SessionBuilder::::new() - .with_num_players(args.players)? + .with_num_players(player_count)? .with_check_distance(args.check_distance) .with_input_delay(args.input_delay); // (optional) set input delay for the local player - for i in 0..args.players { + for i in 0..player_count { sess_build = sess_build.add_player(PlayerType::Local, i)?; } if args.synctest { println!("Start synctest!"); let sess = sess_build.start_synctest_session()?; - commands.insert_resource(Session::SyncTest(sess)); + Ok(Session::SyncTest(sess)) } else { println!("Start p2p!"); let socket = UdpNonBlockingSocket::bind_to_port(6969)?; let sess = sess_build.start_p2p_session(socket)?; - commands.insert_resource(Session::P2P(sess)); + Ok(Session::P2P(sess)) } - - Ok(()) } -pub fn print_events_system(mut session: ResMut>) { +pub fn print_events_system(session: Option>>) { + let Some(mut session) = session else { + return; + }; match session.as_mut() { Session::P2P(s) => { for event in s.events() { diff --git a/src/schedule.rs b/src/schedule.rs index 4758bad..63a16ca 100644 --- a/src/schedule.rs +++ b/src/schedule.rs @@ -21,7 +21,7 @@ pub enum GameplaySet { Physics, /// Resolve the integrated positions against the stage. Collision, - Animation + Animation, } /// Declares the order of [`GameplaySet`] inside [`GgrsSchedule`]. Added by diff --git a/src/stage/debug.rs b/src/stage/debug.rs index 838fd1a..074e6c8 100644 --- a/src/stage/debug.rs +++ b/src/stage/debug.rs @@ -14,6 +14,5 @@ pub fn debug_draw_scene(mut gizmos: Gizmos, stage_collision: Res 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 07260f9..737b345 100644 --- a/src/stage/line.rs +++ b/src/stage/line.rs @@ -4,51 +4,52 @@ use std::default; use bevy::{math::InvalidDirectionError, prelude::*}; -use crate::math::{segment::{FGSegment2d}, vec::FGVec2}; +use crate::math::{segment::FGSegment2d, vec::FGVec2}; #[derive(Reflect, Clone, Copy, PartialEq)] pub enum StagePolyLineSegmentType { - Floor, Wall, Ceiling + Floor, + Wall, + Ceiling, } - #[derive(Reflect, Clone)] pub struct StagePolyLineSegment { pub segment: FGSegment2d, pub normal: FGVec2, - pub segment_type: StagePolyLineSegmentType + pub segment_type: StagePolyLineSegmentType, } #[derive(Reflect, Clone, Default)] pub enum StagePolyType { #[default] Closed, - Platform + Platform, } #[derive(Reflect, Hash, Debug, Clone, Copy, Default)] pub struct StageLineID { pub polygon: usize, - pub segment: usize + pub segment: usize, } #[derive(Component, Reflect, Clone, Default)] #[reflect(Component)] pub struct StagePoly { poly_type: StagePolyType, - pub segments: Vec + pub segments: Vec, } pub struct StagePlaneIntersectResult { pub position: FGVec2, pub segment_idx: usize, - pub normal: FGVec2 + pub normal: FGVec2, } #[derive(Resource, Reflect, Clone, Default)] #[reflect(Resource)] pub struct StageCollision { - pub stage_polys: Vec + pub stage_polys: Vec, } impl StagePoly { @@ -134,20 +135,22 @@ impl StagePoly { .zip(points.iter().cycle().skip(1)) .take(segment_count); - let segments = windows.map(|segment_window| { - let (first_point, segment_type) = segment_window.0; - let (second_point, _) = segment_window.1; - let segment = FGSegment2d::new(*first_point, *second_point); - StagePolyLineSegment { - segment_type: *segment_type, - segment, - normal: segment.left_normal() - } - }).collect(); + let segments = windows + .map(|segment_window| { + let (first_point, segment_type) = segment_window.0; + let (second_point, _) = segment_window.1; + let segment = FGSegment2d::new(*first_point, *second_point); + StagePolyLineSegment { + segment_type: *segment_type, + segment, + normal: segment.left_normal(), + } + }) + .collect(); Self { poly_type, - segments + segments, } } } @@ -205,7 +208,10 @@ mod tests { }; let hit = stage_poly - .intersect_ray(FGSegment2d::new(FGVec2::lit("0", "2"), FGVec2::lit("0", "-1"))) + .intersect_ray(FGSegment2d::new( + FGVec2::lit("0", "2"), + FGVec2::lit("0", "-1"), + )) .unwrap(); assert_eq!(hit.segment_idx, 1); @@ -214,35 +220,53 @@ mod tests { #[test] fn ray_intersection_test() { - let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ - (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor), - (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor), - ]); + let stage_poly = StagePoly::build( + crate::stage::line::StagePolyType::Platform, + &[ + (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor), + ], + ); - let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("0.0", "1.0"), FGVec2::lit("0.0", "-1.0"))); + let res = stage_poly.intersect_ray(FGSegment2d::new( + FGVec2::lit("0.0", "1.0"), + FGVec2::lit("0.0", "-1.0"), + )); assert!(res.is_some()); assert_eq!(res.unwrap().position, FGVec2::ZERO); } #[test] fn ray_intersection_test_side() { - let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ - (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor), - (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor), - ]); + let stage_poly = StagePoly::build( + crate::stage::line::StagePolyType::Platform, + &[ + (FGVec2::lit("-4.5", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("4.5", "0.0"), StagePolyLineSegmentType::Floor), + ], + ); - let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("2.0", "1.0"), FGVec2::lit("2.0", "-1.0"))); + let res = stage_poly.intersect_ray(FGSegment2d::new( + FGVec2::lit("2.0", "1.0"), + FGVec2::lit("2.0", "-1.0"), + )); assert!(res.is_some()); assert!(res.unwrap().position.distance(FGVec2::lit("2.0", "0.0")) < FGi32::from_bits(4)); } #[test] fn ray_intersection_test_diagonal() { - let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Platform, &[ - (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor), - (FGVec2::lit("2.0", "2.0"), StagePolyLineSegmentType::Floor), - ]); + let stage_poly = StagePoly::build( + crate::stage::line::StagePolyType::Platform, + &[ + (FGVec2::lit("0.0", "0.0"), StagePolyLineSegmentType::Floor), + (FGVec2::lit("2.0", "2.0"), StagePolyLineSegmentType::Floor), + ], + ); - let res = stage_poly.intersect_ray(FGSegment2d::new(FGVec2::lit("1.0", "2.0"), FGVec2::lit("1.0", "-2.0"))); + let res = stage_poly.intersect_ray(FGSegment2d::new( + FGVec2::lit("1.0", "2.0"), + FGVec2::lit("1.0", "-2.0"), + )); assert!(res.is_some()); assert_eq!(res.unwrap().position, FGVec2::lit("1.0", "1.0")); } diff --git a/src/stage/mod.rs b/src/stage/mod.rs index b16a612..d60125f 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::{StagePoly, StagePlaneIntersectResult}; +pub use line::{StagePlaneIntersectResult, StagePoly}; /// Owns the stage geometry: which scene gets spawned and how it is drawn. #[derive(Default)] @@ -15,7 +15,14 @@ pub struct StagePlugin; impl Plugin for StagePlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, scene::test_scene.spawn()) - .add_systems(Update, debug::debug_draw_scene); + app.add_systems( + Update, + debug::debug_draw_scene.run_if(resource_exists::), + ); } } + +pub fn spawn_stage(commands: &mut Commands, match_root: Entity, visual: Handle) { + commands.entity(match_root).insert(WorldAssetRoot(visual)); + scene::spawn_stage_support(commands, match_root); +} diff --git a/src/stage/scene.rs b/src/stage/scene.rs index 921aa66..0599d49 100644 --- a/src/stage/scene.rs +++ b/src/stage/scene.rs @@ -1,49 +1,67 @@ -//! The scene content that gets spawned at startup. +//! Code-defined deterministic stage collision and development lighting. use std::f32::consts::PI; use bevy::{camera_controller::free_camera::FreeCamera, prelude::*}; -use crate::{math::vec::FGVec2, stage::{StagePoly, line::{StageCollision, StagePolyLineSegmentType}}}; +use crate::{ + math::vec::FGVec2, + stage::{ + StagePoly, + line::{StageCollision, StagePolyLineSegmentType, StagePolyType}, + }, +}; -/// set up a simple 3D scene -pub fn test_scene() -> impl SceneList { +pub fn spawn_stage_support(commands: &mut Commands, match_root: Entity) { + let stage_poly = StagePoly::build( + StagePolyType::Closed, + &[ + ( + 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("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), + ], + ); - let stage_poly = StagePoly::build(crate::stage::line::StagePolyType::Closed, &[ - (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("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! [ - ( - #StageCollision - template_value(StageCollision { - stage_polys: vec!(stage_poly) - }) - ), - ( - DirectionalLight { - shadow_maps_enabled: true, - } - 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 { - sensitivity: 0.2, - friction: 25.0, - walk_speed: 3.0, - run_speed: 9.0, - } - ) - ] + commands.insert_resource(StageCollision { + stage_polys: vec![stage_poly.clone()], + }); + commands.spawn(( + Name::new("Stage Collision"), + stage_poly, + ChildOf(match_root), + )); + commands.spawn(( + DirectionalLight { + shadow_maps_enabled: true, + ..default() + }, + Transform { + translation: Vec3::new(0.0, 2.0, 0.0), + rotation: Quat::from_rotation_x(-PI / 4.0), + ..default() + }, + ChildOf(match_root), + )); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 10.5, 150.0), + /*FreeCamera { + sensitivity: 0.2, + friction: 25.0, + walk_speed: 3.0, + run_speed: 9.0, + ..default() + },*/ + ChildOf(match_root), + )); }