// Simulation module - Core simulation plugin and systems // Implements deterministic tick-based simulation (D-010 principle 4) use bevy_app::prelude::*; use bevy_ecs::schedule::IntoScheduleConfigs; pub mod chunk_streaming; pub mod contraband; pub mod conversation; pub mod dialogue; pub mod economy; pub mod examine; pub mod follow; pub mod generator; pub mod input; pub mod interaction; pub mod inventory; pub mod knowledge_grant; pub mod line_pool; pub mod listening; pub mod modification; pub mod monologue; pub mod movement; pub mod npc_knowledge_transfer; pub mod path_follow; pub mod pathfinding; pub mod poi; pub mod poi_discovery; pub mod pressure; pub mod rng; pub mod save_io; pub mod save_state; pub mod sound; pub mod spatial; pub mod stance; pub mod ticker; pub mod tier; pub mod time; pub mod triangle; pub mod zone; /// Core simulation plugin /// Manages simulation time, RNG, input processing, and tier transitions pub struct SimulationPlugin; impl Plugin for SimulationPlugin { fn build(&self, app: &mut App) { // Tier marker components (D-026) — must register before behavior systems app.add_plugins(tier::TierPlugin); // Initialize core simulation resources app.init_resource::() .insert_resource(rng::SimRng::new(0)) .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() .init_resource::() // Triangle escalation resources (#250) .init_resource::() .init_resource::() // discover_pois reads VisibilityGeometry (also populated by PerceptionPlugin). // Init here so SimulationPlugin works standalone in tests without PerceptionPlugin. .init_resource::() // transfer_npc_knowledge reads RelationshipGraph (also init by NpcPlugin) and // KnowledgeEventQueue (also init by KnowledgePlugin). // Init here so SimulationPlugin works standalone in tests without those plugins. .init_resource::() .init_resource::() .init_resource::() // Zone crossing event queue (#512, D-077): detect when player moves between zones. .init_resource::() .init_resource::() .add_systems( Update, ( input::process_player_input, // execute_save_load is an exclusive system (takes &mut World). // Must run after process_player_input (which queues the command) // and before compute_observer_snapshot (which consumes the result). save_io::execute_save_load .after(input::process_player_input) .before(crate::perception::observer::compute_observer_snapshot), pathfinding::compute_paths.after(input::process_player_input), path_follow::follow_paths.after(pathfinding::compute_paths), movement::validate_movement.after(path_follow::follow_paths), path_follow::cleanup_path_blocked.after(movement::validate_movement), spatial::sync_spatial_index.after(movement::validate_movement), listening::update_listening_focus.after(movement::validate_movement), contraband::check_contraband_scan .after(movement::validate_movement) .before(crate::perception::observer::compute_observer_snapshot), conversation::run_npc_conversations .after(movement::validate_movement) .before(sound::collect_sound_events), npc_knowledge_transfer::transfer_npc_knowledge .after(conversation::run_npc_conversations), sound::collect_sound_events .after(movement::validate_movement) .before(crate::perception::observer::compute_observer_snapshot), poi_discovery::discover_pois .after(crate::perception::observer::compute_visibility_geometry) .before(crate::perception::observer::compute_observer_snapshot), interaction::process_door_interaction .after(input::process_player_input) .before(movement::validate_movement), interaction::process_terminal_interaction .after(input::process_player_input) .before(crate::perception::observer::compute_observer_snapshot), examine::process_examine_interaction .after(input::process_player_input) .before(crate::perception::observer::compute_observer_snapshot), // Character pressure (#248) — reads NPC awareness + relationship graph pressure::update_character_pressure .after(crate::npc::awareness::detect_player_awareness) .before(crate::perception::observer::compute_observer_snapshot), // Triangle escalation (#250) — runs on game-minute boundaries (every 10 ticks) crate::simulation::triangle::tick_triangle_escalation .after(crate::npc::tolerance::check_tolerance_threshold) .before(crate::perception::observer::compute_observer_snapshot), // Triangle resolution (#250, D-089) — apply player resolve commands crate::simulation::triangle::apply_resolve_triangle .after(input::process_player_input) .before(crate::perception::observer::compute_observer_snapshot), time::advance_tick.after(path_follow::cleanup_path_blocked), ), ) // Zone crossing detection (#512, D-077) — separate call to stay within // Bevy's 20-element system tuple limit. .add_systems( Update, zone::detect_zone_crossings.after(movement::validate_movement), ) // Chunk streaming (#578, D-012) — loads/unloads chunks around the player. // Runs before input processing so chunks are available for the current tick. .add_systems( Update, chunk_streaming::chunk_streaming.before(input::process_player_input), ) // News ticker rotation (#591) — deterministic via SimRng, before snapshot. .add_systems( Update, ticker::tick_news_ticker .before(crate::perception::observer::compute_observer_snapshot), ); // Voice enrichment (D-138, Phase 3) — rewrite NPC text with voiced // variants from cache before the observer snapshot is assembled. // No-op when VoiceCacheResource is absent (voice pipeline disabled). app.add_systems( Update, ( crate::voice::integration::voice_enrich_dialogue_response .after(crate::simulation::dialogue::process_talk_interaction) .before(crate::perception::observer::compute_observer_snapshot), crate::voice::integration::voice_enrich_conversation_events .after(conversation::run_npc_conversations) .before(crate::perception::observer::compute_observer_snapshot), ), ); // Initialize TickerPool with empty default; populated by ContentPlugin at Startup. app.init_resource::(); // Economy simulation (#821, D-031) — loaded once at startup, no-op when DB absent. // Uses seed 0 for now; will be threaded through StartupMessage world seed (#826). if let Some((econ_sim, econ_state)) = economy::try_load_economy(0) { app.insert_resource(econ_sim).insert_resource(econ_state); } // EconQueryBuffer: always registered so EconStateQuery PlayerActions are accepted // even when the economy DB is absent (queries just produce no response). app.init_resource::(); // tick_economy_simulation + serve_econ_state_query use Option> — safe to // register unconditionally. They no-op when EconSimResource / EconStateResource absent. app.add_systems( Update, ( economy::tick_economy_simulation .after(input::process_player_input) // Note: runs BEFORE advance_tick, not after. The economy checks // time.tick which is the CURRENT tick (not yet advanced). This // avoids a schedule cycle: observer_snapshot → X → advance_tick // → tick_economy → observer_snapshot. Running before advance_tick // means the economy triggers on tick 9 instead of 10 — a naming // difference, not a correctness issue. .before(crate::perception::observer::compute_observer_snapshot), economy::serve_econ_state_query .after(economy::tick_economy_simulation) .before(crate::perception::observer::compute_observer_snapshot), ), ); tracing::debug!("SimulationPlugin initialized"); } }