feat(simulation): add storyteller engagement tracking and activation pass

Storyteller chain for Sprint 23 (#570, #571, #572, #579):

- EngagementRecord component: per-NPC observation_time_ticks,
  conversation_count, monologue_trigger_count — incremented by
  perception, dialogue, and monologue systems
- MovementHistoryBuffer resource: ring buffer of player positions
  over last 3000 ticks with npcs_copresent_in_window() query
- Lifecycle rules: single activation per session, no concurrency,
  no cooldown, terminal resolution constants
- activation_pass() system: gate check, proximity query, engagement
  scoring, unentangled-NPC routing, module selection, emits
  TriangleActivatedEvent on 10-tick cadence after contamination

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 19:11:54 +01:00
co-authored by Claude Opus 4.6
parent 7fbfc5ae65
commit 50f5d6c22e
5 changed files with 812 additions and 9 deletions
+7
View File
@@ -12,6 +12,7 @@ use crate::knowledge::{
use crate::perception::cognitive_delay::{CognitiveDelay, PendingRecognition, RecognitionTrigger};
use crate::simulation::movement::{PlayerCharacter, TilePosition};
use crate::simulation::time::SimulationTime;
use crate::storyteller::EngagementRecord;
/// Emit knowledge events for entities entering/leaving the observer's LOS.
///
@@ -29,6 +30,7 @@ pub fn emit_observation_events(
mut event_queue: ResMut<KnowledgeEventQueue>,
entity_positions: Query<&TilePosition>,
anomaly_markers: Query<(), With<crate::perception::anomaly::AnomalyMarker>>,
mut engagement_query: Query<&mut EngagementRecord>,
) {
let Some(snapshot) = &buffer.snapshot else {
return;
@@ -61,6 +63,11 @@ pub fn emit_observation_events(
continue;
};
// Increment observation time for engagement tracking (#570)
if let Ok(mut record) = engagement_query.get_mut(entity) {
record.observation_time_ticks += 1;
}
// Get tile position for knowledge tracking
let Ok(pos) = entity_positions.get(entity) else {
continue;