feat(simulation): implement content loader Phase 2 (#408)
Extend content loader to read and instantiate real YAML content into ECS entities. 2-phase spawn pipeline: Phase 1 creates entities with core components (Want, Tolerance, Contentment, Personality, Tells, Skills), Phase 2 resolves cross-references (Relationships, Secrets, Information via KnowledgeGraph, DailyRoutine). Loads enums, entity attributes, pools, templates, triangles, and NPC profiles. Handles stub files gracefully. 7 integration tests against real content files plus edge case tests for invalid data. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
//! Content loading and entity spawning system.
|
||||
//!
|
||||
//! Phase 2 content loader (ticket #408): loads YAML content files from disk,
|
||||
//! deserializes into intermediate types, and spawns ECS entities.
|
||||
//!
|
||||
//! Architecture (per Tyre's D-020 guidance):
|
||||
//! 1. Deserialize YAML → intermediate content types (types.rs)
|
||||
//! 2. Content discovery + loading (loader.rs) → ContentStore resource
|
||||
//! 3. ContentStore → ECS entity spawning (spawn.rs)
|
||||
//!
|
||||
//! Content schema is decoupled from ECS components. The spawn module
|
||||
//! handles the mapping between the two representations.
|
||||
|
||||
pub mod loader;
|
||||
pub mod spawn;
|
||||
pub mod types;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_ecs::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Configuration for the content loader.
|
||||
/// Set the content root path before adding ContentPlugin.
|
||||
#[derive(Resource, Debug, Clone)]
|
||||
pub struct ContentConfig {
|
||||
/// Root directory containing content.yaml and campaign directories.
|
||||
pub content_root: PathBuf,
|
||||
}
|
||||
|
||||
impl Default for ContentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
content_root: PathBuf::from("content"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Content loading plugin.
|
||||
///
|
||||
/// Loads content from YAML files at startup and spawns ECS entities.
|
||||
/// Requires ContentConfig resource to be inserted before the plugin runs.
|
||||
pub struct ContentPlugin;
|
||||
|
||||
impl Plugin for ContentPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
if !app.world().contains_resource::<ContentConfig>() {
|
||||
app.insert_resource(ContentConfig::default());
|
||||
}
|
||||
|
||||
app.add_systems(Startup, load_and_spawn_content);
|
||||
|
||||
tracing::debug!("ContentPlugin initialized");
|
||||
}
|
||||
}
|
||||
|
||||
/// Startup system: load content from disk and spawn entities.
|
||||
fn load_and_spawn_content(world: &mut World) {
|
||||
let config = world.resource::<ContentConfig>().clone();
|
||||
|
||||
tracing::info!("Loading content from: {:?}", config.content_root);
|
||||
|
||||
match loader::load_content(&config.content_root) {
|
||||
Ok(store) => {
|
||||
let result = spawn::spawn_content(world, &store);
|
||||
tracing::info!(
|
||||
"Content loaded and spawned: {} NPCs",
|
||||
result.npcs_spawned
|
||||
);
|
||||
// Insert the content store as a resource for runtime access
|
||||
// (triangle queries, pool lookups, dialogue selection)
|
||||
world.insert_resource(ContentStoreResource(store));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to load content: {}", e);
|
||||
// Insert empty store so downstream systems don't panic on missing resource
|
||||
world.insert_resource(ContentStoreResource(loader::ContentStore::default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper resource holding the loaded content store.
|
||||
/// Available for runtime systems that need to query content data
|
||||
/// (e.g., dialogue selection, triangle fork evaluation).
|
||||
#[derive(Resource, Debug)]
|
||||
pub struct ContentStoreResource(pub loader::ContentStore);
|
||||
Reference in New Issue
Block a user