Files
settled-reach/governance/decisions/perception.md
T
jpmschweitzerandClaude Opus 4.8 aefbb4bd88 docs(meta): switch ticket-reference convention #N -> T-N (pql migration phase 3)
Adopts the T-NNN convention (T-N == old #N == pql ticket id) across the active
operational layer: governance/ decision records, .claude/{rules,agents,skills},
CLAUDE.md, DECISIONS.md. 283 references rewritten.

Guarded against false positives (17 correctly skipped, each logged):
  - PR references kept (PR #136/#138/... — PRs are a separate #-namespace)
  - non-ticket numbers kept (#4122; the "#1 process failure" idiom; "task #3")
  - only #N where N is an actual ticket id is rewritten; the 1-4 digit word-bounded
    match also excludes 6-digit hex colours in the visual decision records

Git history is NOT rewritten (a commit's #N already equals T-N numerically), and
historical archives (docs/sprints, docs/discussions, docs/workshops) keep their
point-in-time #N. The /pr-process ticket-ID extraction logic moves to T-NNN in the
Phase 4 consumer cutover.

Verified: pql decisions validate ok; sync 357 records / 1057 refs / broken 0 (the
prose edits don't affect decision parsing or the tickets.decision_ref linkage).
Transform committed at tooling/pql-migrate/retag_ticket_refs.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:41:52 +02:00

77 KiB
Raw Blame History

Perception Decisions

How the player observes and interacts with the world: camera, fog, line-of-sight, sound, monologue, perception modes, audio dip profiles, cognitive delays.


D-011: Fog of perception is non-negotiable (Pillar 1 infrastructure)

  • Date: 2026-02-09
  • Decision: Fog of perception is not a feature - it's the minimum requirement for information asymmetry to work with a top-down camera. Without it, the player is omniscient within their viewport and the entire information pillar collapses at the local map level. First-person camera gets this for free; top-down must enforce it.
  • Implementation: Line-of-sight based (shadowcasting), not simple radius. Walls block vision, buildings are opaque, corners create blind spots, z-levels interact with sightlines.
  • Sound as secondary information channel: Events outside LOS can be heard - footsteps, conversations, gunfire, alarms. Sound gives partial/directional information, prompting decisions based on incomplete data.
  • Applies to ALL entities: NPCs use the same LOS/perception system as the player. An NPC who can't see you doesn't know you're there. NPCs have memory and inference (saw you enter a building → knows you're inside). Ties to D-010 principle 2: information boundaries are universal, the player's isn't special.
  • Fog returns when you leave: Previously explored areas revert to fog over time. Information about locations decays. What you saw at the docks yesterday may not be true today.
  • v0.1 scope: 2D shadowcast per z-level + sound propagation. Vertical LOS (looking between floors) deferred.
  • Rationale: Team Leader identified overnight that top-down without fog is a fundamental gap in the information asymmetry design.
  • Raised by: Team Leader (Jeroen), with technical framing by Tyre and Gestalt.

D-015: Camera locked to character, rotation as future option

  • Date: 2026-02-09
  • Decision: Camera is locked to the character at all times. No panning. Optionally rotates to character facing direction (player option, later version).
  • Rationale: Pannable camera breaks the information model - you become a surveillance drone, not a character. Locked camera reinforces "you ARE this person." Rotation with facing direction restores directional audio mapping (binaural becomes viable again) and creates a natural vision cone (front = detailed, peripheral = reduced, behind = blind).
  • Vision cone model (physiological basis):
    • Forward: 120° arc (60° half-angle) — binocular overlap zone where both eyes converge, providing stereoscopic depth and high acuity. Full LOS range, full detail. Server sends only Forward tiles.
    • Behind: 240° arc — not in the forward cone. Previously-explored tiles render with a light fog overlay (art and information preserved, just "not fresh"). The monologue system (D-016) bridges perceptual gaps when the character's senses fire ("Footsteps behind me").
    • Physiological reference: human binocular overlap ~120° (NCBI Visual Fields NBK220).
  • Per-character cone configuration: The 120° baseline is the unaugmented human floor. Bionic implants, perception modes (D-017), and equipment widen the cone. A back-of-head optical implant might expand to 240°+; an ANA-touched character might approach 360°. VisionConeConfig is per-entity — different characters have different awareness envelopes. This is gameplay parameter variation, not a protocol change (D-020).
  • Reference: Hotline Miami's camera made that game terrifying with the same principle.
  • v0.1: Locked camera, no rotation. Vision cone still works on fixed-north map. Rotation deferred as player option.
  • Raised by: Team Leader (Jeroen).
  • Updated: 2026-02-27 — cone angles grounded in human sensory physiology; per-character config documented.

D-016: Internal monologue as core perception/atmosphere system

  • Date: 2026-02-09
  • Decision: The player character has an internal monologue that narrates sensory information the camera can't show, creates atmosphere, provides diegetic hints, and can be an unreliable narrator.
  • Functions:
    • Perception bridge: Translates non-visual senses into character voice. "Footsteps behind me. Two people, unhurried."
    • Atmosphere: Character's running commentary on environment, mood, situation.
    • Diegetic tutorial: Character thinks about what they might do. "That terminal might have access logs." No "press X" popups.
    • Unreliable narrator: Monologue is character's INTERPRETATION, not ground truth. Can be wrong. "Seems quiet. Safe to move." (It wasn't safe.)
    • Character voice: Varies by character background, mood, knowledge. Paranoid Guardian vs confident Senator walking the same street = different monologue = different experience.
  • Production note (Nigel): Cheapest feature on the list - it's text. Thousands of contextual lines, AI-assistable generation, character-specific variants.
  • Raised by: Emerged from team discussion. Tyre proposed text/log as sound option, team recognized broader potential.

D-017: Perception modes as character-build system

  • Date: 2026-02-09
  • Decision: The fog/vision system supports multiple perception modes that vary by character build, equipment, and ascension level. Each mode reveals different information with different trust/quality tradeoffs.
  • Confirmed modes:
    • Natural vision - detail, color, identity. Blocked by walls. Everyone has it.
    • Thermal - heat signatures through walls. Body count but no identity. Higher biononic / military gear.
    • Camera feeds - remote visual from fixed positions. Only where cameras exist. Feed can be spoofed/looped. Hacker, institutional access, insert exploit.
    • Unisphere tracking - location pings of known individuals. Only active inserts, can be masked. Law enforcement / intelligence access.
    • Audio analysis - sound signatures, direction, classification. No visual. Insert processing, trainable skill.
  • Ascension scaling:
    • Baseline: natural vision + carried gear
    • Enhanced: insert-based modes, camera access, audio processing
    • Higher: biononic thermal, enhanced spectrum, passive scanning
    • ANA-touched: pattern recognition across all feeds, predictive awareness
  • Playstyle implications (Nigel): Low-tech Guardian playthrough = survival horror (blind, relying on contacts and paranoia). Senator playthrough = information overload (cameras and tracking but drowning in data). Perception modes are playstyle selectors.
  • Vision cone modification: Perception modes and bionic equipment widen the vision cone beyond the unaugmented 120° baseline (D-015). Examples: back-of-head optical implant → 240°+ arc; thermal overlay → omnidirectional heat signatures through walls; ANA-touched awareness → near-360° pattern recognition. Each mode modifies VisionConeConfig per-entity. The fog shader renders whatever the server classifies — no client-side awareness of cone angles.
  • v0.1: Natural vision cone + basic audio indicators + insert minimap only. Additional modes are milestone features, each self-contained and modular.
  • Engine implication: Each perception mode is an observer query against the information boundary system (D-010 principle 2). Engine doesn't distinguish between eyes/thermal/camera - all are "given this sensor, what state is visible?"
  • Raised by: Team Leader (Jeroen) proposed thermal and camera hacking. Full team developed into perception mode framework.
  • Updated: 2026-02-27 — vision cone modification by perception modes documented.

D-018: Three-range sound model

  • Date: 2026-02-09
  • Decision: Sound information reaches the player through three ranges with decreasing accuracy and trust:
Range Method Info quality Trust level
Close (near/in LOS) Stereo audio (maps to facing direction with camera rotation) High accuracy, identity possible Raw sensory, reliable
Medium (outside LOS, nearby) Visual indicators at fog edge + internal monologue Directional, imprecise, type classification Sensory impression, reliable but vague
Long (across map) Insert notifications, text alerts Specific but delayed, location data Network-dependent, spoofable, manipulable
  • Key insight: Each range is a different information QUALITY, not just distance. Close = trustworthy. Long = potentially compromised. Hostile actors could absolutely spoof long-range feeds.
  • v0.1: Screen-space stereo for close + visual fog-edge indicators for medium. Long-range insert alerts as stretch goal.
  • Raised by: Full team discussion.

D-019: Top-down confirmed as primary camera, 3D cutscenes for key moments [SUPERSEDED by D-148]

  • Date: 2026-02-09
  • Decision: Top-down is the gameplay camera. Final. 3D cutscenes can be used for significant narrative moments (wormhole traversal, Dyson barrier opening, first contact, major reveals).
  • Rationale after full honest review:
    • What first-person would give us (visceral traversal, face-to-face conversations, natural asymmetry, spatial horror) is real but compensated by: vision cone + fog, internal monologue, perception mode overlays, sound model
    • What top-down gives us that first-person can't: multi-layer information observation, tactical combat clarity, system legibility, NPC simulation visibility, strategic UI coexistence, 5-10x faster prototype
    • Key insight (Gestalt): the fun is systems interacting - observation → insert check → thermal → monologue → mental note → exploitation. That sequence is BETTER top-down.
    • Key insight (Paula): monologue interpreting faces/conversations is arguably richer than player reading 3D faces, and more faithful to Hamilton's close-POV writing style
    • 3D cutscenes recapture the visceral moments without burdening gameplay engineering. They're decoupled, can be added as polish, game ships complete without them.
    • Camera change to 3D IS the dramatic signal - player knows something significant is happening (Gestalt)
  • Architecture note (Tyre): Client-server separation means the renderer is swappable. A full first-person client is architecturally possible in the future. Top-down now doesn't mean top-down forever.
  • v0.1: Top-down only. No cutscenes. Those are milestone features.
  • Amendment (2026-02-12, Art Direction Workshop): Camera angle specified as ~15-20° from vertical ("the angle"), rendered in sprite art via orthographic camera. Sprites are drawn as if viewed from a shallow tilt (south-facing front faces visible on objects, entities, and walls), but the Godot camera is purely orthographic — the perspective is an art convention, not a camera setting. Tile grid remains square/orthogonal (64x64). Vision cone math remains pure 2D. 3D render pipeline uses Camera3D at -72.5° from horizontal (midpoint of range) to produce sprites with mathematically correct perspective. Matches Rimworld's approach: orthographic camera, tilt faked entirely in art. Internally referred to as "the angle."
  • Raised by: Team Leader (Jeroen), after full team review of tradeoffs in Round 12. Angle amendment: Team Leader, endorsed unanimously in Art Direction Workshop Round 3.
  • Superseded by: D-148 — Sprint 28 confirmed 30° low-angle with 45° map rotation as default gameplay camera; 3D live rendering replaces sprite art approach.

D-033: Entity color = relationship to player

  • Date: 2026-02-11
  • Decision: Entity color in the top-down view represents the character's relationship to the player character, not an objective property. The same NPC can appear as different colors to different characters. Color is a client-side derivation from RelationshipState component + knowledge state — not an NPC property.
  • Color palette:
    • Unknown/Neutral: Cool teal #4a9ebb — default for unassessed NPCs
    • Known/Friendly: Soft green #6bc9a6 — people the character knows and trusts
    • Person of Interest: Warm amber #e8c547 — monologue or case file has flagged something
    • Hostile/Dangerous: Muted red #d45d5d — character KNOWS there's danger (subjective, not omniscient)
    • Static objects: Muted grey #8b8ba0 — furniture, equipment, environmental
    • Player character: #e0e8ff (detective, cool) / #e8e0d0 (smuggler, warm)
  • Key principle: Red means danger TO YOUR CHARACTER, not danger in the abstract. The detective might see amber (flagged in case file) where the smuggler sees green (trusted colleague). This IS asymmetric information rendered visually.
  • Transition behavior: Color shifts smoothly (0.5s fade) when relationship state changes. THE FRIEND's first color shift (green → amber) should be the first relationship color change in the session — maximum emotional impact.
  • Cross-reference: Internal monologue (D-016), THE FRIEND pattern (D-034)
  • Raised by: Araminta (Round 1 proposal, color palette design), project lead (approved, directive T-2)
  • Dissent: None
  • Amendment (2026-03-17, Sprint 28 workshop): Entity relationship color is NOT displayed via character outlines in normal gameplay. The D-033 color palette (#4a9ebb, #6bc9a6, etc.) is valid within the insert/perception mode overlay only. Normal gameplay renders all characters with uniform dark outlines. See D-154.

D-238: Symmetric shadowcasting (Albert Ford) selected for LOS computation

  • Date: 2026-02-11
  • Renumbered: 2026-06-06 — was D-035, which collided with a distinct same-day decision (content.md "Converged tag taxonomy for dialogue and monologue line pools"). The tag-taxonomy record keeps D-035; this shadowcasting record is now D-238. Pre-2026-06-06 references to "D-035" meaning shadowcasting/LOS point here.
  • Decision: Albert Ford's symmetric shadowcasting algorithm is selected for all line-of-sight computation. The traditional recursive shadowcasting algorithm is rejected.
  • Resolves: Q-018
  • Benchmark results (debug build, 1000 iterations, range 20):
Map Density Symmetric Recursive Speedup
32x32 open 920µs/call 1118µs/call 1.2x
32x32 10% walls 845µs/call 4003µs/call 4.7x
32x32 30% walls 334µs/call 2099µs/call 6.3x
64x64 open 929µs/call 1114µs/call 1.2x
64x64 10% walls 745µs/call 4060µs/call 5.5x
64x64 30% walls 215µs/call 1716µs/call 8.0x
150x150 open 919µs/call 1102µs/call 1.2x
150x150 10% walls 615µs/call 3347µs/call 5.4x
150x150 30% walls 163µs/call 1709µs/call 10.5x
  • Key findings:
    • Symmetric is 1.2-10.5x faster across all configurations (debug build; release will be significantly faster)
    • Advantage increases with wall density — more occlusion means less work for the quadrant-based approach
    • Map size has minimal effect on relative performance at range 20 (both algorithms are bounded by vision range, not map size)
    • All values well within the 100ms tick budget (D-026), even in debug
    • Symmetry property verified: if A sees B, then B always sees A — critical for D-011's requirement that NPCs use the same perception system as the player
  • Implementation: Uses rational fraction slopes (num/den integer pairs) to avoid floating-point drift. Processes 4 cardinal quadrants with coordinate transforms. The production API is compute_fov(is_opaque, origin_x, origin_y, range, z_level) -> VisibilityMap.
  • Raised by: Dudley (implementation + benchmark), Tyre (technical direction)
  • Dissent: None

D-043: Art direction — visual style ("functional warmth")

  • Date: 2026-02-12
  • Decision: Clean 2D with bold silhouettes, tile-based world composition (64x64px visual tiles on dual-scale grid per D-066), lighting-driven atmosphere. Not pixel art, not painted, not 3D. Godot 4 Light2D pipeline (PointLight2D per fixture, LightOccluder2D on walls, CanvasModulate for global ambient). AI-generated assets via Nano Banana / Gemini 2.5 Flash, rendered through 3D pipeline for perspective consistency. Core production principle: "Sprites are shape templates that the lighting system completes" — no baked shadows, no baked lighting, no baked mood.
  • Label: "Functional warmth" (Gore).
  • Resolution chain: 1024x1024 source → 256x256 working (outlines applied at 4-8px) → 64x64 runtime. Bilinear interpolation both passes. Outline color: dark blue-grey #333340.
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.1, §9.1-9.3
  • Raised by: Araminta (lead), unanimously endorsed. Art Direction & Mood Board Workshop (3 rounds + closing).
  • Dissent: None. All four agents independently converged on the same style.

D-044: Art direction — visual hierarchy (entity > object > structure)

  • Date: 2026-02-12
  • Decision: Three-layer visual hierarchy: entities (2px outline, D-033 relationship color, highest saturation) > objects (1px outline, era-appropriate palette, moderate saturation) > structure (minimal/no outline, muted zone palette, lowest saturation). Hard rendering rule: entity always wins visual ties — if an entity and object overlap, the entity's D-033 color must remain visible. This is a readability guarantee, not an aesthetic preference.
  • Entity spec: 24x32 pixel footprint within 64x64px visual tiles. Entities occupy 1x1 subtiles (0.5m) but render across a 2x2 subtile sprite footprint per D-066. D-033 color as primary information signal, silhouette as primary identity signal. One identifying silhouette feature per named NPC (Kael's vest, Lera's apron, Sera's uniform). 3-4 template silhouettes for generic NPCs.
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.4, §1.5
  • Raised by: Araminta (outline spec), Ozzie (readability priority). Unanimously endorsed.
  • Dissent: None
  • Amendment (2026-04-05): The "D-033 relationship color" outline spec for entities applies only in perception mode overlay. D-154 (2026-03-17) establishes uniform dark outlines (#1a1a1a) for all characters in normal gameplay. The visual hierarchy layers remain valid; the entity outline color rule changes from always-on to perception-mode-only.

D-045: Art direction — environmental neutrality (strict zero shift)

  • Date: 2026-02-12
  • Decision: The base world layer never shifts in response to conspiracy activation or investigation state. The rendering pipeline never modifies world-layer visuals in response to narrative state. Zone lighting is fixed. What CANNOT change: light color temperature, shadow depth/direction, tile colors, wall tones, floor patterns, any CanvasModulate shift correlated with narrative state. What CAN change: insert overlay density, entity colors (D-033), monologue frequency/urgency, player character sprite posture. Allowed diegetic changes (everyone experiences them): time-of-day cycle (D-031), weather (physical reality, storyteller can time it).
  • Key insight (Gore): "The player walks into the bar after discovering the conspiracy and it's STILL warm and inviting. That's the horror. The warm light isn't ironic — it's indifferent."
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.10
  • Raised by: Gore and Miri (strict zero position). Araminta and Ozzie moved to this position in Round 2 — convergence through persuasion, not compromise.
  • Dissent: None

D-046: Art direction — lighting system (three-reference model)

  • Date: 2026-02-12
  • Decision: Three-reference lighting: (1) Darkwood — vision cone mechanics, light pooling, graduated fog boundary, darkness-as-weight. Retuned: darkness = uncertainty (not hostility), light = visibility (not safety), beyond the cone = life continuing without you (not monsters). (2) Blade Runner 2049 / Deakins — color temperature as emotional language: warm amber = social/inhabited, cool white = institutional/official, mixed = transitional. (3) Edward Hopper (Nighthawks) — warm interior surrounded by unknowable dark. The game's central image. Emotional register: uncertainty, not dread. Character temperature via spatial paths: smuggler's routine through warm-lit spaces (dock, bar), detective's routine through cool-lit spaces (Commission kiosk, corridors). Same fixtures, different daily routes — no rendering tricks.
  • Godot implementation: PointLight2D (per fixture, per zone) + LightOccluder2D (on walls/obstacles) + CanvasModulate (global ambient) + textured PointLight2D on player (vision cone shape). Stock Godot 4 pipeline.
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.6, §1.11
  • Raised by: Ozzie (Darkwood), Araminta (BR2049 color), Gore (Hopper), Miri (character spatial paths). Unanimously endorsed.
  • Dissent: None

D-047: Art direction — two-tier animation system

  • Date: 2026-02-12
  • Decision: Tier 1 (clear): public daily activities — walking/running (4-6 directional frames), working at terminal/handling cargo (2-3 states), eating/drinking (2-3 states), talking (2-3 states, distinguishable from "standing near"), sleeping (1 state). Instantly readable. Tier 2 (ambiguous): privately motivated behaviors — pausing, looking around, lingering near a location, changing direction, proximity without clear interaction. Player sees the action but cannot determine the intention. The boundary between tiers is invisible to the player.
  • Key insight: "The clear/ambiguous division isn't an art decision — it's the investigation mechanic expressed through animation" (Ozzie). Routine must be readable so deviations are noticeable. Maps to NPC tell system (D-024).
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.12
  • Raised by: Ozzie and Gore (two-tier proposal), Araminta (production spec). Unanimously endorsed.
  • Dissent: None

D-048: Neural insert overlay — visual design

  • Date: 2026-02-12
  • Decision: Geometric data layer (precise positioning, clean lines, structured information) rendered with soft bloom shader pass (~2-3px gaussian blur at ~40% blend on the insert CanvasLayer). D-033 colors gain soft halos rather than hard edges. Smuggler's overlay: thinner, sparser (baseline lattice hardware). Detective's overlay: denser, crisper (augmented lattice hardware). Passive state nearly invisible; active state clean and precise. Insert overlay is NOT affected by the fog shader — insert data is computational, not perceptual. Insert markers can appear in fogged areas if the lattice has that data.
  • Tests: "If switching the overlay OFF would feel like going deaf rather than closing a window, it's working" (Gore). "After 10 minutes, does the player forget the insert is there? If yes, we've succeeded" (Ozzie).
  • Cross-reference: Diegetic insert (D-013), perception modes (D-017)
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.9
  • Raised by: Araminta (geometric spec), Gore (organic requirement). Resolves Round 1 divergence — both survive in synthesis.
  • Dissent: None

D-049: Z-level rendering stack (8 layers)

  • Date: 2026-02-12
  • Decision: 8-layer z-stack: (0) Floor tiles — zone identity, movement surface. (1) Floor objects — cosmetic detail, walked over. (2) Furniture/placed objects — y-sorted with entities, visual complexity center. (3) Entity sprites — y-sorted with Layer 2, D-033 colored. (4) Overhead/wall tops — pipes, ducts, lighting fixtures, signage; semi-transparent partial occlusion. (5) Fog of perception — vision cone mask, fog shader; affects layers 0-4. (6) Insert overlay — lattice HUD elements, bloom-rendered, NOT affected by fog. (7) Monologue/UI — top of stack, always visible. Wall rendering: Option B (visible top + face) for structural walls, Option A (boundary lines) for interior partitions. Sprite stacking out of scope.
  • Key insight (Gore): Overhead occlusion is thematically significant — entities passing behind shelving/under overhangs create local information gaps within otherwise known spaces.
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §1.14
  • Raised by: Araminta (8-layer spec), Gore (overhead as thematic tool). Unanimously endorsed.
  • Dissent: None

D-052: Character favorite colors — object-layer identification

  • Date: 2026-02-12
  • Decision: Each NPC has a favorite color expressed through personal objects (bedding, cushions, mugs, personal items), NOT on entity sprites. Muted register: dusty blue, warm terracotta, faded olive — personal, not faction. Creates secondary identification system: D-033 = relationship to player (entity layer), favorite color = person's identity in space (object layer). Investigation mechanic: recognizing whose stuff is where. Saturation constraint: favorite color saturation must stay below D-033 entity color saturation to preserve visual hierarchy. Godot implementation: mask shader + material duplication per unique color (~0.5-1ms at 50-100 objects). Build for v0.1.2+.
  • Cross-reference: Visual hierarchy (D-044), design principle (D-051)
  • Canonical reference: docs/workshops/art-direction-mood-board/workshop-outcomes.md §7 (D-051 candidate)
  • Raised by: Team Leader (Jeroen), unanimously endorsed.
  • Dissent: None

D-056: Cursor states — insert-styled geometric

  • Date: 2026-02-13
  • Decision: Four cursor states using the neural insert's geometric visual language:
    • Default: Four thin inward-pointing ticks with bloom. White-blue #c8d0e0. Barely visible — the insert's own cursor.
    • Entity hover: Ticks expand outward (150ms). Corner brackets frame entity. Color shifts to D-033 relationship color. Verb tooltip in insert styling. Bloom pulse ~10% brighter on entity outline.
    • Object hover: Ticks rotate 45° to X-shape. Muted grey #8b8ba0 (amber #e8c547 if flagged). Simpler frame than entity.
    • Weapon aim: Hard transition. Ticks extend, center gap widens, lines thicken 1→2px. Warm white #f0e8d8. NO bloom. Entity in sights tints to D-033 color — aiming at a friend (green tint) should feel wrong.
  • All transitions 150ms linear. z-layer 7. Never changes by zone or narrative state (D-045).
  • Cursor changes on LOS, not just proximity. Click interaction range: ~2 subtiles (= 1m per D-066). Weapon-selected mode suppresses interaction prompts unless Shift held ("combat intent trumps social intent").
  • Diegetic test (Stig): Interaction labels render on z-layer 6 (insert overlay). If the insert is off, labels disappear. Passes the "is this information from the character's implant?" test.
  • Rationale: Diegetic — cursor is the insert's own interface element. Consistent with D-048. Entity in weapon sights tinting to D-033 creates moral friction.
  • Cross-reference: Insert overlay (D-048), entity color (D-033), z-stack (D-049)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Araminta (visual spec), Stig (UX rules + diegetic test), Ozzie (weapon suppression)
  • Dissent: None.
  • OQ-07 resolution (2026-02-19, T-522): Insert-off behavior is option (a): cursor shape still changes, verb labels suppressed.
    • Cursor state machine fires normally (entity hover → bracket shape, object hover → X-shape) — the character's body physically orients toward targets as a subconscious/spatial response.
    • Insert does not process targets into actionable data: should_show_interactions() returns false when insert_active == false, and interaction labels (z-layer 6) are hidden via set_insert_active(false) on InteractionList and InteractionPrompt.
    • GameState.insert_active is the source of truth (defaults true in v0.1; wired from snapshot field insert_active).
    • Rationale: diegetically consistent — the body reacts to proximity; the insert reacts to commands.

D-057: Entity interaction — vertical list, insert-styled [PARTIALLY SUPERSEDED — archetype portion deferred to Phase 6]

  • Date: 2026-02-13
  • Supersession note (2026-04-21 / amended 2026-04-22, Sprint 37, T-878, PR #137): The character-archetype verb variation portion of this decision is retired pending a Phase 6 character-model design — not deferred in place with a stub. Per the development cascade (CLAUDE.md), archetype-driven verb relabeling is Phase 6 detail-coloring and has been stripped from the server. Vertical-list structure, Phase 1/Phase 2 split, POI priority flips, and contradiction markers remain live. Relabeling (Open→"Move"/"Stash" vs "Scan"/"Flag") is deleted; container verb labels are now identical across all player states, and uniform labeling is the intended pre-Phase-6 end state, not a regression. Reintroduction gate: a confirmed Phase 6 character-model design is a prerequisite. See docs/architecture/sprint-37-878-audit.md (lead override section) for the cascade rationale.
  • Decision: Entity interactions use a compact vertical list (not radial). 2-4 options max, anchored to entity position. Insert-styled with Araminta's geometric aesthetic. New options unlocked by knowledge changes are highlighted with a gradient glow background. Radial menu reserved for world menu only (D-058). Max 3 visible response options in dialogue context.
  • Server architecture: Two-phase verb computation. Phase 1 (simulation, no KG): compute maximum possible verb set from ObjectType component (Readable, Container, Terminal, Door, Pickup, Furniture — each with specific verb sets). Phase 2 (observer, reads KG): filter by character's knowledge (Confront requires KnowsDetails+ per D-041), apply POI priority flips, add contradiction markers. Character-archetype verb variation implemented as Phase 2 observer filter rules (same crate: smuggler sees "Move/Stash", detective sees "Scan/Flag"). [Removed Sprint 37 — archetype verb relabeling deleted; see supersession note above.]
  • Diegetic test: Labels render on z-layer 6. If insert is off, labels disappear.
  • Rationale: Variable-length text options (e.g., confrontation lines in character voice) break radial spatial memory. List handles 1-4 options cleanly. New-item glow signals "something changed" without UX hazard of geometry transforming under cursor. Two-phase computation enables character differentiation without separate verb systems.
  • References: Disco Elysium (world-embedded indicators), Darkwood (minimal cursor), Rimworld (right-click context list).
  • Cross-reference: Cursor states (D-056), knowledge graph (D-041), z-stack (D-049)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Stig (vertical list structure + diegetic test), Araminta (insert aesthetic), Dudley (two-phase verb computation), Nigel (character-archetype verb sets). Lead resolved: Stig's structure, Araminta's styling.
  • Dissent: Araminta argued for spoke radial (geometry transformation signals qualitative knowledge change — new spoke growing). Lead rejected: items moving under cursor when knowledge changes is a moving goalpost (bad UX while aiming at an option).
  • OQ-07 resolution (2026-02-19, T-522):
    • When insert_active == false, the interaction list hides completely (set_insert_active(false)_hide()).
    • Cursor shape changes still occur per D-056 OQ-07 — list suppression is independent of cursor state.
    • GameState.insert_active drives this at runtime, wired via main.gd on each snapshot.

D-058: World menu — radial, 4 spokes

  • Date: 2026-02-13
  • Decision: Right-click opens a radial world menu. Four spokes: Observe (eye icon), Insert (phone icon), Comms (signal icon), Wait (clock icon). Insert-styled: geometric lines, thin spokes with icons, nearly transparent. Renders on z-layer 6. Drag-release for power users (high-speed drag-to-select), click-click for newcomers. v0.1: 2 spokes only (Observe + Insert), scale to 5-6 later.
  • Rationale: Radial works for world menu because items are fixed categories that don't vary by knowledge state (unlike entity verbs). Spatial memory builds quickly (~10 minutes). Drag-to-select makes the radial feel fast and fluid for experienced players.
  • Cross-reference: Entity interaction (D-057), insert overlay (D-048), z-stack (D-049)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Stig (structure + implementation), Araminta (insert aesthetic)
  • Dissent: None.

D-059: Fog — shader-based, five layers, knowledge-graph-driven

  • Date: 2026-02-13
  • Decision: Fog is a shader-driven system (not particles) with five distinct layers:
    1. Clear (vision cone): Soft gradient edge over 6-8 subtiles (= 3-4 visual tiles per D-066), no hard line. Darkwood approach.
    2. Light fog (peripheral): Desaturated 40-50%, brightness -30%. Animated Perlin noise overlay (8-10s cycle). Entity D-033 colors visible but reduced.
    3. Deep fog (previously explored): Near-monochrome with ~10% "zone temperature" tint (bar=warm dark, hub=cool dark, corridor=neutral dark). More pronounced noise (15-20s cycle). Fog breathes.
    4. Unexplored + maps app: Geometric wireframe outlines #333340. Insert data aesthetic.
    5. Unexplored, no maps: Solid near-black #12141a. Information zero.
  • Fog entities:
    • Sound pings: 2-3 thin concentric expanding rings (sonar-style) from source direction, insert white-blue. Loud = 3 rings bright fast. Quiet = 1 ring faint slow. Fade over 1.5s.
    • Recognized entity: D-033 color glow + faint identifying silhouette feature (e.g., "Kael's vest") + 0.8s breathing pulse + ±0.5 tile position drift (approximate, not exact).
    • Unrecognized entity: neutral grey #555566 blob. No silhouette. No identifying features.
    • Recognition transition governed by cognitive delay (D-060).
  • Knowledge-graph-driven: Same fog shows different information per character based on their KG. Smuggler recognizes dock workers = green icons. Detective sees same entities = grey blobs. "Fog is not darkness — it's the absence of your attention."
  • Soft perception degradation: At high overlay load, diegetic scan-line interference (insert under strain). No hard cap on simultaneous perception modes — soft visual warning instead.
  • Performance: <1ms/frame total. Vision cone = PointLight2D. Fog = natural darkness + noise shader on CanvasGroup (Layer 5). Sound pings = 0-5 sprites. Entity ghosts = 0-10 sprites typical.
  • Cross-reference: Fog of perception (D-011), sound model (D-018), perception modes (D-017), entity color (D-033), z-stack (D-049), knowledge graph (D-041)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Araminta (full visual spec + "fog is not darkness"), Tyre (performance validation), Nigel (replayability case — fog as primary divergence mechanism), Gestalt (signal/answer framework + soft cap rejection), Ozzie (false-positive shapes proposal)
  • Dissent: Gestalt proposed hard cap of 2 active perception modes. Lead rejected — replaced with Araminta's diegetic soft degradation.

D-060: Cognitive delay for fog recognition

  • Date: 2026-02-13
  • Decision: When the player recognizes a heard/sensed entity in fog, recognition is NOT instant. Single cognitive delay system: 0.6s base, 0.3s when observe_anomaly triggers (urgent context). Values are tunable via playtesting. Monologue fires DURING the delay ("Those footsteps... that's Kael's walk"), not after — the monologue IS the recognition. Visual transition: grey blob → D-033 color + silhouette feature over ~0.3s within the cognitive delay window. Natural recognition (organic resolve) feels different from sensor recognition (digital snap with biometric ID — different monologue voice too).
  • Rationale: Recognition should feel like a cognitive event, not a UI update. The delay creates a moment where the player's brain and the character's brain are working together. Context-sensitive urgency (0.3s for anomalies) prevents the delay from feeling sluggish in tense situations.
  • Cross-reference: Fog layers (D-059), monologue (D-016), sound model (D-018)
  • Source: Control & Interaction Workshop (2026-02-13)
  • Raised by: Ozzie (timing values + urgency split, adopted), Araminta (visual transition spec), Gestalt (longer values not adopted, but playtesting may adjust)
  • Dissent: Gestalt proposed 0.8-1.2s (longer, more contemplative). Lead chose Ozzie's shorter values as starting point.

D-061: Dialogue box — unified conversation log, bottom screen, max 20% height, no portraits

  • Date: 2026-02-13
  • Decision: Dialogue occupies the bottom of the screen, max 20% height, max-width 1200px (D-076). NO portraits — the NPC is on screen, a portrait is redundant. Monologue floats ABOVE the dialogue box on z-layer 7 — spatial separation allows monologue to contradict dialogue visually (character thinks one thing while NPC says another). Walk-away via WASD, dialogue fades over 300ms, no close button (D-064). Auto-pause in single-player when implant UI is open; overlay design for multiplayer readiness.
  • Unified conversation log (Sprint 14, T-535): The dialogue box is a single chronological log for player-NPC conversations. Each entry shows Speaker → Target: text with per-character name colours (hash-indexed from configurable palette in data/dialogue-theme.yaml). Player response options render below the log; max 3 visible. Locked options invisible (D-062).
  • Entry lifecycle: All entries share the same timeout (15s + 3s fade, configurable via theme YAML). Walk-away clears response options but preserves log entries — earned information is fair game. Panel auto-hides when all entries expire and no active conversation is in progress.
  • Passive overheard lines: D-078 (overheard NPC conversations) was scrapped per R-012. Passive dialogue display will be redesigned after Phase 5 walkable environment.
  • Rationale: Game world stays live above the dialogue box — player sees NPC body language while talking. Monologue above + dialogue below = the character can think one thing while saying another. Max 3 options + invisible locks = player never knows what they're missing. No portrait because the NPC IS on screen. A single unified log avoids a separate UI element for overheard content and makes the flow of conversation feel natural — active and passive dialogue interleave chronologically.
  • Cross-reference: Invisible locks (D-062), confrontation (D-063), walk-away (D-064), z-stack (D-049), max-width (D-076), overheard NPC conversation (D-078)
  • Source: Control & Interaction Workshop (2026-02-13). Amended Sprint 14 (T-535): unified log architecture.
  • Raised by: Stig (UI spec + no portraits), Lead (20% height constraint + max-width directive). Sprint 14 unified log: Stig (implementation).
  • Dissent: Stig initially proposed 25% height and 50% width centered. Lead constrained to 20% height and max-width.

D-067: Recognition chime fires at onset of cognitive delay

  • Date: 2026-02-16
  • Decision: The monologue recognition chime (D-038 assets 7-8) fires at the ONSET of the cognitive delay (D-060), not at its completion. Chime duration 300-400ms, overlapping with the start of the delay. The chime is "unresolved" — it opens a question, doesn't answer one. Full sequence when recognizing an entity in fog: (1) hear/sense something → (2) chime plays → (3) 0.6s cognitive delay begins (concurrent with chime tail) → (4) monologue text appears during delay ("Those footsteps... that's Kael's walk") → (5) blob transitions to D-033 color + silhouette feature → (6) recognition complete.
  • Rationale: The chime marks the character's attention shifting, not the recognition completing. It creates an "unresolved" sensation — something is happening in the character's mind. Monologue text provides the resolution. This prevents the chime from feeling like a UI notification and instead makes it part of the character's cognitive process.
  • Resolves: Q-014
  • Cross-reference: Audio assets (D-038), cognitive delay (D-060), fog recognition (D-059), monologue (D-016)
  • Raised by: Gestalt (proposal), confirmed by Team Leader (Jeroen)
  • Dissent: None

D-069: Audio dip profiles for dialogue and confrontation

  • Date: 2026-02-16
  • Decision: Three audio dip profiles modify bus volumes during player focus states, all relative to player slider settings (proportional, not absolute):
    • Dialogue dip: Ambient -6 to -8dB, World SFX 0dB, Player Actions 0dB, UI 0dB. 300ms ease-in, 500ms ease-out. Reduces environmental noise to foreground conversation without muting world events.
    • Confrontation dip: Ambient -10 to -12dB + low-pass filter (20kHz → 800Hz), World SFX -4 to -6dB (graduated — loud events like sprinting footsteps break through, quiet sneaking doesn't), Player Actions 0dB, UI 0dB. 500ms ease-in, 1000ms ease-out with filter sweep. Creates emotional/cognitive muffling per D-070.
    • ListeningFocus boost: World SFX +2 to +3dB when player is stationary for 30+ ticks (eavesdrop bonus, per D-071). Expresses heightened attention as audio gain.
  • Key behaviors:
    • All dB values are proportional to player slider setting, not absolute. If player sets World SFX to 50%, the -6dB dip applies to that 50% base.
    • Dips are interruptible — walk-away or stance change kills current tween, starts new tween to neutral.
    • Graduated World SFX dip during confrontation: loud, urgent events (sprinting, doors slamming, alarms) break through at reduced volume. Quiet, careful movement may not be noticed.
  • Rationale: Dips model finite attention. Dialogue suppresses ambient noise (focus on conversation). Confrontation suppresses both ambient and peripheral sounds (emotional tunnel vision per D-070). ListeningFocus boost rewards deliberate eavesdropping. All effects proportional to user volume settings respects player accessibility choices.
  • Cross-reference: Audio architecture (D-068), confrontation vulnerability (D-070), eavesdropping (D-071), audio aesthetic (D-074)
  • Raised by: Inigo (initial spec), Gestalt (graduated approach + ListeningFocus boost), Tyre (implementation details), Ozzie (proportional dip to respect player settings)
  • Dissent: None

D-070: Confrontation as cognitive vulnerability

  • Date: 2026-02-16
  • Decision: Confrontation creates perceptual vulnerability through reduced ambient awareness. Design principle: "Emotional focus creates perceptual vulnerability." Player actions requiring cognitive focus (confrontation dialogue, future: deep terminal reading, complex insert queries) mechanically reduce ambient perception via audio dip (D-069) and may suppress peripheral monologue triggers. This is NOT a punishment — it's a realistic consequence of finite attention. The player mitigates risk by choosing WHERE and WHEN to confront (safe location vs exposed corridor, early shift vs busy period).
  • Mechanical expression:
    • During confrontation: ambient sounds muffled, quiet NPC movement may go undetected (graduated World SFX dip means careful footsteps suppressed, sprinting footsteps still audible).
    • Post-confrontation: delayed monologue may fire for missed events ("Wait — did someone just pass by?").
    • World continues per D-045 environmental indifference — no scripted ambushes, but NPCs on routine may coincidentally pass while player is focused.
    • Vulnerability is probabilistic and emergent, not scripted.
  • Consistency principle: Connects to D-053 sprint suppressing monologue (physical focus limits interpretation). Confrontation suppresses ambient perception (emotional focus limits peripheral awareness). High-focus activities suppress peripheral cognition — this is the shared rule.
  • Must feel "felt, not computed" (Paula): Muffling is psychological tunnel vision, not a debuff tooltip. No UI indicator. Player realizes in retrospect: "I was so focused on Sera I didn't hear Kael walk past."
  • Rationale: Makes WHERE and WHEN to confront meaningful decisions. Confronting in a private corner = safer. Confronting in a busy corridor during shift change = riskier. Player agency through spatial and temporal choice, not RNG.
  • Cross-reference: Audio dip (D-069), confrontation UI (D-063), environmental indifference (D-045), stance system (D-053), monologue (D-016)
  • Raised by: Gestalt (vulnerability principle), Ozzie (graduated dip approach), Paula ("felt not computed" framing)
  • Dissent: None

D-071: No ambient dip for eavesdropping — ListeningFocus boost

  • Date: 2026-02-16
  • Decision: Overheard conversation (eavesdropping) does NOT apply dialogue dip (D-069). Cognitive state is opposite of direct conversation: extracting signal from noise requires MORE ambient awareness, not less. Instead, eavesdrop quality improves via ListeningFocus boost (+2 to +3dB World SFX when stationary 30+ ticks). Eavesdrop information quality is: distance-dependent, stance-modified (D-053 Careful stance grants "tell notice bonus"), ListeningFocus-boosted, and ambient-noise-penalized.
  • Ambient zone affects eavesdrop difficulty:
    • Bar (high ambient) = conversations hidden in murmur, requires proximity + ListeningFocus.
    • Workplace (moderate ambient) = conversations semi-conspicuous, distance-dependent.
    • Corridor (low ambient) = conversations conspicuous, easy to overhear from distance.
  • Information confidence: Overheard information enters knowledge graph at lower confidence (KnowsOf, not KnowsDetails per D-041) unless ListeningFocus + proximity allows high-quality capture.
  • Rationale: Direct conversation = character focuses, ambient dips. Overheard conversation = character strains to hear, ambient stays or boosts. Opposite cognitive states produce opposite audio profiles. Creates meaningful difference between talking TO someone vs listening IN on someone.
  • Cross-reference: Audio dip (D-069), stance system (D-053), knowledge graph (D-041), zone audio (D-072)
  • Raised by: Gestalt (cognitive state framing), Paula (zone-conspicuousness model), unanimously endorsed
  • Dissent: None

D-072: Universal event-driven conversation murmur

  • Date: 2026-02-16
  • Decision: NPC-to-NPC conversations use a single universal murmur sound asset, not zone-specific variants. Zone ambient determines conspicuousness: bar ambient (high murmur density) hides conversations, corridor ambient (low background) makes conversations conspicuous. Maps to D-047 two-tier behavior visibility: bar conversations are Tier 1 (clearly happening, content obscured), corridor conversations are Tier 2 (ambiguous — are they talking or just standing near each other?).
  • Asset split:
    • Bar ambient murmur: baked into amb_bar_layer.ogg (continuous background per D-038).
    • NPC proximity murmur: separate event-driven asset, deferred to future sprint (not in Sprint 7 scope). When implemented, same asset plays everywhere; zone ambient determines audibility.
  • Rationale: One murmur asset + zone-dependent conspicuousness creates the signal/noise dynamic naturally. Bar = high noise floor, conversations blend in. Corridor = low noise floor, conversations stand out. Simpler content production, richer emergent behavior.
  • Cross-reference: Audio assets (D-038), two-tier animation (D-047), eavesdropping (D-071)
  • Raised by: Paula (zone-conspicuousness model), Inigo (scoping to future sprint)
  • Dissent: None

D-076: Dialogue box max-width — 1200px (OQ-29 resolution)

  • Date: 2026-02-19
  • Decision: DIALOGUE_MAX_WIDTH = 1200px. Dialogue box is max 1200px wide, centered on screen. Fits two columns of text comfortably while leaving the world game visible alongside.
  • Derivation: ~62% of target 1920px viewport width. Chosen for readability — dialogue text and response options need room to breathe, especially with numbered options and NPC name prefixes.
  • Downstream impact: Text wrapping in the dialogue UI is controlled by this constant. Box is centered; the game world remains visible left and right.
  • Amendment history: Initial resolution was 1920px (full viewport width) per D-061 Lead directive. Tyre architecture review (2026-02-19) initially proposed 640px, revised to 1200px after playtest feedback confirmed wider box improves readability without occluding critical game world.
  • Implementation: Constants.DIALOGUE_MAX_WIDTH in client/scripts/constants.gd.
  • Cross-reference: Dialogue box (D-061), dual-scale grid (D-066)
  • Amends: D-061 (adds pixel value for max-width)
  • Raised by: Stig (OQ-29), revised per Tyre architecture review and playtest feedback

D-077: Zone temperature memory — server-tracked zone_id (OQ-09 resolution)

  • Date: 2026-02-19
  • Decision: Zone temperature memory is server-tracked via zone_id: Option<u16> on VisibleTile in ObserverSnapshot. The server assigns a zone ID to each tile based on the ZoneMap resource (spatial zone assignment). The client maps zone_id to temperature tint from a local lookup table for deep fog rendering (D-059 layer 3: ~10% zone temperature tint — bar=warm dark, hub=cool dark, corridor=neutral dark).
  • Resolves: OQ-09
  • Rationale: D-073 already mandates zone_id per tile in ObserverSnapshot for audio zone crossfade. With zone_id already on the wire for crossfade, zone temperature memory is essentially free — no additional protocol field needed, no additional server computation beyond the zone lookup. Client-only tracking was rejected because: (1) the server is the authoritative source of zone geometry, (2) client heuristics (remembering last-visited zone) would diverge from server truth at zone boundaries, and (3) the data is already crossing the wire for D-073.
  • Implementation:
    • VisibleTile.zone_id: Option<u16>None for tiles outside any defined zone (corridors, transition spaces). Uses #[serde(default, skip_serializing_if)] for backwards compatibility with v10 clients.
    • ZoneMap resource (simulation/zone.rs): BTreeMap<(i32, i32, i32), u16> mapping tile coordinates to zone IDs. BTreeMap per D-010 principle 4 (deterministic iteration). Populated by map builders at setup time.
    • Zone enrichment runs in compute_observer_snapshot — the observer pipeline's final assembly stage. PerceptionQuery trait remains zone-unaware (zone assignment is map data, not perception geometry).
    • Protocol version bumped to 11.
  • Client contract: Client maintains a zone_id → { name, temperature_tint, ambient_layer } lookup table. Deep fog shader (layer 3) reads zone_id from the last-seen VisibleTile data to apply the ~10% temperature tint. AudioManager reads zone_id to trigger crossfade between ambient layers (D-073).
  • Cross-reference: Fog layers (D-059), zone crossfade (D-073), ObserverSnapshot (D-020), deterministic simulation (D-010)
  • Raised by: Tyre (architecture review, T-523)
  • Dissent: None

D-078: Overheard NPC conversation — passive dialogue panel with occlusion filter SCRAPPED (R-012)

  • Date: 2026-02-19
  • Superseded: 2026-04-10. The entire NPC ambient interaction model is scrapped and will be rebuilt from scratch after a walkable environment exists (Phase 5+). See R-012.
  • Original decision: NPC-to-NPC conversations overheard by the player are displayed in the dialogue panel in passive mode (read-only, no response options). An occlusion filter degrades displayed text based on distance, ambient noise, and player effort (ListeningFocus stance). Monologue remains a separate channel — it reacts to overheard content via the witness_interaction trigger but does not carry conversation text itself.
  • Passive panel behavior:
    • Same D-061 dialogue box, but read-only — no response options rendered.
    • Shows speaker name and intended target (e.g. "Kael → Mira").
    • Visually distinguished from interactive dialogue (dimmed border or header treatment — exact style TBD by Araminta).
    • Walking out of earshot dismisses the panel naturally.
  • Occlusion filter — server-authoritative:
    • Occlusion is computed on the server. The server rolls per-word visibility based on distance, ambient noise, and ListeningFocus stance, then sends pre-occluded text to the client with ... replacing dropped words. The client renders exactly what it receives — no client-side probability logic.
    • Per-word visibility is a probability, not a deterministic cutoff. Each word is independently rolled — same distance, same conversation can yield different fragments across playthroughs.
    • Close / clear: High probability per word — most or all text arrives intact, speaker names shown.
    • Moderate distance / some noise: Medium probability — words arrive as ..., speaker names visible. The player may or may not catch the crucial word.
    • Far / noisy zone: Low probability — speaker indicators only, few or no readable words. Audio murmur still plays per D-072.
    • ListeningFocus stance: Shifts probability curve upward — improves word visibility at current distance.
    • Multiplayer-readiness: Each observer gets their own occluded version computed from their specific distance/noise/stance. Seedable RNG per tick ensures deterministic replay (D-010).
    • Design intent: Stochastic word-drop means the player can never be certain they got the full picture from eavesdropping alone. This pushes them toward direct conversation to confirm — where they must reveal something about what they know. The information asymmetry loop closes on itself.
  • Dual-channel principle: Dialogue panel = what you hear. Monologue = what you think about it. The witness_interaction monologue trigger fires after overheard content is displayed, producing the character's internal reaction ("She's lying to him", "So they do know about the cargo"). These are separate UI elements with separate content pipelines.
  • Content pipeline: Copy team authors NPC-to-NPC dialogue lines once at full fidelity. The server applies occlusion degradation before sending to the client. No need to author multiple fidelity versions.
  • Rationale: Keeps monologue as a clean internal response system — thoughts only, never quoted speech. The dialogue panel already exists for player-NPC interaction; reusing it in passive mode for overheard conversation avoids a new UI element. The occlusion filter makes proximity and attention mechanically meaningful without gating content behind binary thresholds.
  • Cross-reference: Dialogue box (D-061), eavesdropping (D-071), conversation murmur (D-072), monologue system (D-016), knowledge confidence (D-041)
  • Raised by: Lead (passive panel directive, occlusion filter, monologue separation)
  • Dissent: None

D-079: Knowledge Grant Architecture

  • Date: 2026-02-24
  • Decision: All knowledge input (dialogue testimony, physical evidence, POI discovery, NPC-authored initial knowledge) flows through a single KnowledgeEventType::KnowledgeGranted event type. No separate event types for different grant sources. The KnowledgeGrant YAML schema is extended to an untagged enum supporting Fact { fact_id, confidence } and Entity { entity_ref, attributes, confidence } variants. The source field (KnowledgeSource) distinguishes provenance: ToldBy { source_id: StableId, tick } for NPC testimony, DirectObservation { tick } for physical evidence, Heard { tick, range } for overheard conversations. Grants fire at line selection time in process_talk_interaction, server-side, pushed to KnowledgeEventQueue and processed on the same tick. A ContentEntityRegistry resource (BTreeMap<String, StableId>, populated at NPC spawn time) resolves entity_ref strings to StableIds at grant processing time. Content-load validation enforces: confidence strings parse to valid KnowledgeConfidence variants; fact_ids conform to "category.topic" format; entity_refs resolve to registered StableIds. Runtime guardrail: if the granting NPC's KG does not contain the fact being granted, the grant is dropped with tracing::warn!.
  • Scope note — Compound grant variant: Compound { grants: Vec<KnowledgeGrant> } (multiple grants from a single line) deferred to Sprint 18. Sprint 17 ships Fact and Entity variants only. Sprint 18 EntityGrant { attributes: BTreeMap } full variant is committed scope, not aspirational (Paula condition, 2026-02-24).
  • Rationale: Unified event type keeps the knowledge system composable. Grant timing at line selection (not client display) ensures D-010 determinism — tick-stamped and server-authoritative. Entity grants are required for contradiction detection: testimony must create EntityKnowledge with ToldBy source so that a subsequent DirectObservation can detect a discrepancy. Physical evidence uses the same mechanism with DirectObservation source, which is treated as higher-confidence and cannot be contradicted by the NPC-denial path.
  • Raised by: Knowledge Flow & NPC Information Boundaries Workshop — unanimous on grant timing and unified event type. Entity grants in Sprint 17 per team lead decision (2026-02-24), overriding Paula's Round 2 acceptance of Dudley's FactId workaround.
  • Dissent: Paula (Round 2) accepted the FactId workaround with 3 binding conditions; team lead overrode in favour of entity grants path championed by Tyre, Gestalt, and Dudley. Paula's conditions honoured where applicable: (1) ToldBy source flows through ContradictionDetected event payload — satisfied by D-083; (2) Sprint 18 entity grants are committed scope; (3) formal record of this commitment.
  • Implements: Tickets T-545 (schema), T-546 (event + handler)
  • Cross-reference: D-041, D-010, D-083

D-080: NPC-to-NPC Knowledge Propagation

  • Date: 2026-02-24
  • Decision: NPC-to-NPC knowledge transfer is implemented via a transfer_npc_knowledge Bevy system that runs after(run_npc_conversations). The system uses kg_query.get_many_mut([entity_a, entity_b]) for dual-mutable KG access. Knowledge transfer occurs once per conversation at conversation start, not per-line. Rate: flat rng.random_range(1..=3) facts per conversation, drawn from eligible entries sorted by last_updated_tick descending (most recent first). Trust-gated filtering: RelationshipEdge.trust value determines eligible entries (None tier < 0: no transfer; Surface 03: Active facts at KnowsOf+ only; Real 37: facts at any confidence + entity observations; Secret 710: all Active entries). Confidence cap: transferred_confidence = min(source_confidence, KnowledgeConfidence::KnowsOf). Facts with disclosure_blocked: true are never transferred regardless of trust tier. Source construction: KnowledgeSource::ToldBy { source_id: speaker_sid, tick: time.tick }. Player overhearing: when the player is within VOICE_RANGE_TILES (8) of a knowledge-transferring NPC pair, the player gains entity-level KG entries at Suspects confidence with KnowledgeSource::Heard { tick, range: Medium }. Specific overheard fact transfer to player deferred to Sprint 18.
  • Rationale: Reuses run_npc_conversations proximity detection, deterministic StableId-sorted pairing, and conversation lifecycle — avoids building a separate propagation system. Confidence capping at KnowsOf prevents gossip chains from amplifying information. ToldBy source construction is the prerequisite for contradiction detection. The disclosure_blocked flag models secrets whose sharing is existentially dangerous regardless of trust (D-034 NPC design requirement).
  • Resolves: Q-024
  • Raised by: Workshop — unanimous
  • Dissent: None
  • Implements: Ticket T-548
  • Cross-reference: D-041, D-083, D-078 (D-078 scrapped per R-012; transfer_npc_knowledge retained for Phase 5 rewire — see Amendment 2026-04-19)
  • Amendment (2026-04-19, R-012 / T-848): D-078 was scrapped (R-012) and the run_npc_conversations system was deleted in T-848. The transfer_npc_knowledge system is retained in-tree for Phase 5 rewire but no longer fires in production — its Added<NpcConversation> trigger is now only inserted by test fixtures. The design (trust-gated transfer, dual-mutable KG access, KnowsOf confidence cap, disclosure_blocked honoring) is preserved; Phase 5 will wire a new proximity/dialogue trigger in its place. Until then, treat the system as dormant and guard against assuming it runs.

D-081: Unprompted Disclosure Design

  • Date: 2026-02-24
  • Decision: Unprompted disclosure is implemented as a filtered KG query producing a DisclosureCandidates component, consumed by Layer 4 of the dialogue pipeline. The system runs as derive_disclosure_candidates (Active-tier NPCs within player range only; recomputed every 30 ticks via computed_tick freshness check). NPCs check their own KG only — no cross-entity KG queries (D-010 principle 2). Candidate selection filters: Active state only; confidence ≥ KnowsOf (Cautious trait raises threshold); not in per_fact_history (not already disclosed this cooldown period); disclosure_blocked != true. Trait two-stage filter: Stage 1 (what) filters candidate pool by trait-based predicates (Cautious raises confidence floor, Gossipy lowers it, Loyal suppresses facts about protected entities, Talkative overrides witness inhibition). Stage 2 (how) biases line pool scoring in Layer 4. Trigger gates (all must pass): trust tier ≥ Surface toward player; mood not Hostile; contentment ≥ 10; candidates not empty; witness inhibition (no non-trusted NPCs within 5 tiles, OR NPC-player trust = Secret tier, OR Talkative trait override); location privacy (disclosure_context tag compatible with current zone type: private/semi_private/any); per-NPC cooldown (300 ticks / 30 game-minutes) not active; global rate limit not exceeded. Global rate limit: 1 disclosure per 10 ticks maximum across all NPCs; when multiple candidates ready in same window, select by ascending StableId (deterministic, D-010). Rate limiting layers: (1) per-fact per-NPC per_fact_history — primary; (2) per-NPC 300-tick cooldown — secondary; (3) global 1/10-tick cap — tertiary.
  • Rationale: NPCs checking only their own KG (Option A) is both diegetically correct and mechanically richer: NPCs can say things the player already knows, which creates dramatic irony. Option B (NPC checks player's KG) would collapse this asymmetry at exactly the moments their speech would be most dramatically charged. Separate DisclosureCandidates component prevents DerivedTellState serialization bloat. The location privacy gate creates learnable spatial behavior patterns. The per-fact per_fact_history is the primary narrative quality gate.
  • Raised by: Workshop — unanimous on architecture; Paula and Gestalt on trigger gates; Tyre and Dudley on implementation structure
  • Dissent: Gestalt (Round 1) opposed global rate limit as creating invisible NPC competition. Resolved in Round 2 via deterministic StableId selection. Included.
  • Implements: Tickets T-551, T-172, T-173
  • Cross-reference: D-041, D-082, D-034

D-082: NPC Information Boundaries — MVP Scope

  • Date: 2026-02-24
  • Decision: Sprint 17 MVP information boundary scope: (1) tell_state.rs reads NPC relationship state from KG for OTHER-entity state (replaces direct relationships.entries read for Friendly tell derivation); self-axis components (Secret, Contentment, Tolerance, Mood) continue as ground-truth reads. (2) Unprompted disclosure (D-081) uses KG for candidate selection. No other system retrofits in Sprint 17. derive_tell_state adds Option<&KnowledgeGraph> to query; None falls through to current axis-based behavior (existing tests continue passing). All KG-driven behavior scoped to With<ActiveSim> — Background-tier NPCs receive no KG-based boundary changes. Deferred: conversation partner KG-awareness check (Sprint 18 candidate, kg.knows_entity(&partner_sid) in pairing loop); routine KG awareness (v0.2+); pathfinding KG integration (not planned — failure mode has no safe recovery). Fallback on missing KG entry: fall through to ground truth with tracing::debug! structured logging.
  • Rationale: The minimum viable boundary produces maximum gameplay-visible difference for minimum risk. Pathfinding from KG has an unresolvable failure mode (NPC forgets path nodes = stuck NPCs = undefined movement behavior) and must not be implemented. Self-knowledge always comes from axis components because the NPC is the continuous observer of its own state. The Option<&KnowledgeGraph> addition is backward-compatible.
  • Raised by: Workshop — unanimous
  • Dissent: None. Paula adds that tell_state should eventually incorporate KG-derived secret-exposure intensity — deferred to a future sprint as enhancement.
  • Implements: Tickets T-549 (step 1), T-551 (step 2), ticket T-142
  • Cross-reference: D-041, D-081, D-026

D-083: Contradiction Detection Pipeline

  • Date: 2026-02-24
  • Decision: Contradiction detection is event-driven at KG write time — fires inside observe_entity() (server/src/knowledge/graph.rs) before overwriting an existing entry. Detection algorithm: if the existing entry has source: ToldBy { tick: told_tick } AND last_known_position differs from incoming position AND |current_tick - told_tick| < CONTRADICTION_WINDOW_TICKS (default 600; configurable per contradiction type), then: (1) entry.state = KnowledgeState::Contradicted; (2) entry.contradicted_claim = Some(ContradictionClaim { source: entry.source.clone(), claimed_position: entry.last_known_position, detected_at_tick: current_tick }); (3) proceed to write new observation. A ContradictionDetected { observer, entity_sid, source_display_name: Option<String>, subject_display_name: Option<String> } event is pushed; display names are resolved at detection time via EntityRegistry + NpcName so the monologue system is a pure string consumer. EntityKnowledge gains one new field: pub contradicted_claim: Option<ContradictionClaim> with #[serde(skip_serializing_if = "Option::is_none")] and #[serde(default)] — backward-compatible, zero migration required. Downstream event chain: ContradictionDetected → monologue system (named contradiction line) → relationship shift (ToldBy source entity → PersonOfInterest) → D-033 amber via existing pipeline → AnomalyMarker via detect_anomalies (already tested). Both the ToldBy entry and the DirectObservation entry receive Contradicted state (epistemic neutrality). Location contradiction is automatic (Sprint 17). Attribute contradiction uses content-authored YAML pairs (Sprint 18). Fact contradiction uses content-authored contradicts field on KnowledgeGrant entries (Sprint 18). CONTRADICTION_WINDOW_TICKS = 600 (1 game-hour) as default constant, with content-authored override per claim type.
  • Rationale: Event-driven detection avoids per-tick KG scan overhead (99% of ticks have no new information). The ContradictionClaim struct (typed) was chosen over string encoding in known_attributes: the struct is accessed with direct field reads (no string parsing in the detection hot path); the monologue system needs a typed KnowledgeSource to resolve the source entity's name; string encoding in known_attributes mixes semantic metadata with internal bookkeeping; one optional struct field is additive (no breaking changes, no migration). Steps 25 of the downstream chain (anomaly marker, D-033 color, relationship shift, monologue selection) are already implemented and passing tests. Contradiction detection is the missing link that enables THE FRIEND arc.
  • Resolves: Q-026
  • Raised by: Workshop — unanimous on architecture and downstream chain; Tyre and Gestalt on struct approach; Dudley conceded Option B (string encoding).
  • Dissent: Dudley (Round 1): Option B string encoding as Sprint 17 workaround. Conceded in Round 2. No standing dissent.
  • Implements: Tickets T-547 (struct + detection), T-550 (monologue + event chain)
  • Cross-reference: D-034, D-033, D-041, D-079, D-080

D-086: Insert icon system — custom SVG, no icon font

  • Date: 2026-02-24
  • Decision: The insert overlay (z-layer 6) uses custom SVG icons, not an icon font. The v0.1 icon vocabulary (~1215 shapes: stance indicators, perception mode icons, inventory item silhouettes, border arrows) is too small and too specific for a font. No available icon font (Material Symbols, Phosphor, Feather, Tabler) matches the insert's geometric/diegetic visual language — they all read as "app UI," not "neural lattice overlay." Custom SVGs authored to the insert's constraint set (clean geometry, minimal anchor points, #c8d0e0 chrome color, 1px stroke base weight) ensure all insert elements feel generated by the same system.
  • Why not an icon font:
    • Vocabulary mismatch: No font has "hexagonal corridor access token" or "thermal perception mode" in a sci-fi register. Inventory silhouettes are universe-specific — we author them regardless.
    • Weight precision: The spec defines 1px (smuggler/baseline) vs 12px (detective/augmented) line weight. Custom SVG stroke-width responds to lattice_profile parameter directly. Icon fonts can't vary weight per-character in a single rendering pass.
    • Visual coherence: Mixing a generic icon font with the custom geometric primitives (diamond POI markers, 6px dots, 4px evidence squares, commission grid) creates two visual registers on the same UI layer. All insert elements must feel like they came from one design system.
    • Godot integration: Godot 4 handles SVG natively (AtlasTexture, importable SVGs). Runtime color via ShaderMaterial. No font rendering pipeline or bitmap caching needed.
  • Icon vocabulary (v0.1):
    • Inventory item silhouettes: 34 items at 40×40px (manifest, access token, comm log)
    • Perception mode indicators: 23 icons at 20×20px (deferred until T-315 specs the modes)
    • Stance indicators: 4 icons at 20×20px (WALK, CAREFUL, SPRINT, CROUCH) — delivered Sprint 32 alongside text labels. Icons supplement labels; text remains primary affordance.
    • Interaction prompt icons: 4 icons at 16×16px (talk, observe, follow, examine) — delivered Sprint 32. Supplement interaction verb text per insert HUD wireframe §9.
    • Border arrows and geometric markers (diamonds, dots, squares): drawn as primitives, not icons
  • Authoring constraints: 1px stroke weight base (scaled to 2px for lattice_augmented via parameter), #c8d0e0 as design reference color (runtime-replaceable), minimal anchor points, no decorative curves.
  • Calibration reference: Phosphor Light (MIT) — not for use, but for evaluating small-size readability of custom shapes during authoring. If a custom icon doesn't read as cleanly as a Phosphor Light equivalent at 16px, simplify it.
  • Cross-reference: Insert overlay (D-048), insert HUD wireframe (docs/design/insert-hud-wireframe-v01.md), visual grammar (docs/design/visual-grammar-v01.md §7), cursor states (D-056)
  • Raised by: Araminta (analysis and recommendation), project lead (confirmed).
  • Dissent: None.
  • Amendment (2026-04-05): "Smuggler only per D-065" scope on inventory item silhouettes is obsolete. D-117 eliminates the smuggler/detective frame; D-122 removes named characters. Inventory icons are no longer character-specific. D-065 cross-reference removed.

38 decisions. Last updated: 2026-02-24 (D-086: Insert icon system)