//! Social simulation plugin — NPC knowledge transfer, disclosure, and social systems. //! //! All systems run in [`TickPhase::Simulation`]. //! //! T-970 (D-226 layer 1): `TickPhase::Simulation` is NOT set-gated while the //! sim is paused — `collect_sound_events` must keep running every tick //! regardless (see its own `.run_if`-free registration below and the doc //! comment on `tick_phases::TickPhase::configure`). Every other system in //! this phase IS individually gated with `sim_not_paused`, collectively via //! the tuple's own `.run_if()` (safe here because collect_sound_events isn't //! part of that tuple — the anonymous-set-wrapping this creates only affects //! the gated group, not the always-on system). use bevy_app::prelude::*; use bevy_ecs::schedule::IntoScheduleConfigs; use crate::simulation::time::sim_not_paused; use crate::tick_phases::TickPhase; pub struct SocialPlugin; impl Plugin for SocialPlugin { fn build(&self, app: &mut App) { app.init_resource::() .init_resource::() .init_resource::() .init_resource::() // collect_sound_events: NEVER gated (T-970). It only clears-then- // refills the this-tick SoundEventQueue from SoundEventEmitter // components — compute_observer_snapshot (Snapshot phase, // ungated) peeks that queue via Res every tick without draining // it itself, so skipping this system while paused would leave // stale sound events visible in every snapshot taken while // frozen (confirmed by tests/golden_suite.rs going red when this // was gated as part of the whole Simulation set). // // Deliberate, documented semantics (do not "fix" this later): // with collect_sound_events left unconditioned, the FIRST paused // tick clears the queue and no gated producer re-emits into it // (movement/dialogue/etc. that would populate SoundEventEmitter // are all frozen), so every snapshot served while paused shows // sound_events: [] — a stable, silent, frozen inspection view // one tick after the pause takes effect. This matches D-226's // "inspection substrate" intent (sound is an instantaneous // event, not persistent world state, so a frozen view showing // none is correct) rather than replaying/holding the last sound // indefinitely while paused. .add_systems( Update, super::sound::collect_sound_events.in_set(TickPhase::Simulation), ) .add_systems( Update, ( super::npc_knowledge_transfer::transfer_npc_knowledge, // Voice enrichment (D-138) — rewrite NPC text with voiced variants. // No-op when VoiceCacheResource is absent. crate::voice::integration::voice_enrich_dialogue_response, // POI discovery reads visibility geometry (also Simulation phase) super::poi_discovery::discover_pois, // Follow state reads visibility geometry + movement super::follow::update_follow_state, // Contraband scan reads movement + NPC awareness super::contraband::check_contraband_scan, // Pressure reads NPC awareness + relationship graph super::pressure::update_character_pressure, // Triangle escalation (#250) — runs on game-minute boundaries super::triangle::tick_triangle_escalation, ) .run_if(sim_not_paused) .in_set(TickPhase::Simulation), ); } }