Files
settled-reach/docs/design/layer2-situation-mapping.md
T
jpmschweitzerandClaude Opus 4.6 e166409674 fix(copy): PR #53 review round 3 — span gate terminology, mood tags, em-dash consistency
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>
2026-02-21 15:03:06 +01:00

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:

  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.

# 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_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 meetingsituation: [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-confrontationsituation: [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-walkawaysituation: [greeting], access: [peer, insider], mood: [suspicious]. Softer version of post-confrontation.

Last updated: 2026-02-21 (Sprint 15, ticket #170)