Make stages data-driven

This commit is contained in:
2026-08-27 20:35:03 +02:00
parent fb5775fdcd
commit 46c29857ac
10 changed files with 251 additions and 57 deletions
+1 -1
View File
@@ -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<AppState>, message: &str) {
+1
View File
@@ -28,6 +28,7 @@ pub struct GameSettings {
pub fighter_common: FighterSettingsCommon,
pub input_common: InputSettingsCommon,
pub fighter_manifest_paths: Vec<String>,
pub stage_manifest_paths: Vec<String>,
}
#[derive(AssetCollection, Resource)]
+14 -2
View File
@@ -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::<FighterManifest>::new(&["fighter.ron"]),
bevy_common_assets::ron::RonAssetPlugin::<StageManifest>::new(&["stage.ron"]),
bevy_common_assets::ron::RonAssetPlugin::<GameSettings>::new(&["ron"]),
))
.add_loading_state(
@@ -99,10 +101,20 @@ impl Plugin for GamePlugin {
.on_failure_continue_to_state(AppState::CommonAssetLoadFailed)
.load_collection::<fighter::manifest::FighterManifestAssets>(),
)
.add_loading_state(
LoadingState::new(AppState::LoadStageManifests)
.continue_to_state(AppState::PrepareStageManifests)
.on_failure_continue_to_state(AppState::CommonAssetLoadFailed)
.load_collection::<stage::manifest::StageManifestAssets>(),
)
.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,
+22 -18
View File
@@ -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::<PendingMatch>();
let asset_server = world.resource::<AssetServer>();
let registry = world.resource::<FighterManifestRegistry>();
let stage_registry = world.resource::<StageManifestRegistry>();
let manifests = world.resource::<Assets<FighterManifest>>();
let stage_manifests = world.resource::<Assets<StageManifest>>();
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<MatchAssets>,
gltfs: Res<Assets<Gltf>>,
registry: Res<FighterManifestRegistry>,
stage_registry: Res<StageManifestRegistry>,
manifests: Res<Assets<FighterManifest>>,
stage_manifests: Res<Assets<StageManifest>>,
mut graphs: ResMut<Assets<AnimationGraph>>,
args: Res<Args>,
mut next_state: ResMut<NextState<AppState>>,
@@ -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,
+4 -4
View File
@@ -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<StagePolyLineSegment>,
+132
View File
@@ -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<StagePolyDefinition>,
}
#[derive(Resource)]
pub struct StageManifestAssets(pub Vec<Handle<StageManifest>>);
impl StageManifestAssets {
fn requested(world: &World) -> Self {
let settings = world.resource::<GameSettings>();
let asset_server = world.resource::<AssetServer>();
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<UntypedHandle> {
Self::requested(world)
.0
.into_iter()
.map(Handle::untyped)
.collect()
}
}
#[derive(Resource)]
pub struct StageManifestRegistry(HashMap<StageId, Handle<StageManifest>>);
impl StageManifestRegistry {
pub fn get(&self, stage: StageId) -> Option<Handle<StageManifest>> {
self.0.get(&stage).cloned()
}
}
fn fail(next_state: &mut NextState<AppState>, message: &str) {
error!("Stage manifest preparation failed: {message}");
next_state.set(AppState::CommonAssetLoadFailed);
}
pub fn prepare_stage_manifests(
mut commands: Commands,
handles: Res<StageManifestAssets>,
manifests: Res<Assets<StageManifest>>,
mut next_state: ResMut<NextState<AppState>>,
) {
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<StagePoly> = self.polygons.iter().map(|def| {
StagePoly::build(def.poly_type, &def.lines)
}).collect();
StageCollision {
stage_polys
}
}
}
+5 -2
View File
@@ -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<WorldAsset>) {
pub fn spawn_stage(commands: &mut Commands, match_root: Entity, visual: Handle<WorldAsset>, collision: StageCollision) {
commands.entity(match_root).insert(WorldAssetRoot(visual));
scene::spawn_stage_support(commands, match_root);
scene::spawn_stage_support(commands, match_root, collision);
}
+4 -30
View File
@@ -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,