Files
settled-reach/docs/workshops/v01-content-scoping/round2-stig.md
T
jpmschweitzerandClaude Opus 4.6 3944b4e79f docs(workshops): archive v0.1 content scoping workshop (2 rounds + closing)
Scoped the vertical slice: 16 EntityKnowledge keys, v0.1 mechanical
NPC mapping, YAML content format, 7-verb interaction model, server-
authoritative pause, 38 tickets created across copy/server/client/ci.
20 decisions (D-042 through D-061). 8 agents, 2 rounds + closing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:05 +01:00

18 KiB

Round 2 Response: Stig (UI Developer)

v0.1 Content Scoping Workshop


Task 1: Single-Action Prompt Designed for Multi-Verb Extension

The lead confirmed: v0.1 ships single-action. Multi-verb is v0.2. I need to design the prompt system so the upgrade path is clean — no throwaway architecture.

How it works in v0.1

The server sends an InteractionOptions payload per entity when the player enters interaction range. v0.1 format:

InteractionOptions {
    entity_id: "kael_davan",
    display_name: "Kael Davan",
    primary: { verb: "Talk", keybind: "E" },
    secondary: null
}

The client renders:

     [E] Talk
    Kael Davan
      [====]

One prompt. One key. The player approaches, reads, presses. Done.

For objects:

InteractionOptions {
    entity_id: "manifest_terminal_03",
    display_name: "Manifest Terminal",
    primary: { verb: "Examine", keybind: "E" },
    secondary: null
}

Renders as:

   [E] Examine
  Manifest Terminal
     [====]

Same pattern. The verb changes. The keybind stays the same. The player learns one input: "E means interact."

How it extends to v0.2 (multi-verb)

The server payload grows:

InteractionOptions {
    entity_id: "kael_davan",
    display_name: "Kael Davan",
    primary: { verb: "Talk", keybind: "E" },
    secondary: { verb: "Observe", keybind: "F" },
    actions: [
        { verb: "Talk", keybind: "E" },
        { verb: "Observe", keybind: "F" }
    ]
}

The client renders both:

   [E] Talk  [F] Observe
         Kael Davan
           [====]

What changes in the client:

  • The prompt renderer reads actions[] instead of just primary
  • Layout goes from single-label to horizontal label row
  • Keybind display pluralizes

What does NOT change:

  • The prompt's position (world-space, entity-tracked)
  • The fade behavior (proximity-triggered)
  • The input routing (keybind → server action request)
  • The overall visual language (bracket-key + verb)

The v0.1 client code:

# InteractionPrompt.gd

func update_prompt(options: InteractionOptions) -> void:
    if options == null:
        hide()
        return

    # v0.1: show primary action only
    # v0.2: iterate options.actions[] for multi-label
    _label.text = "[%s] %s" % [options.primary.keybind, options.primary.verb]
    _name_label.text = options.display_name

    _target_entity = options.entity_id
    show()

The options.actions[] field exists in the protocol from day one. The v0.1 client ignores it. The v0.2 client reads it. No protocol change needed.

Architectural notes for Dudley/Tyre

The InteractionOptions should be part of the ObserverSnapshot update — not a separate message. When the player is near an entity, the snapshot includes the interaction payload. When they move away, it's null. The client never requests interaction options — they arrive passively as part of perception.

This means the server's InteractionResolver system evaluates available actions every tick for entities in the player's interaction radius. In v0.1 that's a simple lookup (NPC → Talk, Object → Examine). In v0.2 it evaluates relationship state, knowledge, and context to determine the full action set.

For Gestalt's Examine-vs-Talk question: The server models both verbs internally from v0.1 onward. It just surfaces only the primary one to the client. If the server determines that "Observe" is more relevant than "Talk" at a given moment (e.g., player has already talked to this NPC this shift, or NPC is currently in an anomalous state), the server can swap the primary. Single-action doesn't mean single-verb-forever — it means the server picks the best one.


Task 2: Pause System State Machine

Three states. Clean transitions. No ambiguity.

States

