feat(simulation): dialogue pipeline, ContentSlug, and walk-away KG recording

Implement full dialogue selection pipeline (D-028): 4-layer filtering
engine with access tier, situation derivation, trust tier, and weighted
topic+mood scoring via SimRng. Add ContentSlug component for stable
content identity across save/load. Add walk-away KG recording with
IncompleteInteraction events per D-064 three-phase consequences. Bump
protocol to v8 with DialogueResponseEvent. Fixes #305, #427, #452.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-17 17:41:23 +01:00
co-authored by Claude Opus 4.6
parent 695d2ac843
commit 35f55cfa46
9 changed files with 1834 additions and 7 deletions
+36 -1
View File
@@ -32,6 +32,19 @@ use crate::simulation::interaction::Interactable;
use crate::simulation::movement::TilePosition;
use crate::simulation::time::DayPhase;
/// Stable content identifier from YAML (e.g., "kael-davan", "sera-venn").
///
/// Bridges authoring identity to ECS entities. Independent of StableId —
/// StableId is runtime entity tracking (KG references), ContentSlug is
/// authoring/content identity (which authored NPC template). Not all entities
/// have ContentSlugs (e.g., procedurally spawned NPCs, furniture).
///
/// Used by #427 (walk-away KG recording) to record interaction memory
/// against a stable content identity rather than an Entity (which is
/// unstable across save/load).
#[derive(Component, Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContentSlug(pub String);
/// Result of spawning content into the ECS world.
#[derive(Debug, Default)]
pub struct SpawnResult {
@@ -184,7 +197,9 @@ fn spawn_npc(world: &mut World, profile: &types::NpcProfile, result: &mut SpawnR
// Register in EntityRegistry for StableId mapping
let stable_id = world.resource_mut::<EntityRegistry>().register(entity);
world.entity_mut(entity).insert(StableEntityId(stable_id));
world
.entity_mut(entity)
.insert((StableEntityId(stable_id), ContentSlug(profile.canonical_id.clone())));
result
.npc_ids
@@ -722,6 +737,26 @@ mod tests {
assert!(!skills.combat_trained);
}
#[test]
fn spawn_npc_attaches_content_slug() {
let mut world = create_test_world();
let profile = create_test_profile();
let mut result = SpawnResult::default();
spawn_npc(&mut world, &profile, &mut result);
let stable_id = result.npc_ids["test-npc"];
let entity = world
.resource::<EntityRegistry>()
.to_entity(&stable_id)
.unwrap();
let slug = world
.get::<ContentSlug>(entity)
.expect("ContentSlug should be attached during spawn");
assert_eq!(slug.0, "test-npc");
}
#[test]
fn spawn_npc_minimal_profile() {
let mut world = create_test_world();