diff --git a/docs/design/layer2-situation-mapping.md b/docs/design/layer2-situation-mapping.md new file mode 100644 index 000000000..5668e1e5e --- /dev/null +++ b/docs/design/layer2-situation-mapping.md @@ -0,0 +1,258 @@ +# 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."` + +**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)*