Implements #163 (RoleSchema), #164 (SpaceSpec), #165 (TemplateOwnership + TemplateReferenceMap), #106 (TriangleDef), #107 (intra-template triangle generation), and #250 (triangle escalation system) as the foundational Tier 2 template system per D-025. New content/template module with YAML-deserializable schema types, ECS components for ownership/triangle state, escalation system running on game-minute boundaries, and TriangleCrisisEvent emission. Sample YAML templates at server/data/templates/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
120 lines
4.2 KiB
Rust
120 lines
4.2 KiB
Rust
//! Content loading, indexing, and entity spawning system.
|
|
//!
|
|
//! 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)
|
|
//! 4. ContentStore -> indexed line pools (line_pool.rs) -> LinePoolIndex resource
|
|
//! 5. Optional hot-reload (hot_reload.rs) for dev/authoring workflow
|
|
//!
|
|
//! Content schema is decoupled from ECS components. The spawn module
|
|
//! handles the mapping between the two representations.
|
|
|
|
pub mod hot_reload;
|
|
pub mod line_pool;
|
|
pub mod loader;
|
|
pub mod spawn;
|
|
pub mod template;
|
|
pub mod types;
|
|
|
|
use bevy_app::prelude::*;
|
|
use bevy_ecs::prelude::*;
|
|
use std::path::PathBuf;
|
|
|
|
use crate::knowledge::ContentEntityRegistry;
|
|
|
|
/// 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,
|
|
/// Enable hot-reload (timestamp polling). Dev-only, not for production.
|
|
pub hot_reload: bool,
|
|
}
|
|
|
|
impl Default for ContentConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
content_root: PathBuf::from("content"),
|
|
hot_reload: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Content loading plugin.
|
|
///
|
|
/// Loads content from YAML files at startup, spawns ECS entities,
|
|
/// and builds the indexed line pools for dialogue/monologue queries.
|
|
/// Optionally enables hot-reload for the authoring workflow.
|
|
pub struct ContentPlugin;
|
|
|
|
impl Plugin for ContentPlugin {
|
|
fn build(&self, app: &mut App) {
|
|
if !app.world().contains_resource::<ContentConfig>() {
|
|
app.insert_resource(ContentConfig::default());
|
|
}
|
|
|
|
// ContentEntityRegistry is required by spawn_npc (D-079).
|
|
// Init here so ContentPlugin works standalone without KnowledgePlugin.
|
|
app.init_resource::<ContentEntityRegistry>();
|
|
|
|
app.add_systems(Startup, load_and_spawn_content);
|
|
app.add_systems(PostUpdate, hot_reload::hot_reload_content);
|
|
|
|
tracing::debug!("ContentPlugin initialized");
|
|
}
|
|
}
|
|
|
|
/// Startup system: load content from disk, spawn entities, and build line pool index.
|
|
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);
|
|
|
|
// Build line pool index
|
|
let index = line_pool::LinePoolIndex::build(&store);
|
|
tracing::info!(
|
|
"Line pool index built: {} dialogue lines, {} monologue lines",
|
|
index.dialogue_line_count(),
|
|
index.monologue_line_count()
|
|
);
|
|
|
|
world.insert_resource(ContentStoreResource(store));
|
|
world.insert_resource(LinePoolIndexResource(index));
|
|
}
|
|
Err(e) => {
|
|
tracing::error!("Failed to load content: {}", e);
|
|
world.insert_resource(ContentStoreResource(loader::ContentStore::default()));
|
|
world.insert_resource(LinePoolIndexResource(line_pool::LinePoolIndex::default()));
|
|
}
|
|
}
|
|
|
|
// Set up hot-reload if enabled
|
|
if config.hot_reload {
|
|
let watcher = hot_reload::ContentWatcher::new(&config.content_root);
|
|
tracing::info!(
|
|
"Content hot-reload enabled — tracking {} files, polling every ~2s",
|
|
watcher.tracked_file_count()
|
|
);
|
|
world.insert_resource(watcher);
|
|
}
|
|
}
|
|
|
|
/// 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);
|
|
|
|
/// Wrapper resource holding the indexed line pools.
|
|
/// Available for runtime systems that need to query dialogue/monologue lines
|
|
/// through the D-028 four-layer filtering pipeline.
|
|
#[derive(Resource, Debug)]
|
|
pub struct LinePoolIndexResource(pub line_pool::LinePoolIndex);
|