diff --git a/assets/game_settings.ron b/assets/game_settings.ron index 3bd58d2..81090eb 100644 --- a/assets/game_settings.ron +++ b/assets/game_settings.ron @@ -14,6 +14,9 @@ ground_max_horizontal_velocity: "3.0", ground_friction_over_walk_speed_multiplier: "2.0" ), + stage_manifest_paths: [ + "stages/test.stage.ron" + ], fighter_manifest_paths: [ "fighters/test/test.fighter.ron" ] diff --git a/assets/stages/test.stage.ron b/assets/stages/test.stage.ron new file mode 100644 index 0000000..7cab389 --- /dev/null +++ b/assets/stages/test.stage.ron @@ -0,0 +1,65 @@ +( + id: TestStage, + model_path: "stages/battlefield.glb#Scene0", + polygons: [ + ( + poly_type: Platform, + lines: [ + ( + (x: "-59.5", y: "23.45"), + Floor + ), + ( + (x: "-28.0", y: "23.45"), + Floor + ) + ] + ), + ( + poly_type: Platform, + lines: [ + ( + (x: "28.0", y: "23.45"), + Floor + ), + ( + (x: "59.5", y: "23.45"), + Floor + ) + ] + ), + ( + poly_type: Closed, + lines: [ + ( + (x: "-56", y: "-200.0"), + Wall + ), + ( + (x: "-56.0", y: "-3.5"), + Floor, + ), + ( + (x: "-39.2", y: "0.0"), + Floor, + ), + ( + (x: "0.0", y: "0.0"), + Floor + ), + ( + (x: "39.2", y: "0.0"), + Floor + ), + ( + (x: "56.0", y: "-3.5"), + Wall + ), + ( + (x: "56", y: "-200.0"), + Wall + ), + ] + ) + ] +) \ No newline at end of file diff --git a/src/fighter/manifest.rs b/src/fighter/manifest.rs index cd08dbf..49f39a1 100644 --- a/src/fighter/manifest.rs +++ b/src/fighter/manifest.rs @@ -113,7 +113,7 @@ pub fn prepare_fighter_manifests( } commands.insert_resource(FighterManifestRegistry(registry)); - next_state.set(AppState::Idle); + next_state.set(AppState::LoadStageManifests); } fn fail(next_state: &mut NextState, message: &str) { diff --git a/src/game_settings.rs b/src/game_settings.rs index 90ddf22..5850f4b 100644 --- a/src/game_settings.rs +++ b/src/game_settings.rs @@ -28,6 +28,7 @@ pub struct GameSettings { pub fighter_common: FighterSettingsCommon, pub input_common: InputSettingsCommon, pub fighter_manifest_paths: Vec, + pub stage_manifest_paths: Vec, } #[derive(AssetCollection, Resource)] diff --git a/src/lib.rs b/src/lib.rs index dbe7803..bd3c20d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,8 +28,7 @@ use clap::Parser; use jackdaw_runtime::EditorCategory; use crate::{ - fighter::manifest::FighterManifest, game_settings::GameSettings, - schedule::GameplaySchedulePlugin, + fighter::manifest::FighterManifest, game_settings::GameSettings, schedule::GameplaySchedulePlugin, stage::manifest::{StageManifest, StageManifestAssets}, }; mod args; @@ -49,6 +48,8 @@ pub enum AppState { LoadCommonAssets, LoadFighterManifests, PrepareFighterManifests, + LoadStageManifests, + PrepareStageManifests, CommonAssetLoadFailed, Idle, LoadingMatch, @@ -78,6 +79,7 @@ impl Plugin for GamePlugin { fighter::FighterPlugin, stage::StagePlugin, bevy_common_assets::ron::RonAssetPlugin::::new(&["fighter.ron"]), + bevy_common_assets::ron::RonAssetPlugin::::new(&["stage.ron"]), bevy_common_assets::ron::RonAssetPlugin::::new(&["ron"]), )) .add_loading_state( @@ -99,10 +101,20 @@ impl Plugin for GamePlugin { .on_failure_continue_to_state(AppState::CommonAssetLoadFailed) .load_collection::(), ) + .add_loading_state( + LoadingState::new(AppState::LoadStageManifests) + .continue_to_state(AppState::PrepareStageManifests) + .on_failure_continue_to_state(AppState::CommonAssetLoadFailed) + .load_collection::(), + ) .add_systems( OnEnter(AppState::PrepareFighterManifests), fighter::manifest::prepare_fighter_manifests, ) + .add_systems( + OnEnter(AppState::PrepareStageManifests), + stage::manifest::prepare_stage_manifests, + ) .add_systems( OnEnter(AppState::Idle), match_loading::initiate_default_match, diff --git a/src/match_loading.rs b/src/match_loading.rs index bb7ee36..982520c 100644 --- a/src/match_loading.rs +++ b/src/match_loading.rs @@ -18,22 +18,12 @@ use crate::{ }, math::{int::FGi32, vec::FGVec2}, netcode::{GGRSCfg, session::create_session}, - stage, + stage::{ + self, + manifest::{StageId, StageManifest, StageManifestRegistry}, + }, }; -#[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, @@ -62,10 +52,19 @@ impl MatchAssets { let request = world.resource::(); let asset_server = world.resource::(); let registry = world.resource::(); + let stage_registry = world.resource::(); let manifests = world.resource::>(); + let stage_manifests = world.resource::>(); + + let stage_handle = stage_registry + .get(request.stage) + .expect("Startup validation guarantees every stage has a manifest"); + let stage_manifest = stage_manifests + .get(&stage_handle) + .expect("startup-loaded stage manifest should remain available"); Self { - stage: asset_server.load(request.stage.scene_path()), + stage: asset_server.load(stage_manifest.model_path.clone()), fighters: request .players .iter() @@ -131,7 +130,7 @@ pub fn initiate_default_match( &mut commands, &mut next_state, PendingMatch { - stage: StageId::Battlefield, + stage: StageId::TestStage, players, }, ); @@ -143,7 +142,9 @@ pub fn prepare_match( assets: Res, gltfs: Res>, registry: Res, + stage_registry: Res, manifests: Res>, + stage_manifests: Res>, mut graphs: ResMut>, args: Res, mut next_state: ResMut>, @@ -231,7 +232,10 @@ pub fn prepare_match( }; let root = commands.spawn((Name::new("Match"), MatchRoot)).id(); - stage::spawn_stage(&mut commands, root, assets.stage.clone()); + + let stage_handle = stage_registry.get(request.stage).expect("Initialization check should ensure all stages exist"); + let stage_manifest = stage_manifests.get(&stage_handle).expect("Initialization check should ensure stage manifests are kept alive"); + stage::spawn_stage(&mut commands, root, assets.stage.clone(), stage_manifest.to_collision()); for (index, (player, (scene, animations, attributes))) in request.players.iter().zip(prepared).enumerate() @@ -289,7 +293,7 @@ mod tests { #[test] fn match_request_keeps_duplicate_fighter_slots() { let request = PendingMatch { - stage: StageId::Battlefield, + stage: StageId::TestStage, players: vec![ MatchPlayer { handle: 0, diff --git a/src/stage/line.rs b/src/stage/line.rs index bd00be5..17e8e0d 100644 --- a/src/stage/line.rs +++ b/src/stage/line.rs @@ -2,10 +2,11 @@ use bevy::prelude::*; +use serde::{Deserialize, Serialize}; use crate::math::{segment::FGSegment2d, vec::FGVec2}; -#[derive(Reflect, Clone, Copy, PartialEq)] +#[derive(Reflect, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum StagePolyLineSegmentType { Floor, Wall, @@ -19,7 +20,7 @@ pub struct StagePolyLineSegment { pub segment_type: StagePolyLineSegmentType, } -#[derive(Reflect, Clone, Default)] +#[derive(Reflect, Clone, Default, Serialize, Deserialize, Copy)] pub enum StagePolyType { #[default] Closed, @@ -32,8 +33,7 @@ pub struct StageLineID { pub segment: usize, } -#[derive(Component, Reflect, Clone, Default)] -#[reflect(Component)] +#[derive(Reflect, Clone, Default)] pub struct StagePoly { poly_type: StagePolyType, pub segments: Vec, diff --git a/src/stage/manifest.rs b/src/stage/manifest.rs new file mode 100644 index 0000000..cb05e93 --- /dev/null +++ b/src/stage/manifest.rs @@ -0,0 +1,132 @@ +use std::collections::HashMap; + +use bevy_asset_loader::asset_collection::AssetCollection; +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumIter}; +use strum::IntoEnumIterator; + +use crate::{ + AppState, game_settings::GameSettings, math::{int::FGi32, vec::FGVec2}, stage::{StagePoly, line::{StageCollision, StagePolyLineSegmentType, StagePolyType}}, +}; +use bevy::prelude::*; + +#[derive(Reflect, Serialize, Deserialize, Copy, Clone, Eq, PartialEq, Hash, Debug, EnumIter)] +pub enum StageId { + TestStage, +} + +#[derive(Serialize, Deserialize, Clone, Reflect)] +#[reflect(opaque)] +pub struct StagePolyDefinition { + poly_type: StagePolyType, + lines: Vec<(FGVec2, StagePolyLineSegmentType)>, +} + +#[derive(Serialize, Deserialize, Clone, Asset, Reflect)] +pub struct StageManifest { + pub id: StageId, + pub model_path: String, + pub polygons: Vec, +} + +#[derive(Resource)] +pub struct StageManifestAssets(pub Vec>); + +impl StageManifestAssets { + fn requested(world: &World) -> Self { + let settings = world.resource::(); + let asset_server = world.resource::(); + + Self( + settings + .stage_manifest_paths + .iter() + .map(|path| asset_server.load(path.clone())) + .collect(), + ) + } +} + +impl AssetCollection for StageManifestAssets { + 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 StageManifestRegistry(HashMap>); + +impl StageManifestRegistry { + pub fn get(&self, stage: StageId) -> Option> { + self.0.get(&stage).cloned() + } +} + +fn fail(next_state: &mut NextState, message: &str) { + error!("Stage manifest preparation failed: {message}"); + next_state.set(AppState::CommonAssetLoadFailed); +} + + +pub fn prepare_stage_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 registry.insert(manifest.id, handle.clone()).is_some() { + fail( + &mut next_state, + &format!("duplicate manifest for stage {:?}", manifest.id), + ); + return; + } + } + + for stage in StageId::iter() { + if !registry.contains_key(&stage) { + fail( + &mut next_state, + &format!("no manifest configured for stage {stage:?}"), + ); + return; + } + } + + commands.insert_resource(StageManifestRegistry(registry)); + next_state.set(AppState::Idle); +} + +impl StageManifest { + pub fn to_collision(&self) -> StageCollision { + let stage_polys: Vec = self.polygons.iter().map(|def| { + StagePoly::build(def.poly_type, &def.lines) + }).collect(); + + StageCollision { + stage_polys + } + } +} \ No newline at end of file diff --git a/src/stage/mod.rs b/src/stage/mod.rs index d60125f..7ed0a63 100644 --- a/src/stage/mod.rs +++ b/src/stage/mod.rs @@ -6,9 +6,12 @@ use bevy::prelude::*; pub mod debug; pub mod line; pub mod scene; +pub mod manifest; pub use line::{StagePlaneIntersectResult, StagePoly}; +use crate::stage::line::StageCollision; + /// Owns the stage geometry: which scene gets spawned and how it is drawn. #[derive(Default)] pub struct StagePlugin; @@ -22,7 +25,7 @@ impl Plugin for StagePlugin { } } -pub fn spawn_stage(commands: &mut Commands, match_root: Entity, visual: Handle) { +pub fn spawn_stage(commands: &mut Commands, match_root: Entity, visual: Handle, collision: StageCollision) { commands.entity(match_root).insert(WorldAssetRoot(visual)); - scene::spawn_stage_support(commands, match_root); + scene::spawn_stage_support(commands, match_root, collision); } diff --git a/src/stage/scene.rs b/src/stage/scene.rs index 1a67409..2bb323e 100644 --- a/src/stage/scene.rs +++ b/src/stage/scene.rs @@ -3,43 +3,17 @@ use std::f32::consts::PI; use bevy::prelude::*; +use serde::{Deserialize, Serialize}; use crate::{ - math::vec::FGVec2, - stage::{ + math::{int::FGi32, vec::FGVec2}, stage::{ StagePoly, line::{StageCollision, StagePolyLineSegmentType, StagePolyType}, }, }; -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), - ], - ); - - commands.insert_resource(StageCollision { - stage_polys: vec![stage_poly.clone()], - }); - commands.spawn(( - Name::new("Stage Collision"), - stage_poly, - ChildOf(match_root), - )); +pub fn spawn_stage_support(commands: &mut Commands, match_root: Entity, collision: StageCollision) { + commands.insert_resource(collision); commands.spawn(( DirectionalLight { shadow_maps_enabled: true,