Fix "span gate threshold" → "span gate hum" (D-036 canonical term) in detective opening, add missing mood tags on opening_m_d_002 and the-terminal_d_020, normalize sera-venn double-dashes to em-dashes, fix situations.yaml header count, document greeting fallback behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
14 KiB
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:
- Which of the 14 D-035 situations encode relationship history states
- How the engine maps
InteractionMemoryto Layer 2 situation activation - 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.
# 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.
# Pattern: established — peer warmth
situation: [greeting]
access: [peer, insider]
trust: surface
mood: [warm, content]
# 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.
# 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.
# 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.
# 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)
// 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)
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_eventsrecordsInteractionEventKind::WalkAway. The KG recordsIncompleteInteraction. NPC getsRoutineDeviation. On next encounter, the NPC's mild mood shift (suspicious) steers Layer 4 scoring. - Confrontation:
process_confrontation_responsedecrements the NPC'sRelationshipStatein the player's KG. This directly changes which access tiers are available on next encounter. NPC'sCurrentMoodshifts (implementation pending: currently hardcoded to pushTier2animation and spike monologue; mood component update tosuspicious/frustratedis 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. Usesarrivaluntilgreetingsituation 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)