feat(copy): #170 Layer 2 relationship history — greeting variants + situation mapping
New: docs/design/layer2-situation-mapping.md — full InteractionMemory→Layer 2 pipeline documentation, 5 relationship history states, authoring checklist. Greeting variants added: - Kael Davan: 12 lines (4 per location × 3 locations) - Sera Venn: 4 lines at The Last Shift All use situation: [greeting], correct Sprint 14 mood vocabulary. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)*
|
||||
Reference in New Issue
Block a user