RUNNING        ← normal gameplay, full sim speed
OVERLAY_ACTIVE ← UI overlay open (knowledge panel, dialogue), 50% sim speed
PAUSED         ← spacebar pause, 0% sim speed

Transitions

                    [Tab]                     [Space]
    RUNNING ──────────────> OVERLAY_ACTIVE ──────────────> PAUSED
       ^                        |                            |
       |         [Tab]          |          [Space]           |
       +────────────────────────+            |               |
       |                                     |               |
       +─────────────────────────────────────+               |
       |                     [Space]                         |
       +─────────────────────────────────────────────────────+

Rules

Input From RUNNING From OVERLAY_ACTIVE From PAUSED
[Space] → PAUSED → PAUSED → previous state (RUNNING or OVERLAY_ACTIVE)
[Tab] (knowledge panel) → OVERLAY_ACTIVE → RUNNING (closes panel) → PAUSED + OVERLAY_ACTIVE (panel opens, stays paused)
[E] (interaction/dialogue) → OVERLAY_ACTIVE (if dialogue starts) stays OVERLAY_ACTIVE no effect
[Esc] no effect → RUNNING (closes overlay) → previous state

Key behaviors:

  1. Spacebar is king. It always toggles to/from PAUSED. Period. No other input overrides spacebar. If the player is in dialogue and hits space, the game pauses with the dialogue still visible.

  2. Overlay = 50% speed, not 0%. When the knowledge panel is open, the world still breathes. NPCs still move (slowly). Time still passes (slowly). This prevents the knowledge panel from becoming a freeze-frame intelligence tool. The player can check their notes, but the world doesn't wait.

  3. Spacebar from OVERLAY_ACTIVE goes to PAUSED, not RUNNING. If I'm reading my knowledge panel and I want the world to stop completely, I hit space. I don't have to close the panel first. Conversely, hitting space from PAUSED with an overlay open returns to OVERLAY_ACTIVE (50% speed with panel still visible), not RUNNING.

  4. Dialogue is an overlay. When in conversation, sim runs at 50%. NPCs outside the conversation keep moving. This means: while you're talking to Kael, Sera might leave the bar. You could miss it. That's the game. If you want to freeze everything, pause.

  5. Multiple overlays don't stack speed reductions. If dialogue is active (50%) and the player opens the knowledge panel (also 50%), sim stays at 50%. Not 25%.

Implementation (client-side)

# PauseManager.gd
enum SimState { RUNNING, OVERLAY_ACTIVE, PAUSED }

var _state: SimState = SimState.RUNNING
var _pre_pause_state: SimState = SimState.RUNNING
var _active_overlays: int = 0  # count of open overlays

func _input(event: InputEvent) -> void:
    if event.is_action_pressed("pause"):  # spacebar
        _toggle_pause()
    elif event.is_action_pressed("knowledge_panel"):  # tab
        _toggle_knowledge_panel()

func _toggle_pause() -> void:
    if _state == SimState.PAUSED:
        _set_state(_pre_pause_state)
    else:
        _pre_pause_state = _state
        _set_state(SimState.PAUSED)

func _toggle_knowledge_panel() -> void:
    if _knowledge_panel_open:
        _close_knowledge_panel()
    else:
        _open_knowledge_panel()

func _open_knowledge_panel() -> void:
    _knowledge_panel_open = true
    _active_overlays += 1
    if _state == SimState.RUNNING:
        _set_state(SimState.OVERLAY_ACTIVE)
    # if PAUSED, stay PAUSED — panel opens visually but sim doesn't resume

func _close_knowledge_panel() -> void:
    _knowledge_panel_open = false
    _active_overlays -= 1
    if _active_overlays == 0 and _state == SimState.OVERLAY_ACTIVE:
        _set_state(SimState.RUNNING)

func _set_state(new_state: SimState) -> void:
    _state = new_state
    match new_state:
        SimState.RUNNING:
            Engine.time_scale = 1.0
        SimState.OVERLAY_ACTIVE:
            Engine.time_scale = 0.5
        SimState.PAUSED:
            Engine.time_scale = 0.0

