--- title: "Layer 2: Situation-to-Relationship-History Mapping" description: "How InteractionMemory maps to D-028 Layer 2 situation activation and relationship-aware greeting selection" type: design status: active ticket: "#170" decision_refs: [D-028, D-035] author: "Paula" created: 2026-02-21 updated: 2026-02-21 --- # Layer 2: Situation-to-Relationship-History Mapping **Ticket:** #170 | **Sprint:** 15 | **Author:** Paula **Cross-references:** D-028, D-035, server/src/npc/interaction.rs (#325), server/src/simulation/dialogue.rs (#305) --- ## Purpose D-028 Layer 2 modifies greeting and topic selection based on interaction history with a specific NPC. This document defines: 1. Which of the 14 D-035 situations encode relationship history states 2. How the engine maps `InteractionMemory` to Layer 2 situation activation 3. How content authors tag lines to exploit those states --- ## Part A: The 14 Situations — Which Encode Relationship History D-035 defines 14 `situation` enum values. Most are **time-and-context situations** (what's happening in the world right now). A smaller subset are **relationship history markers** that the engine derives from `InteractionMemory`. ### Time-and-Context Situations (engine derives from game state) | Situation | When Active | Relationship-Aware? | |-----------|-------------|---------------------| | `arrival` | Player has just arrived at this location (first visit this game-day) | No | | `shift_start` | NPC is beginning their work shift | No | | `shift_end` | NPC is wrapping up their work shift | No | | `shift_transition` | Gap between shifts — oversight thinnest | No | | `bar_evening` | Social hours at The Last Shift | No | | `night_shift` | Late hours, skeleton crew | No | | `investigation` | Player is actively asking probing questions; activated by `PersonOfInterest` relationship | Yes — via RelationshipState | | `confrontation` | Triangle tension past threshold; NPC is defensive/agitated | No (NPC-side state) | | `social` | NPC is in casual social activity | No | | `alone` | NPC is by themselves | No | | `emergency` | Urgent event occurring | No | | `routine` | Normal daily activity (always-active baseline) | No | | `observation` | Player watching from a distance | No | ### Relationship History Situations | Situation | Relationship History State | How Activated | |-----------|---------------------------|---------------| | `greeting` | **Any initial contact** — the opening exchange of an encounter | Active whenever the player initiates Talk (pending Rust update — see Part D) | The `greeting` situation is the primary Layer 2 tool for content authors. By itself, `greeting` marks a line for the initial-contact phase of any encounter. The engine's `InteractionMemory` state then determines which `greeting`-tagged lines actually surface, via access tier filtering. --- ## Part B: Relationship History States and Content Encoding Five relationship history states matter for Layer 2 dialogue authoring: ### 1. first_meeting **Condition:** `InteractionMemory.interaction_count == 0` **Engine behavior:** Pushes `Situation::FirstMeeting` into active situations **Content encoding:** `situation: [greeting]` + `access: [public]` The player has never spoken to this NPC before. Their `RelationshipState` is `Unknown`, so only `access: [public]` lines are eligible. A `greeting`-tagged line with `access: [public]` fires as the first words exchanged. ```yaml # Pattern: first_meeting situation: [greeting] access: [public] trust: surface mood: [content] # or untagged (neutral) ``` ### 2. established **Condition:** `InteractionMemory.interaction_count >= 3` **Engine behavior:** Pushes `Situation::RepeatedVisit` into active situations **Content encoding:** `situation: [greeting]` + `access: [peer]` or `access: [peer, insider]` The player has spoken to this NPC at least 3 times. The relationship has progressed — typically `Known` (access: Peer) or `Friendly` (access: Peer + Insider). `greeting`-tagged lines with these access tiers become available and are preferred over public lines for greeting selection. ```yaml # Pattern: established — peer warmth situation: [greeting] access: [peer, insider] trust: surface mood: [warm, content] ``` ```yaml # Pattern: established — authority/professional repeat situation: [greeting] access: [authority] trust: surface mood: [focused] ``` ### 3. tense **Condition:** Confrontation logged in `InteractionMemory.notable_events` but relationship has not collapsed **Engine behavior:** No distinct situation pushed; modeled through NPC's `CurrentMood` (set to `suspicious` or `frustrated` by confrontation response system) **Content encoding:** `situation: [greeting]` + `access: [peer, insider]` + `mood: [suspicious, frustrated]` A confrontation was delivered. The relationship may have been decremented (e.g., `Friendly → Known`), but the player still has peer access. The NPC's mood has shifted. `greeting`-tagged lines with `mood: [suspicious, frustrated]` score higher in Layer 4 weighted selection. ```yaml # Pattern: tense — post-confrontation greeting (still peer access) situation: [greeting] access: [peer, insider] trust: surface mood: [suspicious, frustrated] ``` ### 4. post_confrontation **Condition:** Confrontation recorded in `InteractionMemory.notable_events` (kind: `Confrontation`) **Relationship state change:** `process_confrontation_response` decrements `RelationshipState` (e.g., `Known → PersonOfInterest`) **Content encoding:** `situation: [confrontation]` + access tags matching post-decrement relationship Post-confrontation dialogue is primarily handled by existing `situation: [confrontation]` lines and the deflection/response pools. The `greeting` situation isn't the primary driver here — the confrontation situation takes precedence. ```yaml # Pattern: post_confrontation — NPC is now defensive situation: [confrontation] access: [peer, public, authority] trust: surface mood: [hostile, suspicious, frustrated] ``` ### 5. post_walkaway **Condition:** Walk-away recorded in `InteractionMemory.notable_events` (kind: `WalkAway`) **Engine behavior:** `process_walk_away` records `IncompleteInteraction` in KnowledgeGraph; NPC gets `RoutineDeviation` component **Content encoding:** `situation: [greeting]` + `mood: [suspicious]` (lower intensity than confrontation) A walk-away is softer than a confrontation. The NPC noticed, the KG recorded it, but the relationship didn't necessarily decrement. The NPC's mood may shift mildly suspicious. `greeting`-tagged lines with `mood: [suspicious]` will score slightly higher next encounter. ```yaml # Pattern: post_walkaway — NPC is mildly unsettled situation: [greeting] access: [peer, insider] trust: surface mood: [suspicious] ``` --- ## Part C: Situation State Quick Reference | Relationship History State | InteractionMemory Condition | Access Tier in Play | Mood Signal | Layer 2 Mechanism | |--------------------------|----------------------------|--------------------|--------------|--------------------| | `first_meeting` | count == 0 | Public only | neutral/content | `Situation::FirstMeeting` pushed; only Public lines eligible | | `established` | count >= 3 | Peer, Insider, or Authority | warm, content, focused | `Situation::RepeatedVisit` pushed; higher-tier access unlocked | | `tense` | confrontation in notable_events, peer access still active | Peer + Insider | suspicious, frustrated | No distinct situation; NPC CurrentMood drives scoring | | `post_confrontation` | confrontation in notable_events, relationship decremented | Public + Authority (or Peer) | hostile, suspicious | RelationshipState decremented → access tier shifts | | `post_walkaway` | walkaway in notable_events | Peer + Insider | suspicious | KG records IncompleteInteraction; mild mood shift | --- ## Part D: InteractionMemory → Layer 2 Situation Selection **Source:** `server/src/npc/interaction.rs` (#325), `server/src/simulation/dialogue.rs` (#305, L414-421) ### The Derivation Chain When the player initiates a Talk interaction, `process_talk_interaction` derives the active situation set through two passes: **Pass 1: Game-state situations** (`derive_situations`, dialogue.rs L226-251) ```rust // Always-active baseline situations.push(Situation::Routine); // Day phase → situation match day_phase { Morning => situations.push(Situation::ShiftStart), Afternoon => situations.push(Situation::Social), Evening => { situations.push(Situation::BarEvening); situations.push(Situation::Social); } Night => situations.push(Situation::NightShift), } // Relationship context if relationship == PersonOfInterest { situations.push(Situation::Investigation); } ``` **Pass 2: InteractionMemory situations** (dialogue.rs L414-421) ```rust if let Some(ref mem) = interaction_mem_opt { if mem.is_first_meeting() { situations.push(Situation::FirstMeeting); // count == 0 } else if mem.is_repeated_visit() { situations.push(Situation::RepeatedVisit); // count >= 3 } } ``` ### InteractionMemory Thresholds | Method | Condition | Derived Situation | |--------|-----------|-------------------| | `is_first_meeting()` | `interaction_count == 0` | `Situation::FirstMeeting` | | `is_repeated_visit()` | `interaction_count >= 3` | `Situation::RepeatedVisit` | | *(neither)* | count == 1 or 2 | Neither — transitional zone | **Transitional zone (count 1-2):** No `FirstMeeting` or `RepeatedVisit` situation is pushed. The player has spoken to this NPC once or twice — the relationship is warming, but not yet "established." Greeting selection falls through to `access: [public]` lines (relationship likely still `Unknown` or just tipping to `Known`). ### The `greeting` Situation: Current Status and Intent Content lines use `situation: [greeting]` as their tag. The engine currently pushes `Situation::FirstMeeting` and `Situation::RepeatedVisit` — NOT `Situation::Greeting`. **Schema note (dialogue-line.schema.json):** `"'greeting' is not yet in server/src/content/line_pool.rs — lines using it will be skipped until Rust is updated."` **Current fallback behavior:** Until the Rust update lands, lines tagged `situation: [greeting]` are excluded from the candidate pool entirely — they never fire. NPCs greet the player using their `situation: [routine]` or `situation: [social]` lines instead, filtered by access tier and mood as usual. This means Layer 2 relationship-aware greetings are functionally dormant; the content exists and is schema-valid, but the engine skips it. No error is raised — the line pool query simply finds no match for the `greeting` situation and falls through to other eligible lines. **Intended resolution (pending Rust update):** When `server/src/content/line_pool.rs` is updated to include `Situation::Greeting`, `greeting`-tagged content lines will fire. The Layer 2 relationship history distinction will then work through the access tier filtering that is already implemented: - `access: [public]` + `situation: [greeting]` → first meeting lines (Unknown relationship) - `access: [peer, insider]` + `situation: [greeting]` → established lines (Known/Friendly relationship) - `access: [authority]` + `situation: [greeting]` → authority repeat lines (PersonOfInterest relationship) - `mood: [suspicious, frustrated]` on any of the above → strained/post-confrontation lines score higher ### Walk-away and Confrontation: No Separate Situations Walk-aways and confrontations do NOT push a dedicated `Situation::PostWalkAway` or `Situation::PostConfrontation`. Instead: - **Walk-away:** `InteractionMemory.notable_events` records `InteractionEventKind::WalkAway`. The KG records `IncompleteInteraction`. NPC gets `RoutineDeviation`. On next encounter, the NPC's mild mood shift (`suspicious`) steers Layer 4 scoring. - **Confrontation:** `process_confrontation_response` decrements the NPC's `RelationshipState` in the player's KG. This directly changes which access tiers are available on next encounter. NPC's `CurrentMood` shifts (implementation pending: currently hardcoded to push `Tier2` animation and spike monologue; mood component update to `suspicious`/`frustrated` is implied). ### The Layer 2 Pipeline in Full ``` Player initiates Talk ↓ 1. derive_situations(day_phase, relationship) → [Routine, ShiftStart/Social/BarEvening/NightShift, Investigation?] ↓ 2. InteractionMemory → FirstMeeting | RepeatedVisit | (neither) → append to situations list ↓ 3. available_access_tiers(relationship) → [Public] | [Public, Peer] | [Public, Peer, Insider] | [Public, Peer, Authority] | [Hostile] ↓ 4. line_pool.query_dialogue(location, role, access, situations, trust) → candidate lines matching all three filters ↓ 5. Layer 4 weighted scoring: mood match (+3), topic match (+2/each) → NPC's CurrentMood biases toward lines matching their emotional state ↓ 6. Weighted random selection → final line ``` --- ## Authoring Checklist: Layer 2 Greeting Variants When authoring greeting variants for a new Tier 1 NPC, provide at minimum: - [ ] **First meeting** — `situation: [greeting]`, `access: [public]`, neutral mood. Fires before any relationship forms. - [ ] **Established (peer/social)** — `situation: [greeting]`, `access: [peer, insider]`, `mood: [warm, content]`. Fires after 3+ interactions in a social/peer context. - [ ] **Established (authority)** — `situation: [greeting]`, `access: [authority]`, `mood: [focused]`. Fires after 3+ interactions in an authority/investigation context. - [ ] **Post-confrontation** — `situation: [greeting]`, `access: [peer, insider]`, `mood: [suspicious, frustrated]`. Fires when NPC's mood is strained. Optional but recommended: - [ ] **Transitional (1-2 interactions)** — `situation: [arrival, social]`, `access: [public, peer]`. Bridges first meeting and established. Uses `arrival` until `greeting` situation is active in Rust. - [ ] **Post-walkaway** — `situation: [greeting]`, `access: [peer, insider]`, `mood: [suspicious]`. Softer version of post-confrontation. --- *Last updated: 2026-02-21 (Sprint 15, ticket #170)*