D-226 layer 1. World-advancing phases gated on pause: Movement/Storyteller/Knowledge/TickAdvance set-gated via sim_not_paused; Simulation + Economy gated per-system at their registration sites — collect_sound_events and serve_econ_state_query stay unconditioned (transient-buffer clear + paused-allowed query; set-gating Simulation leaked a stale tick-7 footstep into frozen snapshots — caught by golden_suite, fixed without touching the fixture; regression test encodes the bug shape). New PlayerAction::AutoPause/AutoResume + AutoPauseState resource implement Option A reconciliation: auto-resume only fires if auto-pause caused the pause; manual pause and Half rate survive implant open/close. PauseParams SystemParam bundle keeps process_player_input under the 16-param ceiling (BookmarkInputParams precedent). Client: HudGroups.gameplay_occluded now sends AutoPause/AutoResume via send_named_action; 5 gdUnit tests + 7 Rust tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75 lines
3.9 KiB
Rust
75 lines
3.9 KiB
Rust
//! 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::<super::sound::SoundEventQueue>()
|
|
.init_resource::<super::follow::FollowEndEventQueue>()
|
|
.init_resource::<super::monologue::PostConversationQueue>()
|
|
.init_resource::<super::poi_discovery::PoiDiscoveryEventQueue>()
|
|
// 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),
|
|
);
|
|
}
|
|
}
|