Question for Dudley

Does the server need to know about pause state? If the client sets Engine.time_scale = 0.0, does the server keep ticking and the client just stops rendering? Or does the client need to signal the server to pause its tick loop?

For single-player (v0.1), I think the client controls this entirely. The server runs as a subprocess — if the client freezes its consumption of server ticks, the server's output buffer fills and it naturally stalls. But this depends on the IPC design. Dudley should confirm.

For the 50% overlay speed: does the client request half-speed ticks from the server, or does it receive full-speed ticks and render every other one? I lean toward the client setting the tick request rate. Cleaner.


Task 3: Monologue Display Constraints (Confirmed)

Mellanie needs hard numbers. Here they are.

Display constraints

Parameter Value Rationale
Max characters 160 Two lines at ~80 chars/line at reference font size. Fits 40% viewport width.
Max lines 2 One breath. Monologue competes with gameplay — the player is moving, looking, deciding. Two lines is a glance. Three is a paragraph.
Display duration (base) 4.0 seconds Long enough to read 160 characters at comfortable pace (~40 chars/sec reading speed for on-screen text).
Display duration (short, <80 chars) 3.0 seconds Short lines don't need 4 seconds. Scale linearly.
Display duration (urgent chime) 5.0 seconds Anomaly observations deserve an extra beat. The player needs time to register that something changed.
Fade in 0.3 seconds Fast enough to not feel laggy. Slow enough to not pop.
Fade out 0.5 seconds Slightly slower than fade-in. The thought lingers.
Interruption fade 0.15 seconds When a new monologue replaces the current one, the old one exits fast. No collision.
Cooldown between lines 2.0 seconds minimum Two monologues back-to-back feel like a data dump. The gap is where the player absorbs.
Max queue depth 1 If two triggers fire simultaneously, the higher-priority one displays and the other is dropped. No stacking, no scroll-back. The character's mind moves on.

Priority ordering (when two triggers fire at once)

1. observe_anomaly        ← the game is telling you something important
2. witness_interaction    ← the game is telling you something happened
3. discover_evidence      ← you found something
4. post_conversation      ← reaction to what just happened
5. hear_sound             ← environmental awareness
6. observe_npc            ← identification / emotional reaction
7. enter_location         ← atmospheric
8. return_visit           ← atmospheric
9. time_idle              ← lowest — reflective, only when nothing else is happening

observe_anomaly always wins. If THE FRIEND's contradiction fires at the same time as an enter_location line, the contradiction shows. The atmospheric line is gone. The player will never know it existed. That's fine — the important thing happened.

What Mellanie should write to

Target: 80-120 characters for most lines. 160 is the hard maximum — use it rarely. The best monologue lines are 40-80 characters. One punchy thought.

Examples from the authoring guide, with character counts:

  • "Morning shift. Recycled air and cargo lubricant. Home sweet home." — 66 chars. Perfect. One line, one breath.
  • "Kael's here. Good — I was starting to worry." — 47 chars. Even better. Fast, emotional.
  • "Dock worker Davan — lattice activity spiked. Three pings in two minutes. Expecting a message? Or checking for surveillance?" — 124 chars. This pushes into line 2 but earns it — the detective's analytical voice needs the full thought.
  • "He looked left. He always looks left when he's making something up. Two years I've known that tell. Kael is lying to me." — 121 chars. This is a climax moment. Two lines justified.

Rule of thumb: If it fits on one line (~80 chars), it should be one line. Two lines are for moments that matter.

Font specification (for reference)

