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
+4
View File
@@ -33,6 +33,7 @@ use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
use crate::simulation::tier::ActiveSim;
use crate::simulation::time::DayPhase;
use crate::storyteller::EngagementRecord;
// #166 — Template-to-instance mapping
use rand::Rng as _;
@@ -133,6 +134,9 @@ fn spawn_npc(world: &mut World, profile: &types::NpcProfile, result: &mut SpawnR
// Mood state — drives Layer 4 dialogue selection and monologue tone (#323)
entity_commands.insert(npc::mood::MoodState::default());
// Engagement metrics — storyteller activation scoring (#570, #162)
entity_commands.insert(EngagementRecord::default());
// Axis 1: Want
if let Some(want) = &profile.want {
if let Some(kind) = parse_want_kind(&want.primary) {