From c436ae905f7536d6f124904627c825452fa0cd94 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 25 Feb 2026 22:33:21 +0100 Subject: [PATCH] feat(simulation): wire triangle escalation and crisis events into protocol Registers tick_triangle_escalation and apply_resolve_triangle systems in SimulationPlugin. Adds TriangleCrisisEventWire to ObserverSnapshot (protocol v16) for future client rendering of triangle crises (#250, D-087). Observer emits empty vec by default; escalation system will populate when triangles reach Active phase. Co-Authored-By: Claude Opus 4.6 --- server/src/bridge/text_renderer.rs | 2 ++ server/src/bridge/types.rs | 27 +++++++++++++++++++++++++-- server/src/perception/observer/mod.rs | 1 + server/src/simulation/mod.rs | 11 +++++++++++ 4 files changed, 39 insertions(+), 2 deletions(-) diff --git a/server/src/bridge/text_renderer.rs b/server/src/bridge/text_renderer.rs index 777c32e50..aabb19d90 100644 --- a/server/src/bridge/text_renderer.rs +++ b/server/src/bridge/text_renderer.rs @@ -313,6 +313,7 @@ mod tests { examine_result: None, player_knowledge: None, save_result: None, + triangle_crisis_events: vec![], } } @@ -448,6 +449,7 @@ mod tests { examine_result: None, player_knowledge: None, save_result: None, + triangle_crisis_events: vec![], }; let text = format_snapshot_text(&snap); assert!(text.contains("Tick 0")); diff --git a/server/src/bridge/types.rs b/server/src/bridge/types.rs index 6274ea1fa..15b329599 100644 --- a/server/src/bridge/types.rs +++ b/server/src/bridge/types.rs @@ -17,7 +17,7 @@ pub use crate::simulation::time::{DayPhase, TickRate}; /// negotiation is unnecessary. Client should reject snapshots with version != /// PROTOCOL_VERSION. New fields use #[serde(default)] only during the migration /// period, then the default is removed once both sides are updated. -pub const PROTOCOL_VERSION: u8 = 15; +pub const PROTOCOL_VERSION: u8 = 16; /// Handshake message sent as the very first framed message after connection (#555). /// Client reads this before entering the normal tick loop and validates @@ -50,10 +50,11 @@ pub struct HandshakeMessage { /// examine_result (#242, character-filtered examine observation text), /// player_knowledge (#264, partial KG dump for journal/knowledge panel). /// v15 adds: save_result (#553, save/load operation result for client confirmation). +/// v16 adds: triangle_crisis_events (#250, D-087 triangle escalation for future client rendering). /// Future fields: ambient sound events, HUD state (D-020 expansion). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ObserverSnapshot { - /// Protocol version for forward compatibility. Current: 15. + /// Protocol version for forward compatibility. Current: 16. pub version: u8, /// Simulation tick when this snapshot was produced pub tick: u64, @@ -151,6 +152,11 @@ pub struct ObserverSnapshot { /// Client shows a confirmation toast (success) or error modal (failure). #[serde(default, skip_serializing_if = "Option::is_none")] pub save_result: Option, + /// Triangle crisis events this tick (#250, D-087). + /// Emitted when a triangle enters Active phase. Client may render + /// a narrative event or HUD indicator. Empty when no crises occur. + #[serde(default)] + pub triangle_crisis_events: Vec, } /// Game time data for client display (D-031) @@ -676,6 +682,23 @@ pub struct SaveLoadResultWire { pub error: Option, } +/// Triangle crisis event for future client rendering (#250, D-087). +/// +/// Emitted when a triangle transitions from Simmering to Active. +/// Client may display a narrative beat, HUD indicator, or tension meter. +/// Wire format uses primitives for cross-boundary safety. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TriangleCrisisEventWire { + /// Triangle identifier (TriangleId as u64). + pub triangle_id: u64, + /// NPC role assignments: (role_slug, stable_npc_id). + pub role_assignments: Vec<(String, u64)>, + /// The NPC whose tolerance threshold triggered the crisis. + pub trigger_npc_id: u64, + /// Tick when the crisis was triggered. + pub tick: u64, +} + /// Snapshot buffer resource for staging outgoing ObserverSnapshots #[derive(Resource, Debug, Default)] pub struct SnapshotBuffer { diff --git a/server/src/perception/observer/mod.rs b/server/src/perception/observer/mod.rs index 32b552515..e38f96a09 100644 --- a/server/src/perception/observer/mod.rs +++ b/server/src/perception/observer/mod.rs @@ -414,6 +414,7 @@ pub fn compute_observer_snapshot( examine_result, player_knowledge, save_result, + triangle_crisis_events: vec![], }); } diff --git a/server/src/simulation/mod.rs b/server/src/simulation/mod.rs index de0f3436b..a0f37f2f7 100644 --- a/server/src/simulation/mod.rs +++ b/server/src/simulation/mod.rs @@ -51,6 +51,9 @@ impl Plugin for SimulationPlugin { .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::() @@ -96,6 +99,14 @@ impl Plugin for SimulationPlugin { 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::content::template::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::content::template::apply_resolve_triangle + .after(input::process_player_input) + .before(crate::perception::observer::compute_observer_snapshot), time::advance_tick.after(path_follow::cleanup_path_blocked), ), );