Monologue text at reference resolution (1920x1080):

  • Font: Clean sans-serif (Godot default or a custom face — Araminta's call)
  • Size: 18-20px equivalent
  • Line height: 1.4x
  • Max width: 40% of viewport = ~768px at 1920 wide
  • At 18px, ~80 characters fit in 768px with standard proportional font metrics

This gives comfortable readability at typical viewing distance. Not tiny, not shouting. The text whispers.

Monologue during dialogue

When a dialogue panel is active, monologue can still fire (e.g., observe_anomaly while in conversation). The monologue display shifts up to sit above the dialogue panel:

Normal state:
+------------------------------------------------------------------+
|                                                                    |
|                        GAME WORLD                                  |
|                                                                    |
|  +--monologue----------------------------+                         |
|  | Kael's here. Good.                    |                         |
|  +----------------------------------------+                        |
|                                                                    |
+------------------------------------------------------------------+

During dialogue:
+------------------------------------------------------------------+
|                                                                    |
|                        GAME WORLD                                  |
|                                                                    |
|  +--monologue----------------------------+                         |
|  | That's not anyone from our rotation.  |                         |
|  +----------------------------------------+                        |
+------------------------------------------------------------------+
|  KAEL DAVAN                                                        |
|  "Just a friend. Don't worry about it."                            |
|                               [Continue]          [End]            |
+------------------------------------------------------------------+

The monologue sits in the world layer. The dialogue sits in the HUD layer. They don't overlap. The player reads dialogue first (larger, centered, paneled), catches monologue peripherally (smaller, left, ghostly). Two voices — one external, one internal — simultaneously.

This is critical for Wow Moment #3. The contradiction fires monologue WHILE the player might be in conversation. The internal "What the hell?" happens at the same moment as the external smooth deflection. The UI must support both without making the player choose which to read.


Responses to Round 1 Open Questions (Answered by Lead Decisions)

D-04 (Examine vs Talk) — Resolved

Lead decision: conceptually separate, v0.1 ships single-action. Covered in Task 1 above. The server models both verbs; the client surfaces one. Extensible to multi-verb in v0.2 without protocol changes.

D-05 (Knowledge Panel Pause) — Resolved

Lead decision: 50% sim speed for overlays, spacebar always pauses. Covered in Task 2 above.

Client text delivery — Resolved (A-12, proposed D-048)

Server sends full text, client renders. Confirmed by lead and tracking doc. No local content loading on the client.


Updated UI Bill of Materials (incorporating Round 2 decisions)

System Scenes Scripts Shaders Priority Notes
Monologue renderer 1 1 0 P0 Fade, timing, priority queue, character tint
Interaction prompt 1 1 0 P0 World-space tracking, single-action (extensible to multi)
Dialogue panel 1 1 0 P0 Semi-transparent, text advance, exit handling
Entity color system 0 1 1 P0 D-033 palette, 0.5s fade transitions
Pause manager 0 1 0 P0 State machine: RUNNING / OVERLAY / PAUSED
HUD bar 1 1 0 P1 Location + time phase. Placeholder text in v0.1
Minimap 1 1 1 P1 Insert-style, player dot + area bounds
Knowledge panel 1 1 0 P1 Right-side overlay, People/Places/Observations
News ticker 1 1 0 P1 World-space scrolling text on bar display entity
Fog rendering 0 0 1 P0 Shared with server team (shadowcast → visual)
TOTAL 7 9 3

Reduced from 9 scenes to 7 (pause manager and entity colors don't need dedicated scenes). Added 1 shader (fog rendering confirmed as client-side).


Dependencies I'm Waiting On

From What I Need Blocks
Dudley IPC message format — specifically ObserverSnapshot structure for interaction options, monologue payloads, dialogue payloads. Interaction prompt, dialogue panel, monologue renderer
Dudley Pause architecture — does client control sim speed, or does it signal the server? Pause manager
Tyre ObserverSnapshot v3 protocol spec (Round 2 task for Tyre). Everything that reads server state
Araminta Font selection, diegetic styling direction for knowledge panel. Knowledge panel visual treatment
Mellanie First monologue content (even 5-10 lines) for integration testing. Monologue renderer testing

I can build all UI scaffolding with mock data. But integration testing needs real payloads from the server and real text from Mellanie. The sooner those arrive, the sooner we know if the display constraints actually feel right at runtime.


Spacebar always pauses. Everything else fades in when needed and disappears when it's done.