feat(simulation): interaction tracking + trust progression (#325, #324)

InteractionMemory component tracks interaction_count, last_interaction_tick,
and notable_events per NPC. Drives D-028 Layer 2 situation activation:
first_meeting (count==0) and repeated_visit (count>=3). warm_active in mood
system now derives from InteractionMemory within a 300-tick window.

Trust progression wired into dialogue systems: talk completion (+1),
walk-away (-1), confrontation (-2) emit TrustEvents consumed by update_trust.
InteractionEvent (WalkAway, Confrontation) recorded in notable_events for
fast per-pair access.

Adds FirstMeeting and RepeatedVisit Situation variants. 18 unit tests in
interaction.rs. All arithmetic integer-only (D-010 determinism). No HashMap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-20 18:52:23 +01:00
co-authored by Claude Sonnet 4.6
parent a14c980aad
commit dcd15a330f
7 changed files with 338 additions and 9 deletions
+6
View File
@@ -94,6 +94,10 @@ pub enum Situation {
Emergency,
Routine,
Observation,
/// First player-NPC interaction — interaction_count == 0 (#325, D-028 Layer 2).
FirstMeeting,
/// Player has talked to this NPC 3+ times — interaction_count >= 3 (#325, D-028 Layer 2).
RepeatedVisit,
}
impl FromStr for Situation {
@@ -113,6 +117,8 @@ impl FromStr for Situation {
"emergency" => Ok(Self::Emergency),
"routine" => Ok(Self::Routine),
"observation" => Ok(Self::Observation),
"first_meeting" => Ok(Self::FirstMeeting),
"repeated_visit" => Ok(Self::RepeatedVisit),
_ => Err(ParseEnumError {
kind: "Situation",
value: s.to_string(),
+3
View File
@@ -111,6 +111,9 @@ fn spawn_npc(world: &mut World, profile: &types::NpcProfile, result: &mut SpawnR
// Mark NPC as interactable for proximity-based verb detection (#413)
entity_commands.insert(Interactable);
// Interaction history — drives Layer 2 situation activation (#325, D-028)
entity_commands.insert(npc::interaction::InteractionMemory::default());
// Axis 1: Want
if let Some(want) = &profile.want {
if let Some(kind) = parse_want_kind(&want.primary) {