Compare commits

..
61 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.6 4e53a2e979 chore(meta): release v0.1.13
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 18:07:35 +01:00
jpmschweitzerandClaude Opus 4.6 1b08734838 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:53:02 +01:00
jpmschweitzerandClaude Opus 4.6 16bc87c590 docs(sprints): add Sprint 14 "Live" briefings — 22 tickets across 4 teams
NPC mood, trust, routine execution, NPC-to-NPC conversations with D-078
occlusion, monologue display, content line pools, and visual specs.
Teams: server (7), client (3), copy (6), visual (6).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:52:50 +01:00
jpmschweitzerandClaude Opus 4.6 f6eb92cb25 feat(docs): add D-078 — overheard NPC conversation via passive dialogue panel
Server-authoritative stochastic per-word occlusion filter for NPC-to-NPC
conversations. Dialogue panel in passive mode displays pre-occluded text;
monologue remains a clean internal response system. Indexes updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 19:52:44 +01:00
jpmschweitzerandClaude Opus 4.6 e11d54ff5c fix(assets): add missing .import file for sfx_npc_murmur.ogg
Godot requires .import files for stable uid:// references across
editor sessions. All other audio assets have one — this was missed
in the audio branch PR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:48:42 +01:00
jpmschweitzer cf3349b909 Merge remote-tracking branch 'origin/audio' 2026-02-19 18:46:53 +01:00
jpmschweitzer bc1cfabb68 Merge remote-tracking branch 'origin/client' 2026-02-19 18:45:54 +01:00
jpmschweitzerandClaude Opus 4.6 8985340bdd fix(client): address PR #47 review — murmur deferral, zone extraction, 6 tests
Remove premature Voice/VoiceConversation from SOUND_EVENT_ASSETS (D-072
defers murmur to end-to-end sprint). Extract current_zone_id in
GameState.apply_snapshot() as first-class field, eliminating O(N) tile
scan in main.gd (D-020 server-authoritative). Add dir.list_dir_end()
after registry scan. Add rapid zone-crossing + _load_prefs() roundtrip
tests. Enhance comments on hub/workplace same-asset pattern, station
base hum, and confrontation dip replacement semantics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:37:23 +01:00
jpmschweitzer d2d538f4a8 Merge remote-tracking branch 'origin/server' 2026-02-19 18:36:48 +01:00
jpmschweitzerandClaude Opus 4.6 f57560ebe7 docs(meta): add tea comment hanging workaround to CLAUDE.md
tea comment hangs with inline heredocs and multi-line strings.
Document the two-step pattern: write to .tmp/ first, then pass
via $(cat).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:34:39 +01:00
jpmschweitzerandClaude Opus 4.6 dd1dd0feec fix(simulation): address PR #46 review — zone tests, doc accuracy, TBD comment
Add 3 zone enrichment tests to observer pipeline (tile inside zone,
tile outside zone, absent ZoneMap resource). Fix misleading Decay
Observatory doc comment that claimed Direct start when test starts at
KnowsDetails. Add production population TBD note on ZoneMap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:22:48 +01:00
jpmschweitzerandClaude Opus 4.6 513682522f test(audio): Sprint 13 test suite + lambda capture fix (#528-#533)
57 new tests in test_audio_sprint13.gd covering bus setup, dip profiles,
zone crossfade, recognition chime, and murmur wiring. Fix lambda capture
bug in 3 signal tests (String → Array wrapper for reliable closure
capture in Godot 4). Add bus volume reset to before_test() for test
isolation against prefs-loaded state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:19:32 +01:00
jpmschweitzerandClaude Opus 4.6 0e6cc4eef8 feat(audio): Sprint 13 — AudioManager, zone crossfade, dip profiles, murmur wiring (#528, #529, #530, #533)
AudioManager: volume slider persistence (ConfigFile), settings UI with
5 teal-bordered sliders, default_bus_layout.tres for editor. Zone
crossfade: defensive zone_id read from snapshot tiles, 1.5-2s ambient
tween, auto-activates when server ships OQ-09. Dip profiles: dialogue
dip in show/hide_dialogue, ListeningFocus 30-tick gate via
stationary_ticks in game_state.gd. NPC murmur: client plumbing for
event-driven World SFX playback, no-ops until audio asset arrives.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:19:25 +01:00
jpmschweitzerandClaude Opus 4.6 96275c5b90 fix(skills): add agent rules for git and db script usage in sprint-start
Spawned agents are told not to run git commands (team lead handles those)
and to call db/connectors scripts with bare commands only — no python3
prefix or command chaining — so they match the permission whitelist.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:10:07 +01:00
jpmschweitzerandClaude Opus 4.6 51abc3e3c1 feat(simulation): #523 add zone_id to ObserverSnapshot (D-077, protocol v11)
Server-tracked zone_id on VisibleTile for D-073 zone crossfade and
D-059 deep fog temperature tint. ZoneMap resource backed by BTreeMap,
observer enrichment in snapshot assembly. Backwards-compatible: v10
payloads deserialize with zone_id: None.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 18:10:06 +01:00
jpmschweitzerandClaude Sonnet 4.6 c2de509c01 feat(simulation): add Sound Lab, Decay Observatory, Shift Change gauntlet rooms (#505)
Three new gauntlet test rooms for Sprint 13, each targeting a specific
system under test. StableIds are additive-only (66–74).

Sound Lab (66-68): Three NPCs at calibrated tile distances (2, 6, 12
tiles) verify D-018 three-range sound model. Tests confirm Close ≤3,
Medium ≤8, Long ≤20 tile thresholds via SoundEvent::audible_at().

Decay Observatory (69): Single NPC observed in LOS then broken. Tests
confirm D-041 confidence decay: KnowsDetails → KnowsOf → Suspects
(floor), and Stale state when stale_after threshold is exceeded.

Shift Change (70-71): Two NPCs with DailyRoutine components. Test
confirms D-031 phase-boundary check_phase_transition issues PathRequest
only to NPCs not already at their target location.

Sprint 13 reset plates (72-74) wired per existing pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 18:05:46 +01:00
jpmschweitzerandClaude Sonnet 4.6 a394ff6a5f feat(audio): NPC conversation murmur asset (SFX-003)
Produces sfx_npc_murmur.ogg via Stable Audio Open for D-072 universal
event-driven NPC proximity murmur. Organic sonic family (D-074): warm
mid-freq 200-800Hz vocal texture, human voices in adjacent room with
direction and cadence distinguishable, words not. World SFX bus.

Generated 3 candidates (5s/CFG8, 6s/CFG7, 4s/CFG9), selected candidate
1 for richest texture and clearest spatial framing. Unblocks client #533.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 17:10:17 +01:00
jpmschweitzerandClaude Opus 4.6 61c452aca4 chore(config): whitelist remaining skill invocations
Add sprint-plan, pr-push, pr-review, ticket, docs-search, and
workshop-start to shared permission settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:59:58 +01:00
jpmschweitzerandClaude Opus 4.6 4eeaf72456 fix(skills): check sprint progress before closing in sprint-start
Case A on main previously closed the active sprint immediately without
checking ticket completion or open PR status. Now checks both and asks
the user before proceeding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:59:56 +01:00
jpmschweitzer 45a644c701 Merge branch 'main' of ssh://git.schweitz.net:2222/jpmschweitzer/settled-reach 2026-02-19 16:54:35 +01:00
jpmschweitzerandClaude Opus 4.6 c13d97b7d9 chore(meta): release v0.1.12
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:54:22 +01:00
jpmschweitzerandClaude Opus 4.6 749d53f198 chore(meta): release v0.1.12
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:52:07 +01:00
jpmschweitzerandClaude Opus 4.6 bf2ce4ca36 chore(db): backup database after planning merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:51:31 +01:00
jpmschweitzer 72741562e8 Merge remote-tracking branch 'origin/planning'
# Conflicts:
#	CHANGELOG.md
2026-02-19 16:51:20 +01:00
jpmschweitzerandClaude Opus 4.6 6995405950 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:49:57 +01:00
jpmschweitzerandClaude Opus 4.6 03f70ada97 docs(sprints): add Sprint 13 "Sound" briefings — 9 tickets across server, client, audio, visual
Full audio architecture sprint: AudioManager completion, zone crossfade,
dip profiles, recognition chime, murmur pipeline. Server gauntlet expansion
and OQ-09 resolution. Visual monologue display spec unblocks copy pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:49:39 +01:00
jpmschweitzerandClaude Opus 4.6 5ef845e71d chore(db): backup database after copy merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:46:14 +01:00
jpmschweitzer ce5f735949 Merge remote-tracking branch 'origin/copy' 2026-02-19 16:46:06 +01:00
jpmschweitzerandClaude Opus 4.6 500f15960a chore(meta): update changelog
Sprint 12 copy team output: 26 tickets covering foundation docs,
character builds, design documents, content packs, and ambient
expansion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:44:37 +01:00
jpmschweitzerandClaude Sonnet 4.6 d9facc7958 feat(copy): Sprint 12 #194 — generation pass, ambient variant expansion
80 new ambient/routine lines across all 9 content pack dialogue files.
Expands greeting pools and routine floor/bar talk to reduce cycling in play.

Critical lines untouched: knowledge_grants, threshold lines (real trust),
ring-ops insider dialogue, detective confrontation lines.

Per-file additions:
- shift-supervisor: 7 lines (procedural authority variants)
- dock-worker: 12 lines (floor talk, colleague references, shift texture)
- scheduler: 8 lines (scheduling procedure, audit prep)
- new-hire: 7 lines (orientation questions, colleague curiosity)
- courier: 6 lines (arrival/departure variants)
- bar-owner: 8 lines (greeting variants, house-rule phrases)
- bartender: 9 lines (service register, social observations)
- bar-regular: 10 lines (arrival greetings, community texture)
- ring-operative: 7 lines (surface cover only — route-deflection, maintenance cover)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:42:19 +01:00
jpmschweitzerandClaude Sonnet 4.6 2b62aa275f feat(copy): Sprint 12 Wave 3 — workplace, bar, and ring content packs
#190 Workplace content pack — The Terminal:
- shift-supervisor.yaml: Voss (23 lines, ring-complicit via scheduling cover)
- dock-worker.yaml: generic floor worker (21 lines, dual-lens discrepancy awareness)
- scheduler.yaml: Maret (20 lines, WITNESS/SKEPTIC arc, threshold line at real trust)
- new-hire.yaml: fresh hire (14 lines, inadvertent witness via naive observation)
- courier.yaml: Renn Harsk (16 lines, ring-adjacent documentation handler)

#191 Bar content pack — The Last Shift:
- bar-owner.yaml: Lera (23 lines, Torek tab arrangement, Naia threshold line)
- bartender.yaml: Sess (17 lines, social intelligence, behavioral change tracking)
- bar-regular.yaml: generic regular (20 lines, transition window frustration, Tamm notice)

#192 Smuggling ring content pack — Maintenance Corridors:
- ring-operative.yaml: generic operatives (20 lines, dual-register coded language)
  Coded vocabulary: "cans are ready", "manifest is clean", "Drin was asking",
  "dead air", "the gap". Surface and operational registers sound identical.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:36:52 +01:00
jpmschweitzerandClaude Sonnet 4.6 266510c3d8 feat(content): populate knowledge fact catalogs for v0.1 transit district
Add machine-readable fact catalog YAML files for all knowledge categories.
Makes the check-fact-ids pre-commit hook validation functional.

New files: awareness.yaml, behavioral.yaml, knowledge.yaml, social.yaml
Populated: investigation.yaml, contraband.yaml, location.yaml,
           relationship.yaml, world.yaml, progress.yaml

50+ canonical fact IDs covering: smuggling ring operations, investigation
findings, behavioral patterns, situational awareness, location facts,
inter-NPC relationships, and social positioning. Character starting
confidence annotated per fact. Derived from knowledge-vocabulary-v01.md (#368).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:22:12 +01:00
jpmschweitzerandClaude Sonnet 4.6 cfbd00f876 docs(decisions): record Gestalt endorsement of D-075 archetype dimension review
D-075 dissent was pending Gestalt and Nigel input on the no-archetype-tag
decision. Gestalt endorses: access tier tags in content handle archetype
distinction emergently; starting knowledge differentials produce per-character
gate timing without hardcoded archetype filters; maintenance cost of archetype
tags would undermine the "two keyholes on the same world" experience (D-027).
Nigel's input still pending.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:19:31 +01:00
jpmschweitzerandClaude Sonnet 4.6 e40ce61e6f docs(copy): Sprint 12 Wave 2 — Meridian Coverage Map for Sova Transit (#335)
Ten coverage zones from Gate Terminal Approach (comprehensive) through
Corridor 7B (dead air, Severance-enhanced). Maps detective investigative
progression, smuggler operational geography, NPC behavioral modulation by
zone, and coverage-transition monologue triggers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:19:29 +01:00
jpmschweitzerandClaude Sonnet 4.6 6b1c5e4d29 docs(copy): Sprint 12 Wave 1 — Paula's ten design documents
Character schema and builds:
- #179: Character definition schema (6-section formal spec)
- #180: Smuggler character build (full schema instantiation)
- #181: Detective character build (full schema instantiation)
- #182: Divergent starting knowledge (master comparison table)
- #183: Divergent relationships (NPC web per character)

Institutional design:
- #322: Detective chain of command (Veth, Pres, Tsev backstory)

NPC pattern documents:
- #332: Contradiction arc — reusable FRIEND pattern (5 phases, checklist)
- #329: Mirror moment design — 10 paired dual-lens observations

Experience design:
- #259: First 5 minutes (narrative texture additions to Gestalt's systems doc)
- #307: Flat NPC trait pass (Pael, Ren, Tev — traits, routines, dual-lens)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:18:23 +01:00
jpmschweitzerandClaude Sonnet 4.6 6719cf0fe3 docs(copy): Sprint 12 Wave 1 — Miri's five foundation docs
Cultural generation guide (#189), Sova Texture Appendix (#302),
Contraband specification (#321), Sova Station Profile (#320), and
Span Gate Transit Schedule (#336). These are the blocking foundation
docs for all three content packs (#190/#191/#192) and the Meridian
Coverage Map (#335).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:16:04 +01:00
jpmschweitzerandClaude Sonnet 4.6 7916ebac5f feat(copy): Sprint 12 Wave 1 — flat NPCs, environmental text, insert flavor
Completes three unblocked Sprint 12 copy tickets:

#307 — Flat NPC memorable trait pass (3 NPCs)
- npcs/pael.yaml: maintenance tech, NOBODY/CIVILIAN tier 3
- npcs/ren.yaml: drifter, NOBODY/CIVILIAN tier 3
- npcs/tev.yaml: transit worker (looks like lookout), NOBODY/CIVILIAN tier 3
- dialogue/the-terminal/maintenance-tech.yaml: Pael's 6 lines incl. "seems
  important but isn't" B-7 seal complaint (the-terminal_d_040-045)
- dialogue/the-last-shift/day-worker.yaml: Ren's 6 lines incl. night-rotation
  job-seeking line (the-last-shift_d_001-006)
- dialogue/maintenance-corridors/transit-worker.yaml: Tev's 5 lines incl.
  corridor-watching-but-waiting-for-partner line (maintenance-corridors_d_001-005)

#262 — Environmental text content
- environmental/the-terminal.yaml: 7 items — shift board, manifest terminal
  (4471 discrepancy), maintenance clipboard (Pael's B-7 log), compliance
  notice, Drin/Maret work order note, safety placard, bay status board
- environmental/the-last-shift.yaml: 7 items — drink menu, Lera's rules
  (Torek named), notice board (Vessels Tamm missing), graffiti (bathroom),
  specials board, lattice-free zone notice, cargo workers' association flyer
- environmental/maintenance-corridors.yaml: 6 items — access restriction,
  Meridian scan schedule (14:20-14:45 window documented), maintenance log
  (Pael's anomaly report), routing diagram (pencil annotation), air quality
  notice, C-4 temporary storage log (container 4471 thread)

#331 — Diegetic insert flavor text
- insert/smuggler.yaml: POI labels (first-name, state-dependent), operational
  notifications (Meridian window, drop point, ring check-in, Commission patrol),
  system status strings, examination labels
- insert/detective.yaml: POI labels (surname-first, first-name shift for
  allies is the relationship marker), Commission/case-file notifications,
  field logging strings, examination labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 16:10:57 +01:00
jpmschweitzerandClaude Opus 4.6 7c0112bd76 chore(db): backup database after client merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:57:06 +01:00
jpmschweitzer 7874493d2a Merge remote-tracking branch 'origin/client' 2026-02-19 15:56:57 +01:00
jpmschweitzerandClaude Opus 4.6 f42c3209d8 chore(db): backup database after worktree merge
Regenerate msgpack fixtures for combined sound_events + rng_seed struct.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:37:28 +01:00
jpmschweitzerandClaude Opus 4.6 dfcafee3ee fix(client): address PR #43 review — walk key mismatch + 5 suggestions
Fix SOUND_EVENT_ASSETS walk-speed keys to match actual filename
(sfx_footstep_metal_walk), add play_loop null guard, source indicator
colors from Constants, extract CAMERA_DEFAULT_ZOOM, document
consume-once semantics on close_sound_events.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:37:20 +01:00
jpmschweitzer 8c92ae2285 Merge remote-tracking branch 'origin/visual' 2026-02-19 15:36:59 +01:00
jpmschweitzer 4bdcc8c3fa Merge remote-tracking branch 'origin/ci'
# Conflicts:
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_0.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_127.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_2b31m1.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_2b32.msgpack
#	client/tests/fixtures/msgpack/snapshot_boundary_tick_32767.msgpack
#	client/tests/fixtures/msgpack/snapshot_empty.msgpack
#	client/tests/fixtures/msgpack/snapshot_multi_entity.msgpack
#	client/tests/fixtures/msgpack/snapshot_one_npc.msgpack
#	client/tests/fixtures/msgpack/snapshot_player.msgpack
#	client/tests/fixtures/msgpack/snapshot_v2_full.msgpack
#	server/Cargo.toml
#	server/src/bridge/text_renderer.rs
#	server/src/bridge/types.rs
#	server/src/perception/observer/mod.rs
#	server/src/simulation/path_follow.rs
#	server/tests/bridge_ipc.rs
#	server/tests/bridge_tcp.rs
#	server/tests/gen_fixtures.rs
#	server/tests/serialization.rs
2026-02-19 15:35:56 +01:00
jpmschweitzerandClaude Opus 4.6 87a90b27bd feat(simulation): wire footstep sound events into movement system
validate_movement now inserts SoundEventEmitter with Footstep events on
every successful move. Intensity scales by stance: Sprint 0.8, Walk 0.5,
Careful 0.3, Crouch 0.15. Range is Close (3 tiles) for all stances.

This completes the sound event pipeline end-to-end: movement produces
events → collect_sound_events drains to queue → observer snapshot
includes audible events → client bridge receives them.

Addresses Tyre critical review item #1 on PR #42.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:14:52 +01:00
jpmschweitzerandClaude Opus 4.6 f533235f58 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:08:51 +01:00
jpmschweitzerandClaude Opus 4.6 03c44bebaf fix(simulation): address PR #42 review — 8 items from Hoshe and Tyre
1. Sound producers: document empty v0.1 pipeline explicitly (critical)
2. Routine tests: add ActiveSim to 3 tests that passed trivially
3. Rename _observer_pos → observer_pos (used at line 191)
4. Add FactionOnly positive test case (matching faction_id)
5. Fix stale doc comment "Current: 9" → 10 in ObserverSnapshot
6. Remove orphaned SimulationTier/LastInteraction/ScopeTag types
7. Add tracing::warn on FactionOnly non-numeric parse failure
8. Document Medium-range occlusion gap as TODO in audible_at
9. Insert SoundEventQueue in observer test setup_world

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:08:32 +01:00
jpmschweitzerandClaude Sonnet 4.6 3c4bf742ea docs(visual): Sprint 12 — spatial layouts for three v0.1 locations (#311, #312, #313)
Tile-level floor plans with sightline analysis for all three Sova Transit
District locations. Unblocks copy team content packs #190, #191, #192.

- spatial-layout-terminal-v01.md: The Terminal (44×28m), scanner bays,
  main corridor chokepoint, manifest processing, cargo floor, supervisor
  office with window, restricted storage. Sightline table, access tiers,
  NPC traffic density, Triangle 1 + 2 service notes.
- spatial-layout-bar-v01.md: The Last Shift (28×22m + eastern extension).
  Corner booth sightline analysis (sees entrance, bar, card table, back
  room door simultaneously). Bathroom corridor as private exchange zone.
  Triangle 3 + 4 service notes.
- spatial-layout-smuggling-corridors-v01.md: Ring infrastructure overlay
  — restricted storage staging, maintenance corridor network, three
  dead-drop locations with discovery vectors, transition corridor (40m)
  with camera constraint and traffic density. Full operation flow diagram.

Also carries forward minor visual-grammar and placeholder-spec corrections
from earlier in the sprint (wall_top tile spec, cursor D-056 attribution,
transition strip procedural generation clarification).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 15:07:55 +01:00
jpmschweitzerandClaude Opus 4.6 e047218ad2 fix(ci): remove duplicate doc comment, add rng_seed serialization tests (#344, #527)
Review fixes: deduplicate dump_schedule_graph doc comment,
add rng_seed round-trip test and v9→v10 backward compat test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:05:56 +01:00
jpmschweitzerandClaude Opus 4.6 e1a1a9cfc2 fix(client): address PR review warnings — field guard + public API rename
- main.gd: add has("entity_id") guard to _play_recognition_chimes()
  (matches defensive pattern in _play_close_sound_events and update_sound_events)
- sound_indicator_renderer.gd: rename _color_for_type → color_for_type
  (public testable API, not an internal-only method)
- test_rendering.gd: update test calls to match rename

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 15:03:10 +01:00
jpmschweitzerandClaude Opus 4.6 f337bb3ca8 fix(client): correct 5 bugs found in pre-PR review
- sound_indicator_renderer: draw_colored_polygon → draw_polygon (runtime crash)
- sound_indicator_renderer: append+dedup instead of clear — indicators now
  survive 3.5s instead of dying after one server tick
- dialogue_box: wire Constants.DIALOGUE_MAX_WIDTH (640px) instead of
  hardcoded 832px MAX_WIDTH_PX
- dialogue_box.tscn: update default offsets to ±320 (was ±416)
- decisions/perception.md: fix footer typo (1920px → 640px)
- tests updated for new append/dedup indicator behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:59:50 +01:00
jpmschweitzerandClaude Opus 4.6 3d306b8a2b feat(ci): add tracing::instrument to heavy per-tick systems, fix stale protocol comment (#344, #527)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:55:00 +01:00
jpmschweitzerandClaude Sonnet 4.6 a71218f6bc feat(ci): tracing JSON format in CI, tick duration logging, schedule dump (#344, #346)
Tracing (#344):
- Add 'json' feature to tracing-subscriber dependency
- Emit JSON log format when CI=true or RUST_LOG_FORMAT=json is set
  (structured log ingestion in CI pipelines)
- Add tracing::debug! with tick_ms/budget_ms/over_budget fields on each
  tick for performance profiling and tier system debugging prerequisite

Schedule dump (#346):
- Add --dump-schedule CLI flag that prints bevy_ecs schedule graph and exits
  without requiring TCP bridge or world setup
- Add make debug-schedule target for CI artifact generation and diff-based
  regression detection of unintended system reordering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 14:15:46 +01:00
jpmschweitzerandClaude Sonnet 4.6 6a7dc915de feat(bridge): add rng_seed to ObserverSnapshot for deterministic replay (#527)
Adds rng_seed: Option<u64> to ObserverSnapshot. The WRONG button (#507) captures
inputs.jsonl and seed.txt for replay, but seed.txt was writing "unavailable"
because the server did not include the RNG seed in ObserverSnapshot.

Changes:
- bridge/types.rs: PROTOCOL_VERSION 9→10, rng_seed field with serde(default,
  skip_serializing_if = "Option::is_none") for backward compatibility
- perception/observer/mod.rs: inject Res<SimRng> into compute_observer_snapshot,
  populate rng_seed: Some(rng.seed()) each tick
- All test files: add rng_seed: None to ObserverSnapshot constructors
- tests/serialization.rs: bump protocol_version_constant assertion 9→10
- Regenerate msgpack fixtures and golden file for protocol v10

Completes the WRONG button capture loop: replays can now fully reproduce
observed bugs with the exact RNG seed from the capture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 14:15:34 +01:00
jpmschweitzerandClaude Sonnet 4.6 3248603838 feat(simulation): ban HashMap via clippy disallowed_types, fix violations (#343)
Adds server/.clippy.toml with disallowed-types for std::collections::HashMap
and std::collections::HashSet. HashMap iteration order is non-deterministic
and breaks deterministic simulation replay (D-030).

Changes:
- server/.clippy.toml: disallow HashMap and HashSet crate-wide
- simulation/movement.rs: WalkabilityMap.chunks and occupied map → BTreeMap;
  add PartialOrd+Ord to ChunkCoord, TilePosition, TilePresence
- simulation/monologue.rs: MonologueState.shown_ids → BTreeSet (simulation state)
- perception/shadowcast.rs: #![allow] — per-frame FOV scratch, iteration irrelevant
- perception/interpretation.rs: #![allow] — per-frame lookup table, key-only access
- perception/query.rs: #![allow] — sector_lookup is a per-frame read-only cache

Also applies cargo fmt to pre-existing format drift in contraband.rs,
dialogue.rs, test_world/mod.rs, and several integration tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 14:14:07 +01:00
jpmschweitzerandClaude Sonnet 4.6 8e00db2b2d feat(simulation): #94 #99 active tier filtering and transition system
#94 — Active tier simulation (complete):
- Add ActiveSim marker to all 9 test world room NPC spawns
- Fix test entities in routine.rs and path_follow.rs to include ActiveSim
  so With<ActiveSim> queries match correctly in unit tests

#99 — Tier transition logic (complete):
- Implement update_tier_markers system in tier.rs
- Promotes/demotes tier markers by manhattan distance from PlayerCharacter:
  ≤40 tiles → ActiveSim, ≤120 → BackgroundSim, beyond → StateSaved
- Handles cross-z-level as u32::MAX (effectively unreachable)
- No-op when no PlayerCharacter entity present (headless tests safe)
- 11 new unit tests covering all distance bands and boundary cases
- TierPlugin now registers the system after movement::validate_movement

Also picks up extended test coverage added by hoshe:
- observer/tests.rs — 230 lines of perception observer tests
- sound.rs — additional sound event integration tests

All 548 tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 13:54:37 +01:00
jpmschweitzerandClaude Sonnet 4.6 719721cd4c fix(client): apply arch review corrections — 640px dialogue width, D-067 chime
DIALOGUE_MAX_WIDTH: correct to 640px (20 × TILE_SIZE) per Tyre architecture
  review. D-076 updated in decisions/perception.md with amendment note.
  Initial 1920px was D-061 "max-width" but readability wins at 640px.

D-067 recognition chime: wire sfx_monologue_chime to fog entity recognition
  onset. AudioManager.CHIME_RECOGNITION constant added. main.gd tracks seen
  entity IDs in _known_recognition_ids; fires chime on first appearance in
  pending_recognitions, expires when entity leaves the queue. UISounds bus
  (not WorldSFX) per D-038 "monologue chime is a UI sound." Tests added
  to test_audio_bus_routing.gd (Layer 2b).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 13:35:27 +01:00
jpmschweitzerandClaude Opus 4.6 9ea9fd2a74 feat(engine): add rng_seed to ObserverSnapshot for deterministic replay (#527)
Add rng_seed: Option<u64> to ObserverSnapshot (protocol v10). Populated
from SimRng state each tick. Completes the WRONG button capture loop —
seed.txt now writes a valid u64 instead of "unavailable", enabling
deterministic replay from bug reports.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:34:14 +01:00
jpmschweitzerandClaude Opus 4.6 0f5a990f82 feat(ci): ban HashMap in simulation crate via clippy (#343)
Add clippy::disallowed_types for std::collections::HashMap scoped to
the simulation crate. Replace HashMap with BTreeMap in movement.rs for
deterministic iteration order. Allow exception in perception/query.rs
where iteration order is irrelevant (per-frame scratch buffer).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:34:04 +01:00
jpmschweitzerandClaude Opus 4.6 066f8031fd feat(simulation): sprint 12 server — tier system, sound events, KG access, line previewer
Implements 4 completed tickets + partial progress on 2 more:

- #93 Tier marker components (ActiveSim, BackgroundSim, StateSaved + TierPlugin)
- #138 Information tag schema (ObserverAccess enum in knowledge/types.rs)
- #124 Sound event system (SoundEventEmitter, SoundEventQueue, bridge wiring)
- #193 Line previewer CLI (line_preview binary with filter/explain/sequence modes)
- #94 Active tier simulation (in progress — With<ActiveSim> filters)
- #139 Component-level access control (in progress — filter_by_access)

Updates snapshot fixtures and test golden files for new sound_events field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 13:33:23 +01:00
jpmschweitzerandClaude Sonnet 4.6 c6bd7c2db2 feat(client): Sprint 12 — renderer fix, sound pipeline, medium-range indicators
#345: entity_renderer.gd already used entity_id; added regression tests
  confirming old "id" field is rejected and "entity_id" is accepted.

#447 (OQ-29): DIALOGUE_MAX_WIDTH = 1920 added to constants.gd. Full
  viewport width at target resolution (60 × TILE_SIZE), per D-061 Lead
  directive "max-width". Recorded as D-076 in decisions/perception.md.

#126: SoundIndicatorRenderer — fog-edge directional arrows for medium-range
  sound events (D-018). Node2D at z:951 in World scene. Color-coded per
  D-018/D-069 (neutral/voice/danger). GameState.medium_sound_events
  partitions Medium events from snapshot sound_events field. Tests added
  to test_rendering.gd; Hoshe's test_sound_indicators.gd stubs updated.

#125: Close-range stereo audio pipeline wired. AudioManager.play_sound_event()
  maps event_type to D-038 asset key (Footstep/FootstepSprint → sfx_footstep_*).
  GameState.close_sound_events partitions Close events. main.gd calls
  _play_close_sound_events() each snapshot tick. test_audio_bus_routing.gd
  Layer 4 stubs upgraded to real tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 13:33:05 +01:00
jpmschweitzerandClaude Sonnet 4.6 aa6541c745 docs(visual): Sprint 12 — visual grammar and placeholder art spec
Delivers #303 (v0.1 Visual Grammar Document) and #252 (Placeholder Art Spec).

#303 — visual-grammar-v01.md
- 7 sections: zone color palettes (hex values, Zone 1/2/3), entity sizing/proportions
  (24x32px art area in 64x64px canvas, dual-scale grid per D-066), entity color system
  (D-033 relationship colors, transition behavior, saturation hierarchy), z-level rendering
  stack (8 layers, y-sort rules, wall rendering, overhead occlusion), typography baseline
  (Michroma at 4 role sizes, dialogue layout per D-061), animation tier baseline (D-047
  Tier 1/Tier 2 with frame budgets), neural insert overlay (D-048 geometric/bloom spec,
  detective vs smuggler variants, fog interaction, insert-off behavior per D-056/D-057 OQ-07).
- Closes #304 (entity color system spec — covered in §3).
- Unblocks #311, #312, #313, #314, #315, #316, #317, #318, #334.

#252 — placeholder-art-spec-v01.md
- Tile dimensions: 64x64px visual tile, 32x32px sim tile, 1080p base resolution.
- Structural tile colors per zone, object tile spec (tinted rectangles + labels).
- Entity sprite spec: 24x32px art in 64x64px canvas, color-via-shader (no baked colors).
- Animation scope: static placeholder in v0.1, production frame budgets documented.
- File format (PNG with alpha), asset directory structure, naming convention.
- Lighting constraints: no baked lighting, Light2D pipeline does all atmosphere.
- Unblocks #133 (placeholder art pipeline).

Cross-references: D-033, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052,
D-056, D-057, D-059, D-061, D-066.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-19 13:28:17 +01:00
159 changed files with 19369 additions and 136 deletions
+7 -1
View File
@@ -54,7 +54,13 @@
"Skill(git-commit)",
"Skill(worktree-update)",
"Skill(sprint-start)"
"Skill(sprint-start)",
"Skill(sprint-plan)",
"Skill(pr-push)",
"Skill(pr-review)",
"Skill(ticket)",
"Skill(docs-search)",
"Skill(workshop-start)"
],
"deny": [
"Bash(git push --force *)",
+43 -1
View File
@@ -45,7 +45,38 @@ Then follow the **first matching case**:
### Case A: An active sprint exists
The active sprint needs to be closed before moving on.
First, check whether the sprint's work is actually done:
```bash
db/connectors/sprint status
```
This shows ticket counts by status (done, in_progress, backlog).
Also check for open PRs that may contain completed work waiting for
review or merge:
```bash
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
```
Report the full picture to the user:
- Ticket progress (done / in_progress / backlog counts)
- Open PRs (if any — these represent work that's done but not merged)
**If tickets remain unfinished** (in_progress or backlog) **or open PRs
exist**, do NOT close the sprint. Instead, report the current progress
and ask the user what they want to do:
- **Continue working** — switch to a team branch and run `/sprint-start`
there to resume work
- **Review & merge PRs first** — (only if open PRs exist) merge pending
work before deciding whether to close
- **Close anyway** — proceed with the close workflow below (carries over
unfinished tickets)
Use `AskUserQuestion` to confirm. Do not proceed to A1 unless the user
explicitly chooses to close.
#### A1. Close the active sprint
@@ -257,6 +288,17 @@ Task(
prompt: "You are on the {team} team for Sprint {N}.
Branch: `{team}`
RULES:
- GIT: Do NOT run any git commands (commit, push, pull, merge,
checkout, branch, stash, tag, etc.). All git operations are
handled by the team lead.
- DB SCRIPTS: When calling ticket/sprint/sqlite scripts, use
the exact command with no wrappers or chaining. Examples:
db/connectors/ticket show 528
db/connectors/ticket list --sprint {N}
Do NOT prepend python3, do NOT chain with && or ;, do NOT
add cleanup commands. Just the bare command.
1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md
2. Read the decision files referenced in the briefing.
3. Check TaskList for available work.
+2 -4
View File
@@ -116,12 +116,10 @@ git merge origin/main --no-edit
If clean, report the result (fast-forward or merge commit, files changed).
If conflicts, report them and stop.
### 3. Push prompt
### 3. Push
After a successful merge, ask the user if they want to push:
After a successful merge, push the branch:
```bash
git push origin <current-branch>
```
Never push without explicit confirmation.
+67
View File
@@ -6,6 +6,73 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
## [v0.1.13] — 2026-02-20
### Added
- D-078: Overheard NPC conversation — passive dialogue panel with server-authoritative stochastic word occlusion
- Sprint 14 "Live" briefings — 22 tickets across server (7), client (3), copy (6), visual (6)
## [v0.1.12] — 2026-02-19
### Added
- Tier marker components (#93), active tier simulation (#94), tier transition logic (#99)
- Information tag schema (#138), component-level access control (#139)
- Line previewer CLI (#193)
- Sound event system — server pipeline (#124)
- Close-range stereo audio — client positional 2D (#125)
- Medium-range visual indicators — fog-edge directional arrows (#126)
- HashMap ban in simulation crate via clippy (#343)
- Tracing crate infrastructure — JSON format, tick duration logging (#344)
- System dependency graph debug command — `--dump-schedule` CLI flag (#346)
- rng_seed field on ObserverSnapshot for deterministic replay (#527)
- v0.1 Visual Grammar Document (#303)
- Placeholder art specification (#252)
- Spatial layouts: Logistics Hub (#311), Bar (#312), Smuggling corridors (#313)
- Cultural generation guide — 5-dimension framework for Sova Transit District cultural voice (#189)
- Sova Texture Appendix — 20-term slang glossary, sensory profile, Meridian self-censorship rules (#302)
- Contraband specification — unlicensed lattice components, supply chain, street terminology (#321)
- Sova Station Profile — 6 districts, governance, off-station references (#320)
- Span Gate Transit Schedule — hourly schedule, maintenance windows, ring operational calendar (#336)
- Meridian Coverage Map — 10 named zones from Commission-grade to dead air (#335)
- Character definition schema and both character builds — smuggler + detective (#179, #180, #181)
- Divergent starting knowledge and relationships per character (#182, #183)
- Detective institutional chain of command (#322)
- Contradiction arc design document — reusable FRIEND pattern (#332)
- Mirror moment design document — 7 core dual-perspective observation triggers (#329)
- First 5 minutes experience design — systemic opening per character (#259)
- Opening hook content per character (#260)
- Knowledge vocabulary for v0.1 content — entity/world categories, prerequisite format (#368)
- Knowledge state vocabulary — author-facing quick reference (#309)
- Knowledge fact catalogs — 10 YAML files in content/global/knowledge/, 73 canonical facts
- D-075 endorsement — archetype dimension review recorded in decisions/content.md
- Flat NPC memorable trait pass — Pael, Ren, Tev with noise-floor profiles (#307)
- Environmental text content — 20 items across Terminal, Bar, and Corridors with dual-lens notes (#262)
- Diegetic insert flavor text — per-character labels and notification strings (#331)
- News ticker / Meridian feed — 30 lines including batch 44xx recall dual-lens moment (#306)
- Workplace content pack — The Terminal: 5 NPC dialogue files (#190)
- Bar content pack — The Last Shift: 3 NPC dialogue files (#191)
- Smuggling ring content pack — maintenance corridors: coded vocabulary, dual registers (#192)
- Generation pass expansion — 80 ambient variant lines across all 9 dialogue files (#194)
- Sprint 13 "Sound" briefings — 9 tickets across server, client, audio, visual teams; full audio architecture + gauntlet expansion + monologue display spec
### Fixed
- Entity renderer field name bug — `id` vs `entity_id` (#345)
- Dialogue max-width pixel value — 640px per D-076 (#447)
- Routine tests missing ActiveSim — 3 of 5 tests passed trivially without the required tier marker
- `_observer_pos` misleading unused prefix renamed to `observer_pos` (used for sound event filtering)
- Stale protocol version doc comment "Current: 9" corrected to 10
- FactionOnly non-numeric `faction_id` attribute now logs a tracing::warn instead of silently denying
- SOUND_EVENT_ASSETS walk-speed key mismatch — `sfx_footstep_metal` corrected to `sfx_footstep_metal_walk`
### Changed
- Removed orphaned `SimulationTier`/`LastInteraction`/`ScopeTag`/`ScopeKind` types from tier.rs (unused outside own tests)
- Sound pipeline documented as intentionally empty in v0.1 (no producers yet, full pipeline wired)
- Observer test setup now inserts SoundEventQueue resource for integration coverage
- Added FactionOnly positive test case and Medium-range occlusion TODO
- Sound indicator colors sourced from Constants instead of duplicated hex literals
- play_loop() null guard on stream.duplicate()
- Camera zoom fallback uses Constants.CAMERA_DEFAULT_ZOOM
## [v0.1.11] — 2026-02-19
### Added
+7 -1
View File
@@ -35,7 +35,7 @@ db/
decisions/ # Decision domain files (source of truth)
README.md # Domain index and query examples
architecture.md # D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066
perception.md # D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043-D-049, D-052, D-056-D-061
perception.md # D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043-D-049, D-052, D-056-D-061, D-067, D-069-D-072, D-076-D-078
content.md # D-023, D-024, D-025, D-028, D-029, D-032, D-034-D-037, D-050, D-062-D-064
scope.md # D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065
process.md # D-004, D-021, D-022
@@ -132,6 +132,12 @@ tea issue list --login schweitz --repo jpmschweitzer/settled-reach --state open
Key rules:
- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY)
- **Use `--output simple`** for machine-readable output (no table borders)
- **`tea comment` hangs with inline heredocs and multi-line strings.** Always write the comment body to a temp file first, then pass it via `$(cat)`:
```bash
# Step 1: Write content to .tmp/ (gitignored) using the Write tool
# Step 2: Post via cat
tea comment --login schweitz --repo jpmschweitzer/settled-reach <NUMBER> "$(cat .tmp/review-branch.md)"
```
- **`tea pr reject` does not work on your own PRs** — use `tea comment` instead
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
+8 -1
View File
@@ -7,7 +7,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
pre-pr-server pre-pr-client pre-pr-content \
fixtures-client golden-diff golden-update \
checklist-validate checklist-generate \
perf-baseline
perf-baseline debug-schedule
# --- Configuration ---
@@ -53,6 +53,7 @@ help:
@echo " make pre-pr-content Content-scoped pre-PR (schema + cross-ref validation)"
@echo ""
@echo " make setup-hooks Install pre-commit hooks (included in setup)"
@echo " make debug-schedule Print bevy_ecs schedule graph (diff for PR artifacts)"
@echo ""
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
@@ -285,6 +286,12 @@ checklist-generate:
perf-baseline:
@tooling/perf-baseline
# --- Schedule debug (#346) ---
debug-schedule:
@echo "Dumping bevy_ecs schedule graph..."
@cd server && cargo run -- --dump-schedule
content-ron:
cd tooling/content-converter && cargo build --release
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://caeuztw9ah5h5l"
path="res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"
[deps]
source_file="res://assets/audio/sfx_npc_murmur.ogg"
dest_files=["res://.godot/imported/sfx_npc_murmur.ogg-bfd7592cfea1b592d89f107b6cd33838.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
+43
View File
@@ -0,0 +1,43 @@
[gd_resource type="AudioBusLayout" format=3]
; D-068: 5-bus audio architecture — Music, Ambient, WorldSFX, PlayerActions, UISounds.
; All buses route to Master. Volumes managed at runtime by AudioManager autoload.
; AudioManager._setup_buses() creates any missing buses on startup (no-op if present).
[resource]
bus/0/name = "Master"
bus/0/solo = false
bus/0/mute = false
bus/0/bypass_fx = false
bus/0/volume_db = 0.0
bus/0/send = &""
bus/1/name = "Music"
bus/1/solo = false
bus/1/mute = false
bus/1/bypass_fx = false
bus/1/volume_db = 0.0
bus/1/send = &"Master"
bus/2/name = "Ambient"
bus/2/solo = false
bus/2/mute = false
bus/2/bypass_fx = false
bus/2/volume_db = 0.0
bus/2/send = &"Master"
bus/3/name = "WorldSFX"
bus/3/solo = false
bus/3/mute = false
bus/3/bypass_fx = false
bus/3/volume_db = 0.0
bus/3/send = &"Master"
bus/4/name = "PlayerActions"
bus/4/solo = false
bus/4/mute = false
bus/4/bypass_fx = false
bus/4/volume_db = 0.0
bus/4/send = &"Master"
bus/5/name = "UISounds"
bus/5/solo = false
bus/5/mute = false
bus/5/bypass_fx = false
bus/5/volume_db = 0.0
bus/5/send = &"Master"
+4
View File
@@ -24,6 +24,10 @@ UIStrings="*res://scripts/autoloads/ui_strings.gd"
FogState="*res://scripts/autoloads/fog_state.gd"
AudioManager="*res://scripts/autoloads/audio_manager.gd"
[audio]
buses/default_bus_layout="res://default_bus_layout.tres"
[gui]
theme/custom="res://assets/theme/game_theme.tres"
+13 -1
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=20 format=3 uid="uid://bswrmh7w8dbgm"]
[gd_scene load_steps=22 format=3 uid="uid://bswrmh7w8dbgm"]
[ext_resource type="Script" path="res://scripts/main.gd" id="1_main"]
[ext_resource type="Script" path="res://scripts/rendering/world_renderer.gd" id="2_world"]
@@ -16,9 +16,11 @@
[ext_resource type="PackedScene" path="res://ui/world_radial.tscn" id="14_radial"]
[ext_resource type="PackedScene" path="res://ui/dialogue_box.tscn" id="15_dialogue"]
[ext_resource type="Script" path="res://scripts/rendering/fog_entities.gd" id="16_fogent"]
[ext_resource type="Script" path="res://scripts/rendering/sound_indicator_renderer.gd" id="20_soundind"]
[ext_resource type="PackedScene" path="res://ui/gauntlet_hud.tscn" id="17_gauntlet"]
[ext_resource type="PackedScene" path="res://ui/checklist_overlay.tscn" id="18_checklist"]
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="19_bugreport"]
[ext_resource type="PackedScene" path="res://ui/settings_dialog.tscn" id="21_settings"]
[node name="Game" type="Node2D"]
script = ExtResource("1_main")
@@ -96,6 +98,13 @@ script = ExtResource("4_fog")
z_index = 950
script = ExtResource("16_fogent")
; --- z:951 — Medium-range sound indicators (#126, D-018) ---
; Directional arrows at fog boundary for sounds outside LOS.
; Above FogEntities (z:950), below InsertOverlay (CanvasLayer 10).
[node name="SoundIndicators" type="Node2D" parent="World"]
z_index = 951
script = ExtResource("20_soundind")
; --- Camera ---
[node name="Camera2D" type="Camera2D" parent="."]
position_smoothing_enabled = true
@@ -154,3 +163,6 @@ layer = 30
; #495: WRONG button (F12) — bug report capture dialog
[node name="BugReportDialog" parent="ModalLayer" instance=ExtResource("19_bugreport")]
; #528: Audio settings dialog — 5-bus volume sliders, ESC/OPEN_MENU to toggle
[node name="SettingsDialog" parent="ModalLayer" instance=ExtResource("21_settings")]
+129 -5
View File
@@ -5,6 +5,11 @@ extends Node
## No-op fallback when audio assets absent (D-038).
## Spatial audio positioning for close-range sounds (D-018).
# --- D-067: Recognition chime asset key ---
# Fires on first fog recognition (cognitive delay onset). UISounds bus (not WorldSFX).
# Matches sfx_monologue_chime.ogg from D-038 — "neural lattice firing" feel.
const CHIME_RECOGNITION := "sfx_monologue_chime"
# --- Bus names (D-068) ---
const BUS_MUSIC := "Music"
const BUS_AMBIENT := "Ambient"
@@ -36,6 +41,23 @@ const DIP_SPECS := {
# Low-pass filter default cutoff — effectively bypassed at this value.
const FILTER_CUTOFF_DEFAULT := 20500.0
# --- D-073: Zone crossfade constants ---
const CROSSFADE_DURATION := 1.8 # D-073: 1.5-2s spec, mid-range
# Maps server zone_id strings to ambient asset keys (filenames in res://audio/).
# Hub and Workplace intentionally share the same ambient layer (amb_hub_layer) —
# they are the same location type, so hub→workplace transition is a same-asset no-op
# (old_asset != new_asset guard skips the fade-out). Sprint brief consolidates
# D-038's "amb_workplace_layer" to "amb_hub_layer" for v0.1.
# Note: amb_station_base (D-038 global base hum) plays globally via play_loop()
# at startup — it is not zone-dependent and has no ZONE_ASSETS entry.
const ZONE_ASSETS: Dictionary = {
"hub": "amb_hub_layer",
"workplace": "amb_hub_layer",
"bar": "amb_bar_layer",
"corridor": "amb_corridor_layer",
}
# Asset registry: filename stem (e.g. "amb_station_base") → AudioStream
var _registry: Dictionary = {}
@@ -52,12 +74,19 @@ var _ambient_filter: AudioEffectLowPassFilter = null
# Ambient loop players keyed by asset_key (D-073 zone crossfade)
var _ambient_players: Dictionary = {}
# D-073: Zone crossfade state
var _current_zone_id: String = ""
var _zone_tweens: Array = []
signal dip_changed(profile: String)
const PREFS_PATH := "user://audio_prefs.cfg"
func _ready() -> void:
_setup_buses()
_scan_registry()
_load_prefs()
# --- Bus setup ---
@@ -105,6 +134,7 @@ func _scan_dir(path: String) -> void:
if stream:
_registry[file_name.get_basename()] = stream
file_name = dir.get_next()
dir.list_dir_end()
func has_asset(asset_key: String) -> bool:
@@ -137,6 +167,8 @@ func play_loop(asset_key: String, bus: String = BUS_AMBIENT) -> AudioStreamPlaye
return null
stop_loop(asset_key)
var loop_stream := stream.duplicate() as AudioStream
if loop_stream == null:
return null
_enable_loop(loop_stream)
var player := AudioStreamPlayer.new()
player.stream = loop_stream
@@ -163,6 +195,32 @@ func stop_all_loops() -> void:
stop_loop(key)
# --- Audio asset registry: event type → asset key (D-018, #125) ---
# Maps server-sent sound event_type strings to audio asset keys.
# Keys match filename stems in res://audio/ (scanned by _scan_registry).
# Audio assets per D-038: footstep variants (walk / run), NPC murmur (D-072, #532).
# Missing assets no-op gracefully (D-038 fallback pattern).
const SOUND_EVENT_ASSETS: Dictionary = {
"Footstep": "sfx_footstep_metal_walk",
"FootstepWalk": "sfx_footstep_metal_walk",
"FootstepCareful":"sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
"FootstepCrouch": "sfx_footstep_metal_walk", # D-053: same asset until stance-differentiated audio lands
"FootstepSprint": "sfx_footstep_metal_run",
"FootstepRun": "sfx_footstep_metal_run",
}
## Play a close-range sound event at a world tile position (D-018, #125).
## event_type: server RangeCategory::Close event type string (e.g. "Footstep").
## world_tile_pos: server tile coordinates — converted to world pixels internally.
## No-ops if event_type has no registered asset or asset file is absent.
func play_sound_event(event_type: String, world_tile_pos: Vector2) -> void:
var asset_key: String = SOUND_EVENT_ASSETS.get(event_type, "")
if asset_key.is_empty():
return
play_at(asset_key, world_tile_pos * Constants.TILE_SIZE)
# --- Playback: spatial (D-018 close-range) ---
## Play a one-shot spatial sound at a world position (pixels).
@@ -270,19 +328,85 @@ func set_volume(bus: String, volume_db: float) -> void:
var buses: Dictionary = spec.get("buses", {})
var offset_db: float = buses.get(bus, 0.0)
AudioServer.set_bus_volume_db(idx, volume_db + offset_db)
_save_prefs()
func get_volume(bus: String) -> float:
return _bus_volumes.get(bus, 0.0)
# --- Zone crossfade (D-073 stub) ---
# --- Volume persistence (user://audio_prefs.cfg) ---
func _load_prefs() -> void:
var cfg := ConfigFile.new()
if cfg.load(PREFS_PATH) != OK:
return
for bus_name in BUSES:
if cfg.has_section_key("audio", bus_name):
var db: float = cfg.get_value("audio", bus_name, 0.0)
# Apply directly: bypass _save_prefs() on initial load.
_bus_volumes[bus_name] = db
var idx := AudioServer.get_bus_index(bus_name)
if idx >= 0:
AudioServer.set_bus_volume_db(idx, db)
func _save_prefs() -> void:
var cfg := ConfigFile.new()
for bus_name in BUSES:
cfg.set_value("audio", bus_name, _bus_volumes.get(bus_name, 0.0))
var err := cfg.save(PREFS_PATH)
if err != OK:
push_warning("AudioManager: failed to save prefs to %s (error %d)" % [PREFS_PATH, err])
# --- Zone crossfade (D-073) ---
## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot.
## Full crossfade implementation deferred to Sprint 9+ (D-073).
## Stub exists so server integration can call it without conditional checks.
func set_zone(_zone_id: String) -> void:
pass
## Hard boundary trigger with 1.5-2s audio crossfade between ambient layers.
## Interruptible — mid-crossfade zone change tweens from current position.
## No-op if assets absent (D-038) or same zone.
func set_zone(zone_id: String) -> void:
if zone_id == _current_zone_id:
return
var new_asset: String = ZONE_ASSETS.get(zone_id, "")
var old_asset: String = ZONE_ASSETS.get(_current_zone_id, "")
_current_zone_id = zone_id
_kill_zone_tweens()
# Fade out old ambient layer (if different asset from incoming zone)
if not old_asset.is_empty() and old_asset != new_asset:
if _ambient_players.has(old_asset):
var old_player: AudioStreamPlayer = _ambient_players[old_asset]
if is_instance_valid(old_player):
var tween := create_tween()
tween.tween_property(old_player, "volume_db", -80.0, CROSSFADE_DURATION)
tween.tween_callback(stop_loop.bind(old_asset))
_zone_tweens.append(tween)
# Fade in new ambient layer
if not new_asset.is_empty():
if _ambient_players.has(new_asset):
# Already playing (interrupted reverse crossfade) — tween from current volume
var existing: AudioStreamPlayer = _ambient_players[new_asset]
if is_instance_valid(existing):
var tween := create_tween()
tween.tween_property(existing, "volume_db", 0.0, CROSSFADE_DURATION)
_zone_tweens.append(tween)
elif has_asset(new_asset):
var new_player := play_loop(new_asset, BUS_AMBIENT)
if new_player:
new_player.volume_db = -80.0
var tween := create_tween()
tween.tween_property(new_player, "volume_db", 0.0, CROSSFADE_DURATION)
_zone_tweens.append(tween)
func _kill_zone_tweens() -> void:
for tween in _zone_tweens:
if tween != null and tween.is_valid():
tween.kill()
_zone_tweens.clear()
# --- Internal helpers ---
+56
View File
@@ -54,6 +54,25 @@ var rng_seed: Variant = null
# v7 fields (#431, D-059/D-060)
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
# #126, D-018: Medium-range sound events for fog-edge directional indicators.
# Format: [{x, y, event_type, range_category}] — server sends current medium events per tick.
var medium_sound_events: Array = []
# #125, D-018: Close-range sound events for positional 2D audio.
# Format: [{x, y, event_type, range_category}] — consumed once per tick in main.gd.
var close_sound_events: Array = []
# D-071 (#530): Consecutive ticks without player position change.
# Incremented per snapshot in apply_snapshot(). Reset to 0 on movement.
# ListeningFocus boost activates at 30+ ticks (main.gd manages the dip).
var stationary_ticks: int = 0
var _prev_player_position: Vector2 = Vector2(-1e9, -1e9) # sentinel: no previous position
# D-073 (#529): Server-authoritative zone_id from the player's current tile.
# Extracted in apply_snapshot() — avoids O(N) tile scan in main.gd per Tyre review.
# Empty string when zone_id field absent (server hasn't shipped OQ-09 yet).
var current_zone_id: String = ""
func apply_snapshot(snapshot: Dictionary) -> void:
current_snapshot = snapshot
@@ -75,6 +94,14 @@ func apply_snapshot(snapshot: Dictionary) -> void:
push_warning("GameState: no Player entity found in %d entities" % [
visible_entities.size()])
# D-071 (#530): Track consecutive stationary ticks for ListeningFocus boost.
# Compares current player_position against previous snapshot's position.
if player_position == _prev_player_position:
stationary_ticks += 1
else:
stationary_ticks = 0
_prev_player_position = player_position
# Tiles for rendering: test mode sends "tiles", live server sends tile data in "visible_tiles"
if snapshot.has("tiles"):
visible_tiles = snapshot.tiles
@@ -157,6 +184,35 @@ func apply_snapshot(snapshot: Dictionary) -> void:
else:
rng_seed = null
# D-018: Sound events from server — partition by range_category.
# #126: Medium → fog-edge directional indicators.
# #125: Close → positional 2D audio via AudioManager.
if snapshot.has("sound_events") and snapshot.sound_events is Array:
medium_sound_events = []
close_sound_events = []
for se in snapshot.sound_events:
if not se is Dictionary:
continue
var rc: String = se.get("range_category", "")
if rc == "Medium":
medium_sound_events.append(se)
elif rc == "Close":
close_sound_events.append(se)
else:
medium_sound_events = []
close_sound_events = []
# D-073 (#529): Extract zone_id from the player's current tile (server-authoritative).
# O(1) via visible_positions dict would be ideal, but tiles are arrays without
# positional indexing — use the same tile iteration below instead.
current_zone_id = ""
var _px := int(player_position.x)
var _py := int(player_position.y)
for _ztile in visible_tiles:
if _ztile is Dictionary and _ztile.get("x") == _px and _ztile.get("y") == _py:
current_zone_id = _ztile.get("zone_id", "")
break
# v2: visible_tiles with visibility sectors
# Derives visible_positions when not explicitly provided (real server mode)
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
+9
View File
@@ -90,6 +90,15 @@ const PERIPHERAL_ALPHA: float = 0.5
const FACING_INDICATOR_SIZE: float = 6.0
const FACING_INDICATOR_OFFSET: float = 14.0
# D-076 (OQ-29 resolution): Dialogue box max-width in pixels.
# 640px = 20 × TILE_SIZE (32px) — grid-aligned, ~33% of 1920px viewport.
# Tyre architecture review 2026-02-19: readability over max-width; fits
# two columns of text comfortably, leaves world game visible alongside.
const DIALOGUE_MAX_WIDTH: int = 640
# Default camera zoom — used as fallback when get_camera_2d() returns null
const CAMERA_DEFAULT_ZOOM: Vector2 = Vector2(2.0, 2.0)
# #517: Implant UI font color grading — avoid pure white, project through a lens
const IMPLANT_TEXT_COLOR: Color = Color("#E0F7FA") # Cyan-white — primary text
const IMPLANT_TEXT_DIM: Color = Color("#9EBFC4") # Dimmed variant — secondary text
+87
View File
@@ -15,14 +15,19 @@ extends Node2D
@onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests
@onready var checklist_overlay = $UILayer/ChecklistOverlay # #503: auto-checklist progress
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
var _camera_anchored: bool = false
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
var _last_dialogue_tick: int = -1
var _known_recognition_ids: Dictionary = {} # D-067: entity_ids that have already chimed
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay (shared: teleport preempts amber)
var _teleport_in_progress: bool = false # #501: defer smoothing re-enable by one frame after teleport
var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames; flushed into record_tick() on snapshot arrival
var _current_zone: String = "" # D-073 (#529): zone tracking for ambient crossfades
const LISTENING_FOCUS_TICKS: int = 30 # D-071: stationary ticks before ListeningFocus boost activates
func _ready() -> void:
print("The Settled Reach — client initialized")
@@ -113,6 +118,9 @@ func _process(_delta: float) -> void:
if fog_entities and fog_entities.has_method("update_from_state"):
fog_entities.update_from_state()
# D-067: Recognition chime — fire sfx_monologue_chime on first fog recognition
_play_recognition_chimes()
# #496: Update gauntlet HUD (room timer + personal bests)
if gauntlet_hud and gauntlet_hud.has_method("update_from_state"):
gauntlet_hud.update_from_state()
@@ -121,6 +129,16 @@ func _process(_delta: float) -> void:
if checklist_overlay and checklist_overlay.has_method("update_from_state"):
checklist_overlay.update_from_state()
# D-018 #125: Play close-range sound events via positional 2D audio
_play_close_sound_events()
# D-073 (#529): Zone ambient crossfade — detect player tile zone, trigger set_zone on change.
_update_zone()
# D-071 (#530): ListeningFocus boost — stationary 30+ ticks boosts WorldSFX.
# Only activates when no dialogue/confrontation dip is active (D-070).
_update_listening_focus()
# Show monologue if server sent one this tick (#414)
_consume_monologue()
@@ -155,6 +173,14 @@ func _process(_delta: float) -> void:
if bug_report_dialog and not bug_report_dialog.is_active():
bug_report_dialog.start_capture()
continue
# #528: ESC/OPEN_MENU — client-only, toggle audio settings dialog
if input.action == InputMapper.Action.OPEN_MENU:
if settings_dialog:
if settings_dialog.is_open():
settings_dialog.close()
else:
settings_dialog.open()
continue
if input.action == InputMapper.Action.INTERACT:
# D-057: prefer interaction list (multi-verb), fall back to prompt (v0.1)
var target_id: int = -1
@@ -191,6 +217,67 @@ func _process(_delta: float) -> void:
_pending_record_inputs.clear()
# D-018 #125: Play close-range sound events — fired once per snapshot tick.
# Each event is passed to AudioManager.play_sound_event() for 2D positional playback
# on the WorldSFX bus. Events with no registered asset are silently skipped (D-038).
# Consume-once: events are cleared after processing so they don't replay if
# _process runs again before the next server tick (D-009 multiplayer-safe pattern).
func _play_close_sound_events() -> void:
for evt in GameState.close_sound_events:
if not evt is Dictionary or not evt.has("x") or not evt.has("y"):
continue
AudioManager.play_sound_event(
evt.get("event_type", ""),
Vector2(float(evt.x), float(evt.y))
)
GameState.close_sound_events = []
# D-067: Recognition chime — fires sfx_monologue_chime when a fog entity
# enters the cognitive delay recognition queue for the first time.
# "The chime marks the character's attention shifting" (D-067).
# Entities that complete recognition (leave pending_recognitions) are removed
# from _known_recognition_ids so they can chime again if re-encountered.
func _play_recognition_chimes() -> void:
var active_ids: Dictionary = {}
for rec in GameState.pending_recognitions:
if not rec is Dictionary or not rec.has("entity_id"):
continue
var eid: int = rec.entity_id
active_ids[eid] = true
if not _known_recognition_ids.has(eid):
_known_recognition_ids[eid] = true
AudioManager.play(AudioManager.CHIME_RECOGNITION)
# Expire IDs no longer in the recognition queue
for eid in _known_recognition_ids.keys():
if not active_ids.has(eid):
_known_recognition_ids.erase(eid)
# D-073 (#529): Zone ambient crossfade — reads zone_id from GameState.current_zone_id
# (extracted in apply_snapshot(), server-authoritative per D-020).
# Calls AudioManager.set_zone() when zone changes (AudioManager handles crossfade).
func _update_zone() -> void:
var zone := GameState.current_zone_id
if zone != _current_zone:
_current_zone = zone
AudioManager.set_zone(zone)
# D-071 (#530): ListeningFocus boost — World SFX +2.5dB when stationary 30+ ticks.
# Uses AudioManager.get_active_dip() as single source of truth (no separate flag).
# Only activates when no other dip (dialogue/confrontation) is running.
# Only deactivates its own dip — never touches dialogue/confrontation.
# D-070: no UI indicator — the boost is "felt, not computed."
func _update_listening_focus() -> void:
var current_dip := AudioManager.get_active_dip()
var threshold_met := GameState.stationary_ticks >= LISTENING_FOCUS_TICKS
if threshold_met and current_dip == "":
AudioManager.apply_dip("listening_focus")
elif not threshold_met and current_dip == "listening_focus":
AudioManager.clear_dip()
# Consume-once per tick: show monologue text, then clear.
# Tick guard prevents re-triggering when the same tick is polled multiple
# times (client FPS > sim tick rate).
@@ -0,0 +1,152 @@
class_name SoundIndicatorRenderer
extends Node2D
## Medium-range sound indicators (#126, D-018).
## Renders directional arrows at the fog boundary for sounds outside LOS.
##
## Close-range sounds → 2D positional audio (handled by AudioManager).
## Medium-range sounds → visual arrow at fog edge pointing toward source.
##
## Color per D-018/D-069:
## Neutral #c8d0e0 — footsteps, generic sounds
## Voice #e8c547 — speech, conversation, social sounds
## Danger #d45d5d — alert, gunshot, explosion, threat
##
## Each indicator lives for INDICATOR_LIFETIME seconds and fades out over
## the last FADE_DURATION seconds. New events are appended each tick;
## expired indicators are removed by _process(). Deduplication prevents
## the same source position from stacking multiple arrows.
const TILE_SIZE: int = Constants.TILE_SIZE
# Visual parameters
const INDICATOR_LIFETIME: float = 3.5 # Total seconds visible
const FADE_DURATION: float = 0.6 # Fade-out window at end
const EDGE_INSET: float = 20.0 # Pixels inward from viewport edge
const ARROW_HALF: float = 7.0 # Half-width of arrowhead base
const ARROW_LEN: float = 12.0 # Length from tip to base
# D-018/D-069 colors — sourced from Constants to prevent palette drift
const COLOR_NEUTRAL: Color = Constants.INSERT_COLOR_TEXT # Generic / footstep
const COLOR_VOICE: Color = Constants.ENTITY_COLOR_POI # Speech / conversation
const COLOR_DANGER: Color = Constants.ENTITY_COLOR_HOSTILE # Alert / threat / gunshot
# Indicators: [{x, y, event_type, elapsed}]
var _indicators: Array = []
func _process(delta: float) -> void:
if _indicators.is_empty():
return
var i := _indicators.size() - 1
while i >= 0:
_indicators[i].elapsed += delta
if _indicators[i].elapsed >= INDICATOR_LIFETIME:
_indicators.remove_at(i)
i -= 1
queue_redraw()
## Append new medium-range sound events from snapshot.
## events: Array of {x: float, y: float, event_type: String}
## Only Medium range_category events should be passed.
## Deduplicates by tile position — if an indicator already exists at (x,y),
## its timer resets instead of spawning a duplicate arrow.
func update_sound_events(events: Array) -> void:
for evt in events:
if not evt.has("x") or not evt.has("y"):
continue
var ex: float = float(evt.x)
var ey: float = float(evt.y)
# Deduplicate: reset timer if an indicator already exists at this tile
var found := false
for ind in _indicators:
if is_equal_approx(ind.x, ex) and is_equal_approx(ind.y, ey):
ind.elapsed = 0.0
ind.event_type = evt.get("event_type", "")
found = true
break
if not found:
_indicators.append({
"x": ex,
"y": ey,
"event_type": evt.get("event_type", ""),
"elapsed": 0.0,
})
if not _indicators.is_empty():
queue_redraw()
func _draw() -> void:
if _indicators.is_empty():
return
var player_world: Vector2 = GameState.player_position * TILE_SIZE
# Compute half-extents of the visible world area from camera zoom + viewport.
# Arrows are placed at this boundary minus EDGE_INSET so they sit just inside
# the fog edge and don't clip to the physical screen border.
var vp_size := get_viewport().get_visible_rect().size
var cam := get_viewport().get_camera_2d()
var zoom := cam.zoom if cam else Constants.CAMERA_DEFAULT_ZOOM
var half_extents: Vector2 = vp_size / (2.0 * zoom)
for ind in _indicators:
var sound_world: Vector2 = Vector2(ind.x, ind.y) * TILE_SIZE
var dir: Vector2 = sound_world - player_world
if dir.is_zero_approx():
continue
dir = dir.normalized()
# Project direction onto the visible-area rectangle boundary
var edge_pt: Vector2 = _rect_edge_point(player_world, dir, half_extents, EDGE_INSET)
# Alpha: full for most of lifetime, fade out over last FADE_DURATION seconds
var t: float = ind.elapsed / INDICATOR_LIFETIME
var fade_start: float = 1.0 - FADE_DURATION / INDICATOR_LIFETIME
var alpha: float
if t < fade_start:
alpha = 1.0
else:
alpha = lerpf(1.0, 0.0, (t - fade_start) / (FADE_DURATION / INDICATOR_LIFETIME))
var color: Color = color_for_type(ind.event_type)
color.a = alpha * 0.9
_draw_arrow(edge_pt, dir, color)
## Project from center along dir to the boundary of a rectangle with
## half_extents, inset by inset pixels. Returns the boundary point.
func _rect_edge_point(center: Vector2, dir: Vector2, half: Vector2, inset: float) -> Vector2:
var h := Vector2(
maxf(half.x - inset, 8.0),
maxf(half.y - inset, 8.0),
)
# Ray-AABB slab test: find smallest positive t where the ray exits
var t_x: float = INF if abs(dir.x) < 1e-6 else abs(h.x / dir.x)
var t_y: float = INF if abs(dir.y) < 1e-6 else abs(h.y / dir.y)
var t: float = minf(t_x, t_y)
return center + dir * t
## Draw a filled arrowhead at pos pointing in dir.
## The tip is at pos; the base is ARROW_LEN pixels behind along -dir.
func _draw_arrow(pos: Vector2, dir: Vector2, color: Color) -> void:
var perp := Vector2(-dir.y, dir.x)
var base_center := pos - dir * ARROW_LEN
draw_polygon(
PackedVector2Array([pos, base_center - perp * ARROW_HALF, base_center + perp * ARROW_HALF]),
PackedColorArray([color, color, color])
)
## Map event type string → D-018 color category.
func color_for_type(event_type: String) -> Color:
var et := event_type.to_lower()
if et.contains("voice") or et.contains("speech") or et.contains("convers") or et.contains("talk"):
return COLOR_VOICE
if et.contains("danger") or et.contains("gunshot") or et.contains("explosion") \
or et.contains("alert") or et.contains("threat"):
return COLOR_DANGER
return COLOR_NEUTRAL
@@ -16,6 +16,7 @@ extends Node2D
@onready var tile_renderer = $FogGroup/FloorTiles
@onready var fog_renderer = $FogOverlay
@onready var entity_renderer = $FogGroup/YSortGroup/Entities
@onready var sound_indicator_renderer = $SoundIndicators # #126 D-018 medium-range indicators
var _last_tick: int = -1
@@ -42,3 +43,7 @@ func update_from_state() -> void:
# Update entity sprites
if entity_renderer and entity_renderer.has_method("update_entities"):
entity_renderer.update_entities(GameState.visible_entities)
# D-018 #126: Update medium-range sound indicators
if sound_indicator_renderer and sound_indicator_renderer.has_method("update_sound_events"):
sound_indicator_renderer.update_sound_events(GameState.medium_sound_events)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+357
View File
@@ -0,0 +1,357 @@
## Test suite for #125: Close-range stereo audio — bus routing (D-068/D-069)
## Spec refs:
## D-068: 5-bus audio architecture (Music, Ambient, WorldSFX, PlayerActions, UISounds)
## D-069: Audio dip profiles — confrontation drops WorldSFX 46dB
## D-018: Three-range sound model — close-range → WorldSFX bus, 2D positional audio
##
## Test layers:
## 1. D-068 Bus architecture constants (no implementation required)
## 2. D-069 Dip profile spec values (no implementation required)
## 3. AudioManager API — dip state machine, signal emission
## 4. D-018 Snapshot integration stubs — graceful skip until #125 wires sound_events
class_name TestAudioBusRouting
extends GdUnitTestSuite
func before_test() -> void:
AudioManager.clear_dip()
for bus in AudioManager.BUSES:
AudioManager.set_volume(bus, 0.0)
GameState.stationary_ticks = 0
GameState._prev_player_position = Vector2(-1e9, -1e9)
func after_test() -> void:
AudioManager.clear_dip()
GameState.stationary_ticks = 0
GameState._prev_player_position = Vector2(-1e9, -1e9)
# ==============================================================================
# Layer 1: D-068 Bus Architecture Constants
# ==============================================================================
func test_d068_bus_world_sfx_name() -> void:
## D-068: WorldSFX bus name must match exactly — used by all sound event routing.
assert_that(AudioManager.BUS_WORLD_SFX).is_equal("WorldSFX")
func test_d068_bus_ambient_name() -> void:
assert_that(AudioManager.BUS_AMBIENT).is_equal("Ambient")
func test_d068_bus_player_actions_name() -> void:
assert_that(AudioManager.BUS_PLAYER_ACTIONS).is_equal("PlayerActions")
func test_d068_bus_ui_sounds_name() -> void:
assert_that(AudioManager.BUS_UI_SOUNDS).is_equal("UISounds")
func test_d068_bus_music_name() -> void:
assert_that(AudioManager.BUS_MUSIC).is_equal("Music")
func test_d068_five_buses_defined() -> void:
## D-068: Exactly 5 buses in the architecture.
assert_that(AudioManager.BUSES.size()).is_equal(5)
func test_d068_all_bus_names_present_in_buses_array() -> void:
## D-068: BUSES array must contain all 5 named buses.
assert_that(AudioManager.BUSES.has(AudioManager.BUS_MUSIC)).is_true()
assert_that(AudioManager.BUSES.has(AudioManager.BUS_AMBIENT)).is_true()
assert_that(AudioManager.BUSES.has(AudioManager.BUS_WORLD_SFX)).is_true()
assert_that(AudioManager.BUSES.has(AudioManager.BUS_PLAYER_ACTIONS)).is_true()
assert_that(AudioManager.BUSES.has(AudioManager.BUS_UI_SOUNDS)).is_true()
func test_d068_bus_names_are_unique() -> void:
## D-068: No two buses share a name.
var seen: Dictionary = {}
for bus_name in AudioManager.BUSES:
assert_that(seen.has(bus_name)).is_false()
seen[bus_name] = true
# ==============================================================================
# Layer 2: D-069 Dip Profile Spec Values
# Close-range sound events go on WorldSFX; confrontation dips that bus 46 dB.
# ==============================================================================
func test_d069_three_dip_profiles_exist() -> void:
## D-069: Three profiles — dialogue, confrontation, listening_focus.
assert_that(AudioManager.DIP_SPECS.has("dialogue")).is_true()
assert_that(AudioManager.DIP_SPECS.has("confrontation")).is_true()
assert_that(AudioManager.DIP_SPECS.has("listening_focus")).is_true()
func test_d069_confrontation_dips_world_sfx_4_to_6_db() -> void:
## D-069: Confrontation must drop WorldSFX by 46 dB. Sprint 12 uses -5.0 dB.
var buses: Dictionary = AudioManager.DIP_SPECS["confrontation"]["buses"]
assert_that(buses.has("WorldSFX")).is_true()
var dip_db: float = buses["WorldSFX"]
assert_that(dip_db >= -6.0 and dip_db <= -4.0).is_true()
func test_d069_confrontation_dips_ambient() -> void:
## D-069: Confrontation suppresses both Ambient and WorldSFX.
var buses: Dictionary = AudioManager.DIP_SPECS["confrontation"]["buses"]
assert_that(buses.has("Ambient")).is_true()
func test_d069_confrontation_has_low_pass_filter_at_800hz() -> void:
## D-069: Confrontation sweeps Ambient bus low-pass to ~800 Hz.
var spec: Dictionary = AudioManager.DIP_SPECS["confrontation"]
assert_that(spec.has("filter_hz")).is_true()
assert_that(spec["filter_hz"]).is_equal_approx(800.0, 50.0)
func test_d069_dialogue_dips_ambient_not_world_sfx() -> void:
## D-069: Dialogue dips Ambient only — world sounds are NOT suppressed.
## Confrontation is the one that mutes world SFX.
var buses: Dictionary = AudioManager.DIP_SPECS["dialogue"]["buses"]
assert_that(buses.has("Ambient")).is_true()
assert_that(buses.has("WorldSFX")).is_false()
func test_d069_listening_focus_boosts_world_sfx() -> void:
## D-069/D-071: Listening focus BOOSTS WorldSFX (positive offset) for eavesdropping.
var buses: Dictionary = AudioManager.DIP_SPECS["listening_focus"]["buses"]
assert_that(buses.has("WorldSFX")).is_true()
assert_that(buses["WorldSFX"] > 0.0).is_true()
func test_d069_all_profiles_have_positive_ease_times() -> void:
## D-069: All profiles have ease_in and ease_out > 0 (no instant transitions).
for profile in AudioManager.DIP_SPECS.keys():
var spec: Dictionary = AudioManager.DIP_SPECS[profile]
assert_that(spec.has("ease_in")).is_true()
assert_that(spec.has("ease_out")).is_true()
assert_that(spec["ease_in"] > 0.0).is_true()
assert_that(spec["ease_out"] > 0.0).is_true()
func test_d069_confrontation_ease_out_longer_than_dialogue() -> void:
## D-069: Confrontation exits slowly (1.0s) vs dialogue (0.5s) — more immersive.
var conf_out: float = AudioManager.DIP_SPECS["confrontation"]["ease_out"]
var dial_out: float = AudioManager.DIP_SPECS["dialogue"]["ease_out"]
assert_that(conf_out >= dial_out).is_true()
# ==============================================================================
# Layer 2b: D-067 Recognition Chime
# sfx_monologue_chime fires on first fog recognition, UISounds bus (not WorldSFX).
# ==============================================================================
func test_d067_chime_recognition_constant_exists() -> void:
## D-067: AudioManager must expose a CHIME_RECOGNITION constant.
assert_that("CHIME_RECOGNITION" in AudioManager).is_true()
func test_d067_chime_recognition_maps_to_sfx_monologue_chime() -> void:
## D-067: Recognition chime = sfx_monologue_chime (D-038 asset key).
## "Neural lattice firing" feel — soft crystalline tone.
assert_that(AudioManager.CHIME_RECOGNITION).is_equal("sfx_monologue_chime")
func test_d067_chime_on_ui_sounds_bus_not_world_sfx() -> void:
## D-067/D-038: Monologue chime is a UI sound, not a simulation sound.
## Must use BUS_UI_SOUNDS, not BUS_WORLD_SFX.
## Verify by checking that BUS_WORLD_SFX != BUS_UI_SOUNDS.
assert_that(AudioManager.BUS_WORLD_SFX != AudioManager.BUS_UI_SOUNDS).is_true()
## CHIME_RECOGNITION is played via AudioManager.play() which defaults to BUS_UI_SOUNDS.
## No further assertion needed — play() default bus IS UISounds by design.
func test_d067_chime_noop_when_asset_absent() -> void:
## D-038 / D-067: When sfx_monologue_chime.ogg is not in registry,
## play(CHIME_RECOGNITION) must be a no-op (no crash).
AudioManager.play(AudioManager.CHIME_RECOGNITION)
## No assertion — absence of crash is the test.
# ==============================================================================
# Layer 3: AudioManager API — Dip State Machine
# ==============================================================================
func test_audio_manager_initial_state_no_active_dip() -> void:
assert_that(AudioManager.get_active_dip()).is_equal("")
func test_audio_manager_apply_dip_sets_active() -> void:
AudioManager.apply_dip("dialogue")
assert_that(AudioManager.get_active_dip()).is_equal("dialogue")
func test_audio_manager_clear_dip_resets_active() -> void:
AudioManager.apply_dip("confrontation")
AudioManager.clear_dip()
assert_that(AudioManager.get_active_dip()).is_equal("")
func test_audio_manager_apply_unknown_dip_leaves_state_unchanged() -> void:
## Unknown profile: push_warning + early return, active dip stays empty.
AudioManager.apply_dip("not_a_real_profile")
assert_that(AudioManager.get_active_dip()).is_equal("")
func test_audio_manager_apply_dip_emits_dip_changed_signal() -> void:
## apply_dip() must emit dip_changed(profile) synchronously.
## Array wrapper used for lambda capture — GDScript 4 captures String locals by value,
## so a mutable reference type is required to observe signal argument inside the closure.
var received := [""]
var conn := func(p: String) -> void: received[0] = p
AudioManager.dip_changed.connect(conn)
AudioManager.apply_dip("dialogue")
AudioManager.dip_changed.disconnect(conn)
assert_that(received[0]).is_equal("dialogue")
func test_audio_manager_clear_dip_emits_dip_changed_empty() -> void:
## clear_dip() must emit dip_changed("") to signal audio restored.
## Array wrapper used for lambda capture — same reason as apply_dip signal test above.
AudioManager.apply_dip("dialogue")
var received := ["sentinel"]
var conn := func(p: String) -> void: received[0] = p
AudioManager.dip_changed.connect(conn)
AudioManager.clear_dip()
AudioManager.dip_changed.disconnect(conn)
assert_that(received[0]).is_equal("")
func test_audio_manager_apply_dip_interrupts_previous() -> void:
## Switching profiles mid-dip: active profile must update to the new one.
AudioManager.apply_dip("dialogue")
AudioManager.apply_dip("confrontation")
assert_that(AudioManager.get_active_dip()).is_equal("confrontation")
func test_audio_manager_dip_changed_fires_on_profile_switch() -> void:
## Switching from dialogue to confrontation emits dip_changed("confrontation").
## Array wrapper used for lambda capture — same reason as apply_dip signal test above.
AudioManager.apply_dip("dialogue")
var received := [""]
var conn := func(p: String) -> void: received[0] = p
AudioManager.dip_changed.connect(conn)
AudioManager.apply_dip("confrontation")
AudioManager.dip_changed.disconnect(conn)
assert_that(received[0]).is_equal("confrontation")
func test_audio_manager_has_asset_false_for_unknown_key() -> void:
## has_asset() must return false for a key that was never registered.
## Ensures no-op fallback path (D-038) is correctly guarded.
assert_that(AudioManager.has_asset("nonexistent_test_asset_xyz")).is_false()
# ==============================================================================
# Layer 4: D-018 Snapshot Integration — #125 implemented
# GameState partitions sound_events by range_category into close_sound_events
# and medium_sound_events. Close events are played via AudioManager.play_sound_event().
# ==============================================================================
func test_snapshot_close_events_stored_in_close_sound_events() -> void:
## #125: Close-range events must land in GameState.close_sound_events.
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 5.0, "y": 5.0, "event_type": "Footstep", "range_category": "Close"},
]
})
assert_that(GameState.close_sound_events.size()).is_equal(1)
assert_that(GameState.close_sound_events[0].event_type).is_equal("Footstep")
func test_snapshot_medium_events_stored_in_medium_sound_events() -> void:
## #126/#125: Medium-range events land in medium_sound_events, not close.
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 20.0, "y": 20.0, "event_type": "Footstep", "range_category": "Medium"},
]
})
assert_that(GameState.medium_sound_events.size()).is_equal(1)
assert_that(GameState.close_sound_events.size()).is_equal(0)
func test_snapshot_partitions_close_and_medium_events() -> void:
## D-018: Mixed batch — Close and Medium events partitioned correctly.
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 3.0, "y": 3.0, "event_type": "Footstep", "range_category": "Close"},
{"x": 15.0, "y": 15.0, "event_type": "Voice", "range_category": "Medium"},
{"x": 50.0, "y": 50.0, "event_type": "Footstep", "range_category": "Long"},
]
})
assert_that(GameState.close_sound_events.size()).is_equal(1)
assert_that(GameState.medium_sound_events.size()).is_equal(1)
func test_snapshot_empty_sound_events_clears_both_arrays() -> void:
## When snapshot has no sound_events, both close and medium arrays are cleared.
GameState.apply_snapshot({
"tick": 1,
"sound_events": [{"x": 1.0, "y": 1.0, "event_type": "Footstep", "range_category": "Close"}]
})
assert_that(GameState.close_sound_events.size()).is_equal(1)
GameState.apply_snapshot({"tick": 2})
assert_that(GameState.close_sound_events.size()).is_equal(0)
assert_that(GameState.medium_sound_events.size()).is_equal(0)
# ==============================================================================
# Layer 5: AudioManager.play_sound_event — asset registry and routing
# ==============================================================================
func test_audio_manager_sound_event_assets_registry_exists() -> void:
## #125: SOUND_EVENT_ASSETS must be a non-empty dictionary.
assert_that(AudioManager.SOUND_EVENT_ASSETS is Dictionary).is_true()
assert_that(AudioManager.SOUND_EVENT_ASSETS.size()).is_greater(0)
func test_audio_manager_footstep_maps_to_sfx_footstep_metal() -> void:
## #125: "Footstep" event type → sfx_footstep_metal_walk (D-038 asset).
assert_that(AudioManager.SOUND_EVENT_ASSETS.has("Footstep")).is_true()
assert_that(AudioManager.SOUND_EVENT_ASSETS["Footstep"]).is_equal("sfx_footstep_metal_walk")
func test_audio_manager_footstep_sprint_maps_to_sfx_footstep_metal_run() -> void:
## #125: "FootstepSprint" → sfx_footstep_metal_run (D-038 faster footstep).
assert_that(AudioManager.SOUND_EVENT_ASSETS.has("FootstepSprint")).is_true()
assert_that(AudioManager.SOUND_EVENT_ASSETS["FootstepSprint"]).is_equal("sfx_footstep_metal_run")
func test_audio_manager_play_sound_event_noop_for_unknown_type() -> void:
## #125 / D-038: Unknown event types must be silently skipped (no-op).
## play_sound_event should not crash and should not emit sound.
AudioManager.play_sound_event("UnknownEventXYZ", Vector2(5.0, 5.0))
# No assertion needed — absence of crash is the test.
func test_audio_manager_play_sound_event_noop_for_empty_type() -> void:
## #125: Empty event type string must be a no-op.
AudioManager.play_sound_event("", Vector2(5.0, 5.0))
# No assertion needed — absence of crash is the test.
# ==============================================================================
# Layer 6: D-071 (#530) — Stationary tick tracking for ListeningFocus
# ==============================================================================
func test_d071_stationary_ticks_increments_when_position_unchanged() -> void:
## D-071: stationary_ticks must increment on each snapshot where player doesn't move.
var player_entity := {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": {}}}
GameState.apply_snapshot({"tick": 1, "entities": [player_entity]})
GameState.apply_snapshot({"tick": 2, "entities": [player_entity]})
GameState.apply_snapshot({"tick": 3, "entities": [player_entity]})
assert_that(GameState.stationary_ticks).is_equal(2) # 2 ticks of no movement (tick 2 and 3)
func test_d071_stationary_ticks_resets_on_movement() -> void:
## D-071: stationary_ticks must reset to 0 when the player position changes.
var pos_a := {"entity_id": 1, "x": 10.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": {}}}
var pos_b := {"entity_id": 1, "x": 11.0, "y": 10.0, "z": 0, "kind": {"variant": "Player", "data": {}}}
GameState.apply_snapshot({"tick": 1, "entities": [pos_a]})
GameState.apply_snapshot({"tick": 2, "entities": [pos_a]})
assert_that(GameState.stationary_ticks).is_equal(1)
GameState.apply_snapshot({"tick": 3, "entities": [pos_b]})
assert_that(GameState.stationary_ticks).is_equal(0)
func test_d071_stationary_ticks_reaches_threshold() -> void:
## D-071: stationary_ticks must be able to reach 30+ for ListeningFocus activation.
var player_entity := {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": {}}}
for tick in range(31):
GameState.apply_snapshot({"tick": tick, "entities": [player_entity]})
assert_that(GameState.stationary_ticks >= 30).is_true()
# ==============================================================================
# Layer 7: D-069 (#530) — Dialogue dip profile interaction
# ==============================================================================
func test_d069_dialogue_dip_overridden_by_confrontation() -> void:
## D-069: Confrontation dip must override dialogue dip (apply_dip interrupts).
AudioManager.apply_dip("dialogue")
assert_that(AudioManager.get_active_dip()).is_equal("dialogue")
AudioManager.apply_dip("confrontation")
assert_that(AudioManager.get_active_dip()).is_equal("confrontation")
func test_d069_listening_focus_cleared_by_dialogue() -> void:
## D-069: Dialogue dip must override listening_focus (higher priority focus state).
AudioManager.apply_dip("listening_focus")
assert_that(AudioManager.get_active_dip()).is_equal("listening_focus")
AudioManager.apply_dip("dialogue")
assert_that(AudioManager.get_active_dip()).is_equal("dialogue")
func test_d069_clear_dip_noop_when_empty() -> void:
## D-069: clear_dip() when no dip active must be a safe no-op.
AudioManager.clear_dip() # Should not crash
assert_that(AudioManager.get_active_dip()).is_equal("")
+634
View File
@@ -0,0 +1,634 @@
## Test suite for Sprint 13 audio tickets (D-067, D-068, D-069, D-071, D-072, D-073).
##
## Spec refs:
## D-067: Recognition chime fires at ONSET of cognitive delay, not at completion.
## D-068: 5-bus audio architecture (Music, Ambient, WorldSFX, PlayerActions, UISounds).
## D-069: Audio dip profiles — timing values, dB offsets, interruptibility.
## D-071: ListeningFocus boost (30+ tick gate, caller's responsibility).
## D-072: Universal NPC conversation murmur on WorldSFX — no zone-specific variants.
## D-073: Zone crossfade — hard boundary trigger, 1.5-2s tween, interruptible.
##
## Tickets covered:
## #529 — Zone crossfade implementation (set_zone body + tween timing)
## #530 — Dip profile call sites (dialogue/confrontation/ListeningFocus wiring)
## #531 — Recognition chime fires at cognitive delay onset
## #533 — NPC conversation murmur wired to WorldSFX bus
##
## Test layers:
## 1. Zone crossfade spec and API (D-073 / #529)
## 2. D-069 dip timing and dB spec values (supplementing test_audio_bus_routing)
## 3. Volume slider proportional dip (D-068 / D-069)
## 4. Dip call site wiring via GameState snapshot (D-069 / D-070 / #530)
## 5. Recognition chime onset verification (D-067 / #531)
## 6. NPC murmur routing (D-072 / #533)
## 7. AudioManager no-op fallback sanity (D-068 / D-038)
class_name TestAudioSprint13
extends GdUnitTestSuite
func before_test() -> void:
AudioManager.clear_dip()
AudioManager.stop_all_loops()
AudioManager.set_volume(AudioManager.BUS_AMBIENT, 0.0)
AudioManager.set_volume(AudioManager.BUS_WORLD_SFX, 0.0)
AudioManager.set_volume(AudioManager.BUS_UI_SOUNDS, 0.0)
AudioManager.set_volume(AudioManager.BUS_PLAYER_ACTIONS, 0.0)
AudioManager.set_volume(AudioManager.BUS_MUSIC, 0.0)
func after_test() -> void:
AudioManager.clear_dip()
AudioManager.stop_all_loops()
# ==============================================================================
# Layer 1: Zone Crossfade — D-073 / #529
# set_zone() stub exists now; full implementation lands in #529.
# ==============================================================================
func test_d073_set_zone_method_exists() -> void:
## D-073 / #529: AudioManager must expose set_zone(zone_id: String).
assert_that(AudioManager.has_method("set_zone")).is_true()
func test_d073_set_zone_hub_does_not_crash() -> void:
## D-073: set_zone("hub") is a safe call. No crash even before #529 implementation.
AudioManager.set_zone("hub")
func test_d073_set_zone_bar_does_not_crash() -> void:
## D-073: set_zone("bar") is a safe call.
AudioManager.set_zone("bar")
func test_d073_set_zone_corridor_does_not_crash() -> void:
## D-073: set_zone("corridor") is a safe call.
AudioManager.set_zone("corridor")
func test_d073_set_zone_empty_string_does_not_crash() -> void:
## D-073: set_zone("") edge case — no zone ID. Must not crash.
AudioManager.set_zone("")
func test_d073_set_zone_unknown_zone_does_not_crash() -> void:
## D-073: Unmapped zone ID (no matching asset) — no crash, graceful no-op.
AudioManager.set_zone("nonexistent_zone_xyz")
func test_d073_zone_asset_hub_key_matches_filename_convention() -> void:
## D-073 / #529: v0.1 zone-to-asset mapping per sprint brief.
## hub/workplace → amb_hub_layer (must match filename stem in res://audio/).
## Test verifies naming convention is documentable. Activates once #529 adds the map.
if not "ZONE_ASSETS" in AudioManager:
push_warning("TestAudioSprint13: ZONE_ASSETS not yet defined (#529 pending) — skip zone map test")
return
var zone_assets: Dictionary = AudioManager.ZONE_ASSETS
assert_that(zone_assets.has("hub")).is_true()
assert_that(zone_assets["hub"]).is_equal("amb_hub_layer")
func test_d073_zone_asset_bar_key_matches_filename_convention() -> void:
## D-073 / #529: bar → amb_bar_layer
if not "ZONE_ASSETS" in AudioManager:
push_warning("TestAudioSprint13: ZONE_ASSETS not yet defined (#529 pending) — skip zone map test")
return
var zone_assets: Dictionary = AudioManager.ZONE_ASSETS
assert_that(zone_assets.has("bar")).is_true()
assert_that(zone_assets["bar"]).is_equal("amb_bar_layer")
func test_d073_zone_asset_corridor_key_matches_filename_convention() -> void:
## D-073 / #529: smuggling corridor → amb_corridor_layer
if not "ZONE_ASSETS" in AudioManager:
push_warning("TestAudioSprint13: ZONE_ASSETS not yet defined (#529 pending) — skip zone map test")
return
var zone_assets: Dictionary = AudioManager.ZONE_ASSETS
assert_that(zone_assets.has("corridor")).is_true()
assert_that(zone_assets["corridor"]).is_equal("amb_corridor_layer")
func test_d073_game_state_extracts_zone_id_from_player_tile() -> void:
## D-073 / Tyre review: zone_id is extracted in GameState.apply_snapshot() as
## a first-class field (like player_facing, player_stance), avoiding O(N) tile
## scan in main.gd per D-020 server-authoritative state.
var player := {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": {}}}
GameState.apply_snapshot({
"tick": 1,
"entities": [player],
"tiles": [
{"x": 5, "y": 5, "z": 0, "type": "floor", "zone_id": "bar"},
{"x": 6, "y": 5, "z": 0, "type": "floor", "zone_id": "hub"},
],
})
assert_that(GameState.current_zone_id).is_equal("bar")
GameState.apply_snapshot({"tick": 2})
func test_d073_game_state_zone_id_empty_when_field_absent() -> void:
## D-073: Defensive — zone_id missing from tile data (server hasn't shipped OQ-09).
var player := {"entity_id": 1, "x": 5.0, "y": 5.0, "z": 0, "kind": {"variant": "Player", "data": {}}}
GameState.apply_snapshot({
"tick": 1,
"entities": [player],
"tiles": [{"x": 5, "y": 5, "z": 0, "type": "floor"}],
})
assert_that(GameState.current_zone_id).is_equal("")
GameState.apply_snapshot({"tick": 2})
func test_d073_crossfade_duration_in_1_5_to_2_0s_range() -> void:
## D-073: Crossfade tween duration must be 1.5-2.0s.
## Activates once #529 defines the duration constant.
if not "CROSSFADE_DURATION" in AudioManager:
push_warning("TestAudioSprint13: CROSSFADE_DURATION not yet defined (#529 pending) — skip duration test")
return
var duration: float = AudioManager.CROSSFADE_DURATION
assert_that(duration >= 1.5 and duration <= 2.0).is_true()
func test_d073_set_zone_same_zone_repeated_is_noop() -> void:
## D-073: Crossing back to the current zone should not restart a crossfade.
## (No audio pops when zone boundary is ambiguous.) Activates post-#529.
if not "ZONE_ASSETS" in AudioManager:
push_warning("TestAudioSprint13: set_zone body not yet implemented (#529) — skip no-op test")
return
AudioManager.set_zone("hub")
AudioManager.set_zone("hub")
## Expect exactly one or zero ambient players after same-zone calls (no stacked tweens).
## Stub: assert no crash and ambient_players size is 0 or 1, not 2.
assert_that(AudioManager._ambient_players.size() <= 1).is_true()
func test_d073_rapid_zone_crossing_interruptible() -> void:
## D-073: Rapid back-and-forth zone crossing (interruptible crossfade).
## When _kill_zone_tweens() fires mid-fade, old player stays at intermediate
## volume — new tween starts from current position. No stacked tweens, no crash.
if not "ZONE_ASSETS" in AudioManager:
push_warning("TestAudioSprint13: ZONE_ASSETS not yet defined — skip rapid crossing test")
return
# Cross hub → bar → hub rapidly (simulates player walking back and forth)
AudioManager.set_zone("hub")
AudioManager.set_zone("bar") # Interrupts hub fade-in mid-tween
AudioManager.set_zone("hub") # Interrupts bar fade-in mid-tween
AudioManager.set_zone("corridor") # Interrupts hub fade-in mid-tween
## After rapid crossing: at most 2 ambient players (outgoing fade-out + incoming fade-in).
## No stacked tweens — _kill_zone_tweens clears previous tweens each time.
assert_that(AudioManager._ambient_players.size() <= 2).is_true()
## Zone tweens array should only contain active tweens from the last set_zone call.
assert_that(AudioManager._zone_tweens.size() <= 2).is_true()
AudioManager.stop_all_loops()
func test_d068_load_prefs_persistence_roundtrip() -> void:
## #528: Volume persistence — save/load roundtrip via ConfigFile.
## Sets non-default volumes, saves, reloads, and verifies values match.
AudioManager.set_volume(AudioManager.BUS_AMBIENT, -8.5)
AudioManager.set_volume(AudioManager.BUS_WORLD_SFX, -3.0)
AudioManager.set_volume(AudioManager.BUS_UI_SOUNDS, -12.0)
## _save_prefs() fires inside set_volume() — prefs file is written.
## Reload prefs by calling _load_prefs() directly.
AudioManager._load_prefs()
assert_that(AudioManager.get_volume(AudioManager.BUS_AMBIENT)).is_equal_approx(-8.5, 0.01)
assert_that(AudioManager.get_volume(AudioManager.BUS_WORLD_SFX)).is_equal_approx(-3.0, 0.01)
assert_that(AudioManager.get_volume(AudioManager.BUS_UI_SOUNDS)).is_equal_approx(-12.0, 0.01)
## Reset to defaults for subsequent tests.
for bus in AudioManager.BUSES:
AudioManager.set_volume(bus, 0.0)
func test_d068_load_prefs_handles_missing_config_gracefully() -> void:
## #528: _load_prefs() with a missing/corrupted config file must not crash.
## The config path is user://audio_prefs.cfg — if absent, _load_prefs returns early.
## This test documents the graceful fallback behavior.
AudioManager._load_prefs()
## No assertion — absence of crash is the test.
# ==============================================================================
# Layer 2: D-069 Dip Timing and dB Spec Values
# These tests supplement test_audio_bus_routing.gd with timing and dB precision.
# ==============================================================================
func test_d069_dialogue_ease_in_is_300ms() -> void:
## D-069: Dialogue dip ease-in = 300ms (0.3s).
var ease_in: float = AudioManager.DIP_SPECS["dialogue"]["ease_in"]
assert_that(ease_in).is_equal_approx(0.3, 0.05)
func test_d069_dialogue_ease_out_is_500ms() -> void:
## D-069: Dialogue dip ease-out = 500ms (0.5s).
var ease_out: float = AudioManager.DIP_SPECS["dialogue"]["ease_out"]
assert_that(ease_out).is_equal_approx(0.5, 0.05)
func test_d069_confrontation_ease_in_is_500ms() -> void:
## D-069: Confrontation dip ease-in = 500ms (0.5s).
var ease_in: float = AudioManager.DIP_SPECS["confrontation"]["ease_in"]
assert_that(ease_in).is_equal_approx(0.5, 0.05)
func test_d069_confrontation_ease_out_is_1000ms() -> void:
## D-069: Confrontation dip ease-out = 1000ms (1.0s) — longer exit for immersion.
var ease_out: float = AudioManager.DIP_SPECS["confrontation"]["ease_out"]
assert_that(ease_out).is_equal_approx(1.0, 0.05)
func test_d069_dialogue_ambient_dip_within_6_to_8_db() -> void:
## D-069: Dialogue dip: Ambient -6 to -8dB. Mid-range value (-7) used.
var dip: float = AudioManager.DIP_SPECS["dialogue"]["buses"]["Ambient"]
assert_that(dip >= -8.0 and dip <= -6.0).is_true()
func test_d069_confrontation_ambient_dip_within_10_to_12_db() -> void:
## D-069: Confrontation dip: Ambient -10 to -12dB. Mid-range value (-11) used.
var dip: float = AudioManager.DIP_SPECS["confrontation"]["buses"]["Ambient"]
assert_that(dip >= -12.0 and dip <= -10.0).is_true()
func test_d069_confrontation_world_sfx_dip_within_4_to_6_db() -> void:
## D-069: Confrontation dip: WorldSFX -4 to -6dB (graduated — loud events break through).
var dip: float = AudioManager.DIP_SPECS["confrontation"]["buses"]["WorldSFX"]
assert_that(dip >= -6.0 and dip <= -4.0).is_true()
func test_d069_listening_focus_world_sfx_boost_within_2_to_3_db() -> void:
## D-069 / D-071: ListeningFocus boosts WorldSFX +2 to +3dB (eavesdrop bonus).
var boost: float = AudioManager.DIP_SPECS["listening_focus"]["buses"]["WorldSFX"]
assert_that(boost >= 2.0 and boost <= 3.0).is_true()
func test_d069_dialogue_spec_only_affects_ambient_bus() -> void:
## D-069: Dialogue dip touches ONLY Ambient. WorldSFX, PlayerActions, UISounds, Music
## must NOT appear in the spec — world events remain audible during conversation.
var buses: Dictionary = AudioManager.DIP_SPECS["dialogue"]["buses"]
assert_that(buses.has("Ambient")).is_true()
assert_that(buses.has("WorldSFX")).is_false()
assert_that(buses.has("PlayerActions")).is_false()
assert_that(buses.has("UISounds")).is_false()
assert_that(buses.has("Music")).is_false()
func test_d069_confrontation_spec_does_not_affect_player_actions() -> void:
## D-069: Confrontation dip leaves PlayerActions at 0 — player sounds are NOT muffled.
var buses: Dictionary = AudioManager.DIP_SPECS["confrontation"]["buses"]
assert_that(buses.has("PlayerActions")).is_false()
func test_d069_confrontation_spec_does_not_affect_ui_sounds() -> void:
## D-069: Confrontation dip leaves UISounds at 0 — chimes and UI remain audible.
var buses: Dictionary = AudioManager.DIP_SPECS["confrontation"]["buses"]
assert_that(buses.has("UISounds")).is_false()
func test_d069_listening_focus_spec_does_not_affect_ambient() -> void:
## D-071: ListeningFocus boost is ONLY on WorldSFX. Ambient is NOT modified.
## D-071: Eavesdropping requires MORE ambient awareness, not less — no ambient dip.
var buses: Dictionary = AudioManager.DIP_SPECS["listening_focus"]["buses"]
assert_that(buses.has("Ambient")).is_false()
func test_d069_filter_cutoff_default_is_approx_20khz() -> void:
## D-069: Default low-pass filter cutoff is ~20kHz — effectively bypassed.
## Confrontation dip sweeps it down to 800Hz. Default must be >= 20000Hz.
assert_that(AudioManager.FILTER_CUTOFF_DEFAULT >= 20000.0).is_true()
func test_d069_confrontation_filter_hz_is_800hz() -> void:
## D-069: Confrontation sweeps low-pass filter to ~800Hz for muffled feel (D-070).
var filter_hz: float = AudioManager.DIP_SPECS["confrontation"]["filter_hz"]
assert_that(filter_hz).is_equal_approx(800.0, 50.0)
# ==============================================================================
# Layer 3: Volume Slider Proportional Dip — D-068 / D-069
# ==============================================================================
func test_d068_set_volume_get_volume_roundtrip() -> void:
## D-068: set_volume / get_volume roundtrip preserves the slider value.
AudioManager.set_volume(AudioManager.BUS_AMBIENT, -6.0)
assert_that(AudioManager.get_volume(AudioManager.BUS_AMBIENT)).is_equal_approx(-6.0, 0.01)
AudioManager.set_volume(AudioManager.BUS_AMBIENT, 0.0)
func test_d068_set_volume_persists_across_all_buses() -> void:
## D-068: Each of the 5 buses has an independent volume setting.
AudioManager.set_volume(AudioManager.BUS_MUSIC, -10.0)
AudioManager.set_volume(AudioManager.BUS_AMBIENT, -5.0)
AudioManager.set_volume(AudioManager.BUS_WORLD_SFX, -3.0)
AudioManager.set_volume(AudioManager.BUS_PLAYER_ACTIONS, -2.0)
AudioManager.set_volume(AudioManager.BUS_UI_SOUNDS, -1.0)
assert_that(AudioManager.get_volume(AudioManager.BUS_MUSIC)).is_equal_approx(-10.0, 0.01)
assert_that(AudioManager.get_volume(AudioManager.BUS_AMBIENT)).is_equal_approx(-5.0, 0.01)
assert_that(AudioManager.get_volume(AudioManager.BUS_WORLD_SFX)).is_equal_approx(-3.0, 0.01)
assert_that(AudioManager.get_volume(AudioManager.BUS_PLAYER_ACTIONS)).is_equal_approx(-2.0, 0.01)
assert_that(AudioManager.get_volume(AudioManager.BUS_UI_SOUNDS)).is_equal_approx(-1.0, 0.01)
func test_d069_get_volume_returns_slider_base_not_effective_volume() -> void:
## D-069: get_volume() always returns the player slider setting (base).
## The effective AudioServer volume during a dip = base + offset_db.
## Callers storing the slider value must always read get_volume(), not AudioServer directly.
AudioManager.set_volume(AudioManager.BUS_AMBIENT, 0.0)
AudioManager.apply_dip("dialogue")
## Even during dip, get_volume returns the base (not base + dip offset).
assert_that(AudioManager.get_volume(AudioManager.BUS_AMBIENT)).is_equal_approx(0.0, 0.01)
func test_d069_set_volume_during_active_dip_updates_base() -> void:
## D-069: Changing slider mid-dip must update the base so the proportional
## calculation uses the new slider value (not the pre-dip value).
AudioManager.set_volume(AudioManager.BUS_AMBIENT, 0.0)
AudioManager.apply_dip("dialogue")
AudioManager.set_volume(AudioManager.BUS_AMBIENT, -3.0)
assert_that(AudioManager.get_volume(AudioManager.BUS_AMBIENT)).is_equal_approx(-3.0, 0.01)
func test_d069_clear_dip_does_not_change_stored_slider_value() -> void:
## D-069: clear_dip() restores AudioServer volumes to base, but get_volume()
## must still reflect the player slider setting (not the dipped value).
AudioManager.set_volume(AudioManager.BUS_AMBIENT, -5.0)
AudioManager.apply_dip("dialogue")
AudioManager.clear_dip()
assert_that(AudioManager.get_volume(AudioManager.BUS_AMBIENT)).is_equal_approx(-5.0, 0.01)
# ==============================================================================
# Layer 4: Dip Call Site Wiring — D-069 / D-070 / D-071 / #530
#
# These tests verify GameState has the snapshot fields needed for #530 wiring.
# The assertions on AudioManager dip activation are stubbed pending implementation.
# ==============================================================================
func test_d069_game_state_current_dialogue_field_exists() -> void:
## #530 precondition: GameState.current_dialogue is the trigger for dialogue dip.
## Field must exist so #530 wiring can check it.
assert_that("current_dialogue" in GameState).is_true()
func test_d069_snapshot_with_dialogue_sets_current_dialogue() -> void:
## #530 precondition: apply_snapshot() with current_dialogue populates GameState correctly.
GameState.apply_snapshot({
"tick": 1,
"current_dialogue": {
"npc_name": "Kael", "npc_entity_id": 2,
"speech": "Haven't seen you around.", "options": [],
},
})
assert_that(GameState.current_dialogue != null).is_true()
assert_that(GameState.current_dialogue is Dictionary).is_true()
GameState.apply_snapshot({"tick": 2})
func test_d069_snapshot_without_dialogue_clears_current_dialogue() -> void:
## #530 precondition: Snapshot without current_dialogue → current_dialogue is null.
GameState.apply_snapshot({
"tick": 1,
"current_dialogue": {"npc_name": "Kael", "npc_entity_id": 2, "speech": "...", "options": []},
})
GameState.apply_snapshot({"tick": 2})
assert_that(GameState.current_dialogue == null).is_true()
func test_d069_dialogue_dip_wired_to_game_state_dialogue_activation() -> void:
## D-069 / #530: When current_dialogue becomes active, apply_dip("dialogue") fires.
## TODO(#530): Uncomment the assertion once call site is wired in sim_bridge/game_state.
AudioManager.clear_dip()
GameState.apply_snapshot({
"tick": 1,
"current_dialogue": {"npc_name": "Kael", "npc_entity_id": 2, "speech": "...", "options": []},
})
## Precondition: dialogue IS active in GameState.
assert_that(GameState.current_dialogue != null).is_true()
## ASSERTION (activate once #530 is implemented):
## assert_that(AudioManager.get_active_dip()).is_equal("dialogue")
GameState.apply_snapshot({"tick": 2})
AudioManager.clear_dip()
func test_d069_dialogue_dip_cleared_when_dialogue_ends() -> void:
## D-069 / #530: When current_dialogue returns to null, clear_dip() fires.
## TODO(#530): Uncomment the assertion once call site is wired.
GameState.apply_snapshot({
"tick": 1,
"current_dialogue": {"npc_name": "Kael", "npc_entity_id": 2, "speech": "...", "options": []},
})
GameState.apply_snapshot({"tick": 2}) # dialogue ends
## ASSERTION (activate once #530 is implemented):
## assert_that(AudioManager.get_active_dip()).is_equal("")
assert_that(GameState.current_dialogue == null).is_true()
func test_d071_listening_focus_gate_is_30_ticks() -> void:
## D-069 / D-071: ListeningFocus boost activates after 30+ stationary ticks.
## The tick gate is the CALLER's responsibility per AudioManager comment.
## This test documents the threshold so it doesn't silently drift.
## Wiring via sim_bridge.gd tracking stationary_ticks lands in #530.
const LISTENING_FOCUS_TICK_GATE := 30
## Stub: verify the spec value is documented.
assert_that(LISTENING_FOCUS_TICK_GATE).is_equal(30)
func test_d070_no_ui_indicator_means_no_signal_named_confrontation_ui() -> void:
## D-070: Confrontation muffling is felt, not announced. No UI indicator.
## Verify AudioManager does not expose a confrontation_ui_shown signal.
var signals: Array = AudioManager.get_signal_list().map(
func(s: Dictionary) -> String: return s.name
)
assert_that(signals.has("confrontation_ui_shown")).is_false()
assert_that(signals.has("listening_focus_shown")).is_false()
# ==============================================================================
# Layer 5: Recognition Chime Onset — D-067 / #531
#
# Chime fires at ONSET of cognitive delay (when entity FIRST appears in
# pending_recognitions), NOT at completion (when it leaves).
# ==============================================================================
func test_d067_chime_recognition_constant_defined() -> void:
## D-067 / D-038: AudioManager must expose CHIME_RECOGNITION asset key constant.
assert_that("CHIME_RECOGNITION" in AudioManager).is_true()
func test_d067_chime_recognition_matches_d038_asset_key() -> void:
## D-067 / D-038: sfx_monologue_chime = "neural lattice firing" feel.
## Key must match filename stem in res://audio/.
assert_that(AudioManager.CHIME_RECOGNITION).is_equal("sfx_monologue_chime")
func test_d067_play_routes_to_ui_sounds_bus_by_default() -> void:
## D-067: play(CHIME_RECOGNITION) routes to BUS_UI_SOUNDS by default.
## Chime is a cognitive/UI signal, not a world sound — must NOT go on WorldSFX.
## Verify play() default bus is UISounds (the chime caller uses the default).
## Edge: BUS_WORLD_SFX and BUS_UI_SOUNDS must be distinct.
assert_that(AudioManager.BUS_WORLD_SFX).is_not_equal(AudioManager.BUS_UI_SOUNDS)
func test_d067_play_chime_recognition_noop_when_asset_absent() -> void:
## D-067 / D-038: play(CHIME_RECOGNITION) is a silent no-op if asset file is absent.
## Client must not crash when audio branch has not yet provided the .ogg file.
AudioManager.play(AudioManager.CHIME_RECOGNITION)
## No assertion needed — absence of crash is the test.
func test_d067_onset_is_when_remaining_equals_total_delay_ticks() -> void:
## D-067: "Onset" of cognitive delay = first frame an entity appears in
## pending_recognitions, at remaining_ticks == total_delay_ticks.
## This is the moment the chime must fire.
GameState.apply_snapshot({
"tick": 1,
"pending_recognitions": [
{"entity_id": 99, "x": 10.0, "y": 10.0, "z": 0,
"remaining_ticks": 6, "total_delay_ticks": 6},
],
})
assert_that(GameState.pending_recognitions.size()).is_equal(1)
var rec: Dictionary = GameState.pending_recognitions[0]
## Onset condition: remaining == total (delay just started)
assert_that(rec.remaining_ticks).is_equal(rec.total_delay_ticks)
GameState.apply_snapshot({"tick": 2})
func test_d067_completion_is_when_entity_absent_from_pending() -> void:
## D-067: Recognition COMPLETES (blob transitions) when entity leaves pending_recognitions.
## The chime must NOT fire at this point — it fired at onset.
GameState.apply_snapshot({
"tick": 1,
"pending_recognitions": [
{"entity_id": 99, "x": 10.0, "y": 10.0, "z": 0,
"remaining_ticks": 1, "total_delay_ticks": 6},
],
})
assert_that(GameState.pending_recognitions.size()).is_equal(1)
## Completion: entity removed from array
GameState.apply_snapshot({"tick": 2, "pending_recognitions": []})
assert_that(GameState.pending_recognitions.size()).is_equal(0)
## Chime state: AudioManager must not have a dip triggered by recognition.
## (Chime is a play() call, not a dip — this verifies no side effects on dip state.)
assert_that(AudioManager.get_active_dip()).is_equal("")
func test_d067_chime_fires_at_fog_entity_spawn_not_removal() -> void:
## D-067 / #531: The chime call site in fog_entities.gd / entity_renderer.gd
## must be inside the "new entity" branch (not entity.has(eid)), NOT the cleanup loop.
## This test verifies FogEntities correctly identifies the onset condition.
## An entity with remaining_ticks == total_delay_ticks is a NEW entity entering delay.
var onset_tick := {"entity_id": 5, "x": 5.0, "y": 5.0, "z": 0,
"remaining_ticks": 6, "total_delay_ticks": 6}
var mid_tick := {"entity_id": 5, "x": 5.0, "y": 5.0, "z": 0,
"remaining_ticks": 3, "total_delay_ticks": 6}
## Frame 1: entity APPEARS (onset — chime should fire here)
GameState.apply_snapshot({"tick": 1, "pending_recognitions": [onset_tick]})
assert_that(GameState.pending_recognitions[0].remaining_ticks
== GameState.pending_recognitions[0].total_delay_ticks).is_true()
## Frame 2: entity mid-progress (chime must NOT re-fire)
GameState.apply_snapshot({"tick": 2, "pending_recognitions": [mid_tick]})
assert_that(GameState.pending_recognitions[0].remaining_ticks).is_equal(3)
## Frame 3: entity completes (chime must NOT fire)
GameState.apply_snapshot({"tick": 3, "pending_recognitions": []})
assert_that(GameState.pending_recognitions.size()).is_equal(0)
func test_d067_chime_duration_spec_is_300_to_400ms() -> void:
## D-067: Chime duration is 300-400ms per spec. The asset (.ogg) carries this duration.
## This test documents the spec range so asset authoring can be validated.
## When sfx_monologue_chime.ogg is present, its AudioStream.get_length() should
## return a value in this range.
const CHIME_MIN_DURATION := 0.3
const CHIME_MAX_DURATION := 0.4
if not AudioManager.has_asset(AudioManager.CHIME_RECOGNITION):
push_warning("TestAudioSprint13: sfx_monologue_chime asset absent — skip duration test")
return
var stream: AudioStream = AudioManager._registry.get(AudioManager.CHIME_RECOGNITION)
if stream == null:
push_warning("TestAudioSprint13: could not retrieve chime stream from registry")
return
assert_that(stream.get_length() >= CHIME_MIN_DURATION
and stream.get_length() <= CHIME_MAX_DURATION).is_true()
# ==============================================================================
# Layer 6: NPC Murmur Routing — D-072 / #533
# Single universal asset, WorldSFX bus, no zone-specific variants.
# ==============================================================================
func test_d072_world_sfx_bus_is_correct_for_murmur() -> void:
## D-072 / #533: NPC murmur routes to WorldSFX bus per D-068 architecture.
## BUS_WORLD_SFX must be "WorldSFX" — zone ambient conspicuousness is determined
## by that bus's noise floor relative to the murmur volume.
assert_that(AudioManager.BUS_WORLD_SFX).is_equal("WorldSFX")
func test_d072_play_at_method_accepts_bus_parameter() -> void:
## D-072 / #533: play_at() must accept an optional bus string parameter.
## Proximity murmur uses play_at(asset_key, world_pos, BUS_WORLD_SFX).
assert_that(AudioManager.has_method("play_at")).is_true()
func test_d072_play_at_noop_when_murmur_asset_absent() -> void:
## D-072 / D-038: sfx_npc_murmur.ogg arrives from audio branch (#532).
## Until then, play_at("sfx_npc_murmur", ...) must be a silent no-op.
AudioManager.play_at("sfx_npc_murmur", Vector2(100.0, 100.0), AudioManager.BUS_WORLD_SFX)
## No assertion — absence of crash is the test.
func test_d072_play_noop_when_murmur_asset_absent() -> void:
## D-072 / D-038: play("sfx_npc_murmur", BUS_WORLD_SFX) also no-ops gracefully.
AudioManager.play("sfx_npc_murmur", AudioManager.BUS_WORLD_SFX)
## No assertion — absence of crash is the test.
func test_d072_no_zone_specific_murmur_variants() -> void:
## D-072: SINGLE universal murmur asset — no zone-specific variants.
## "One murmur asset + zone-dependent conspicuousness creates the signal/noise
## dynamic naturally." Zone-specific variants MUST NOT exist in the registry.
assert_that(AudioManager.has_asset("sfx_npc_murmur_bar")).is_false()
assert_that(AudioManager.has_asset("sfx_npc_murmur_corridor")).is_false()
assert_that(AudioManager.has_asset("sfx_npc_murmur_hub")).is_false()
assert_that(AudioManager.has_asset("sfx_npc_murmur_workplace")).is_false()
func test_d072_murmur_does_not_use_ambient_bus() -> void:
## D-072: Bar ambient murmur is baked into amb_bar_layer (continuous background).
## The NPC proximity murmur is a SEPARATE event-driven asset on WorldSFX, not Ambient.
## Verify bus constant distinction.
assert_that(AudioManager.BUS_AMBIENT).is_not_equal(AudioManager.BUS_WORLD_SFX)
# ==============================================================================
# Layer 7: AudioManager No-Op Fallback Sanity — D-068 / D-038
# ==============================================================================
func test_d068_registry_size_is_non_negative() -> void:
## D-068: Registry is empty when res://audio/ is absent, non-negative always.
assert_that(AudioManager.get_registry_size() >= 0).is_true()
func test_d068_play_loop_returns_null_for_missing_asset() -> void:
## D-068 / D-038: play_loop() with unregistered asset key returns null (no crash).
var result: Variant = AudioManager.play_loop("nonexistent_ambient_xyzabc")
assert_that(result == null).is_true()
func test_d068_stop_loop_noop_for_unknown_key() -> void:
## D-068: stop_loop() on a key never started — no crash, no error.
AudioManager.stop_loop("nonexistent_key_xyzabc")
func test_d068_stop_all_loops_when_none_playing() -> void:
## D-068: stop_all_loops() with no active ambient players — no crash.
AudioManager.stop_all_loops()
func test_d068_has_asset_returns_false_for_unknown_key() -> void:
## D-068 / D-038: has_asset() guards all play methods. Verify false for unknown key.
assert_that(AudioManager.has_asset("totally_unknown_asset_key_abc123")).is_false()
func test_d068_play_noop_does_not_change_dip_state() -> void:
## D-068: play() on a missing asset must not modify the dip state machine.
## Verifies no-op fallback has zero side effects.
AudioManager.apply_dip("dialogue")
AudioManager.play("nonexistent_asset_xyzabc")
assert_that(AudioManager.get_active_dip()).is_equal("dialogue")
+199
View File
@@ -5,6 +5,7 @@ extends GdUnitTestSuite
var EntityRendererScript = load("res://scripts/rendering/entity_renderer.gd")
var TileRendererScript = load("res://scripts/rendering/tile_renderer.gd")
var SoundIndicatorScript = load("res://scripts/rendering/sound_indicator_renderer.gd")
# -- Test data matching Protocol decoded format --
@@ -230,6 +231,19 @@ func test_entity_renderer_skips_missing_entity_id() -> void:
assert_that(renderer.entity_nodes.size()).is_equal(0)
renderer.queue_free()
# Regression test for #345: protocol uses entity_id (not id).
# An entity dict keyed with "id" (old/wrong format) must be silently dropped.
func test_entity_renderer_rejects_old_id_field_format() -> void:
var renderer := _make_entity_renderer()
# Old (wrong) format: "id" instead of "entity_id"
renderer.update_entities([{"id": 99, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}}])
assert_that(renderer.entity_nodes.size()).is_equal(0)
# Correct format: "entity_id" — should be accepted
renderer.update_entities([{"entity_id": 99, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}}])
assert_that(renderer.entity_nodes.size()).is_equal(1)
assert_that(renderer.entity_nodes.has(99)).is_true()
renderer.queue_free()
func test_entity_renderer_player_color_differs_from_npc() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities)
@@ -334,6 +348,191 @@ func test_entity_renderer_npc_has_no_facing_indicator() -> void:
renderer.queue_free()
# -- EntityRenderer: regression #345 (entity_id field name bug) --
# Bug: entity_renderer.gd checked entity_data.has("id") instead of
# entity_data.has("entity_id"), causing all entities to be silently dropped.
# Fix: use entity_id (Protocol v2 canonical field name).
func test_regression_345_entity_id_field_renders() -> void:
# Regression: entity with correct "entity_id" field MUST be rendered.
var renderer := _make_entity_renderer()
renderer.update_entities([
{"entity_id": 99, "x": 4.0, "y": 4.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
])
assert_that(renderer.entity_nodes.size()).is_equal(1)
assert_that(renderer.entity_nodes.has(99)).is_true()
renderer.queue_free()
func test_regression_345_old_id_field_not_rendered() -> void:
# Negative regression: entity using OLD field name "id" (not "entity_id")
# must be silently dropped. Pre-fix code accepted "id"; this verifies the fix.
var renderer := _make_entity_renderer()
renderer.update_entities([
{"id": 99, "x": 4.0, "y": 4.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
])
assert_that(renderer.entity_nodes.size()).is_equal(0)
renderer.queue_free()
func test_regression_345_mixed_batch_only_entity_id_renders() -> void:
# Mixed batch: one entity with correct "entity_id", one with old "id" only.
# Only the entity_id entity should appear — no cross-contamination.
var renderer := _make_entity_renderer()
renderer.update_entities([
{"entity_id": 1, "x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
{"id": 2, "x": 2.0, "y": 2.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
])
assert_that(renderer.entity_nodes.size()).is_equal(1)
assert_that(renderer.entity_nodes.has(1)).is_true()
assert_that(renderer.entity_nodes.has(2)).is_false()
renderer.queue_free()
func test_regression_345_entity_id_node_keyed_by_id_value() -> void:
# Regression: entity_nodes dict must be keyed by the entity_id VALUE,
# not by a string "entity_id" or by the old "id" value.
var renderer := _make_entity_renderer()
renderer.update_entities([
{"entity_id": 42, "x": 3.0, "y": 3.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
])
assert_that(renderer.entity_nodes.has(42)).is_true()
assert_that(renderer.entity_nodes.has("entity_id")).is_false()
assert_that(renderer.entity_nodes.has(0)).is_false()
renderer.queue_free()
func test_regression_345_entity_position_set_from_entity_id_entity() -> void:
# Regression: entity rendered via entity_id must have correct pixel position.
var renderer := _make_entity_renderer()
renderer.update_entities([
{"entity_id": 5, "x": 6.0, "y": 7.0, "z": 0, "kind": {"variant": "Npc", "data": null}},
])
var node = renderer.entity_nodes[5]
var offset: float = (Constants.TILE_SIZE - 24) / 2.0
assert_that(node.position.x).is_equal_approx(6.0 * Constants.TILE_SIZE + offset, 0.01)
assert_that(node.position.y).is_equal_approx(7.0 * Constants.TILE_SIZE + offset, 0.01)
renderer.queue_free()
# -- SoundIndicatorRenderer: #126 D-018 medium-range fog-edge indicators --
func _make_sound_indicator_renderer() -> Node2D:
var renderer = Node2D.new()
renderer.set_script(SoundIndicatorScript)
add_child(renderer)
return renderer
func test_sound_indicator_accepts_medium_events() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([
{"x": 10.0, "y": 10.0, "event_type": "Footstep"},
{"x": 15.0, "y": 15.0, "event_type": "Voice"},
])
assert_that(renderer._indicators.size()).is_equal(2)
renderer.queue_free()
func test_sound_indicator_drops_events_without_position() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([
{"event_type": "Footstep"}, # missing x, y
{"x": 5.0, "event_type": "Voice"}, # missing y
{"x": 8.0, "y": 3.0, "event_type": "Footstep"}, # valid
])
assert_that(renderer._indicators.size()).is_equal(1)
renderer.queue_free()
func test_sound_indicator_appends_new_events() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([
{"x": 1.0, "y": 1.0, "event_type": "Voice"},
{"x": 2.0, "y": 2.0, "event_type": "Footstep"},
])
assert_that(renderer._indicators.size()).is_equal(2)
# New update appends — existing indicators persist until they expire
renderer.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Footstep"}])
assert_that(renderer._indicators.size()).is_equal(3)
renderer.queue_free()
func test_sound_indicator_empty_update_preserves_existing() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([{"x": 3.0, "y": 3.0, "event_type": "Voice"}])
assert_that(renderer._indicators.size()).is_equal(1)
# Empty update does not clear existing indicators — they expire via _process
renderer.update_sound_events([])
assert_that(renderer._indicators.size()).is_equal(1)
renderer.queue_free()
func test_sound_indicator_color_voice() -> void:
var renderer := _make_sound_indicator_renderer()
assert_that(renderer.color_for_type("voice")).is_equal(SoundIndicatorRenderer.COLOR_VOICE)
assert_that(renderer.color_for_type("Voice")).is_equal(SoundIndicatorRenderer.COLOR_VOICE)
assert_that(renderer.color_for_type("speech")).is_equal(SoundIndicatorRenderer.COLOR_VOICE)
renderer.queue_free()
func test_sound_indicator_color_danger() -> void:
var renderer := _make_sound_indicator_renderer()
assert_that(renderer.color_for_type("Gunshot")).is_equal(SoundIndicatorRenderer.COLOR_DANGER)
assert_that(renderer.color_for_type("alert")).is_equal(SoundIndicatorRenderer.COLOR_DANGER)
assert_that(renderer.color_for_type("danger")).is_equal(SoundIndicatorRenderer.COLOR_DANGER)
renderer.queue_free()
func test_sound_indicator_color_neutral_for_unknown() -> void:
var renderer := _make_sound_indicator_renderer()
assert_that(renderer.color_for_type("Footstep")).is_equal(SoundIndicatorRenderer.COLOR_NEUTRAL)
assert_that(renderer.color_for_type("")).is_equal(SoundIndicatorRenderer.COLOR_NEUTRAL)
assert_that(renderer.color_for_type("Unknown")).is_equal(SoundIndicatorRenderer.COLOR_NEUTRAL)
renderer.queue_free()
func test_sound_indicator_deduplicates_same_position() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Voice"}])
assert_that(renderer._indicators.size()).is_equal(1)
# Same position again — should reset timer, not add a duplicate
renderer.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Voice"}])
assert_that(renderer._indicators.size()).is_equal(1)
renderer.queue_free()
func test_sound_indicator_elapsed_starts_at_zero() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([{"x": 10.0, "y": 5.0, "event_type": "Voice"}])
assert_that(renderer._indicators[0].elapsed).is_equal_approx(0.0, 0.001)
renderer.queue_free()
func test_sound_indicator_events_expire_after_lifetime() -> void:
var renderer := _make_sound_indicator_renderer()
renderer.update_sound_events([{"x": 10.0, "y": 5.0, "event_type": "Footstep"}])
# Manually age the indicator past its lifetime
renderer._indicators[0].elapsed = SoundIndicatorRenderer.INDICATOR_LIFETIME + 0.01
renderer._process(0.0) # zero delta so no additional aging
assert_that(renderer._indicators.size()).is_equal(0)
renderer.queue_free()
# -- GameState: medium_sound_events from snapshot --
func test_game_state_filters_medium_sound_events() -> void:
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 5.0, "y": 5.0, "event_type": "Footstep", "range_category": "Close"},
{"x": 8.0, "y": 8.0, "event_type": "Voice", "range_category": "Medium"},
{"x": 20.0, "y": 20.0, "event_type": "Footstep", "range_category": "Long"},
]
})
assert_that(GameState.medium_sound_events.size()).is_equal(1)
assert_that(GameState.medium_sound_events[0].event_type).is_equal("Voice")
func test_game_state_medium_sound_events_empty_when_no_field() -> void:
GameState.apply_snapshot({"tick": 1})
assert_that(GameState.medium_sound_events.size()).is_equal(0)
func test_game_state_medium_sound_events_cleared_between_ticks() -> void:
GameState.apply_snapshot({
"tick": 1,
"sound_events": [{"x": 5.0, "y": 5.0, "event_type": "Voice", "range_category": "Medium"}]
})
assert_that(GameState.medium_sound_events.size()).is_equal(1)
# Next tick without sound_events clears them
GameState.apply_snapshot({"tick": 2})
assert_that(GameState.medium_sound_events.size()).is_equal(0)
# -- TileRenderer: tile type constants --
func test_tile_type_map_covers_required_types() -> void:
+236
View File
@@ -0,0 +1,236 @@
## Test suite for #126: Medium-range visual indicators (D-018)
## Spec refs:
## D-018: Medium-range (outside LOS, nearby) → fog-edge directional indicators
## D-049: Z-stack — indicators must render above fog layer (z:900)
## D-069: Color palette for indicators (neutral/voice/danger)
##
## Color spec (sprint brief + D-018/D-069):
## Neutral: #c8d0e0 (matches INSERT_COLOR_TEXT — insert visual language)
## Voices: #e8c547 (matches ENTITY_COLOR_POI — amber, voice context)
## Danger: #d45d5d (matches ENTITY_COLOR_HOSTILE — muted red)
##
## Test layers:
## 1. D-018 Indicator color spec — verifies Constants match the spec
## 2. D-018 Range routing — Medium vs Close events belong to different systems
## 3. Direction geometry — angle from player position to sound source
## 4. Snapshot integration stubs — graceful skip until #126 lands
## 5. Indicator script API — graceful skip until Stig creates the script
class_name TestSoundIndicators
extends GdUnitTestSuite
# ==============================================================================
# Layer 1: D-018/D-069 Indicator Color Spec
# Sound indicators reuse existing Constants to avoid palette drift.
# ==============================================================================
func test_d018_indicator_color_neutral_is_insert_color() -> void:
## D-018: Neutral indicator = #c8d0e0 = INSERT_COLOR_TEXT.
## Indicators use the insert visual language (diegetic, not world-layer UI).
assert_that(Constants.INSERT_COLOR_TEXT).is_equal(Color("#c8d0e0"))
func test_d018_indicator_color_voices_is_poi_amber() -> void:
## D-018/D-069: Voice indicator = #e8c547 = ENTITY_COLOR_POI (amber).
assert_that(Constants.ENTITY_COLOR_POI).is_equal(Color("#e8c547"))
func test_d018_indicator_color_danger_is_hostile_red() -> void:
## D-018/D-069: Danger indicator = #d45d5d = ENTITY_COLOR_HOSTILE (muted red).
assert_that(Constants.ENTITY_COLOR_HOSTILE).is_equal(Color("#d45d5d"))
func test_d018_three_indicator_colors_are_distinct() -> void:
## D-018: All three indicator states must be visually distinct.
var neutral: Color = Constants.INSERT_COLOR_TEXT
var voice: Color = Constants.ENTITY_COLOR_POI
var danger: Color = Constants.ENTITY_COLOR_HOSTILE
assert_that(neutral != voice).is_true()
assert_that(neutral != danger).is_true()
assert_that(voice != danger).is_true()
func test_d049_fog_layer_z_index() -> void:
## D-049: Fog overlay is at z:900. Indicators must render above it (z > 900).
## This verifies the z constant — indicator placement depends on it.
assert_that(Constants.Z_FOG).is_equal(900)
# ==============================================================================
# Layer 2: D-018 Range Routing — Medium vs Close
# Close-range events → AudioManager.play_at() (spatial audio, no indicator)
# Medium-range events → fog-edge indicators (no direct audio)
# ==============================================================================
func test_d018_medium_range_events_stored_in_snapshot() -> void:
## GameState.medium_sound_events receives Medium-range events (#126 implemented).
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 20.0, "y": 20.0, "event_type": "Footstep", "range_category": "Medium"},
]
})
assert_that(GameState.medium_sound_events.size()).is_equal(1)
assert_that(GameState.medium_sound_events[0].event_type).is_equal("Footstep")
func test_d018_close_range_events_do_not_appear_in_medium() -> void:
## D-018: Close events must NOT appear in medium_sound_events.
## They go to close_sound_events for 2D positional audio.
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 3.0, "y": 3.0, "event_type": "Voice", "range_category": "Close"},
{"x": 15.0, "y": 15.0, "event_type": "Footstep", "range_category": "Medium"},
]
})
assert_that(GameState.medium_sound_events.size()).is_equal(1)
assert_that(GameState.close_sound_events.size()).is_equal(1)
func test_d018_medium_range_event_has_position_fields() -> void:
## Medium-range events must include source position for direction calculation.
GameState.apply_snapshot({
"tick": 1,
"sound_events": [
{"x": 12.0, "y": 8.0, "event_type": "Footstep", "range_category": "Medium"},
]
})
var ev: Dictionary = GameState.medium_sound_events[0]
assert_that(ev.has("x")).is_true()
assert_that(ev.has("y")).is_true()
assert_that(float(ev["x"])).is_equal_approx(12.0, 0.001)
assert_that(float(ev["y"])).is_equal_approx(8.0, 0.001)
# ==============================================================================
# Layer 3: Direction Geometry
# The fog-edge indicator must point toward the sound source from the player.
# These tests validate the math used by the indicator rendering — if the
# indicator uses Vector2.angle() or atan2, these confirm expected output.
# ==============================================================================
## Compute directional angle (radians) from player tile pos to source tile pos.
## 0 = East, -PI/2 = North, PI/2 = South, ±PI = West (Godot Y-down convention).
func _direction_angle(player: Vector2, source: Vector2) -> float:
return player.direction_to(source).angle()
func test_indicator_direction_north() -> void:
## Source directly north of player (lower y in Godot) → angle -PI/2.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(10.0, 0.0))
assert_that(angle).is_equal_approx(-PI / 2.0, 0.01)
func test_indicator_direction_east() -> void:
## Source directly east → angle 0.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(20.0, 10.0))
assert_that(angle).is_equal_approx(0.0, 0.01)
func test_indicator_direction_south() -> void:
## Source directly south (higher y) → angle PI/2.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(10.0, 20.0))
assert_that(angle).is_equal_approx(PI / 2.0, 0.01)
func test_indicator_direction_west() -> void:
## Source directly west → angle ≈ ±PI.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(0.0, 10.0))
assert_that(absf(angle)).is_equal_approx(PI, 0.01)
func test_indicator_direction_northeast() -> void:
## Source equal offset northeast → angle -PI/4.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(20.0, 0.0))
assert_that(angle).is_equal_approx(-PI / 4.0, 0.01)
func test_indicator_direction_southeast() -> void:
## Source equal offset southeast → angle PI/4.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(20.0, 20.0))
assert_that(angle).is_equal_approx(PI / 4.0, 0.01)
func test_indicator_direction_southwest() -> void:
## Source equal offset southwest → angle 3PI/4.
var angle := _direction_angle(Vector2(10.0, 10.0), Vector2(0.0, 20.0))
assert_that(angle).is_equal_approx(3.0 * PI / 4.0, 0.01)
func test_indicator_direction_changes_with_source_position() -> void:
## Sanity: direction is not degenerate — different source positions give different angles.
var north := _direction_angle(Vector2(10.0, 10.0), Vector2(10.0, 0.0))
var east := _direction_angle(Vector2(10.0, 10.0), Vector2(20.0, 10.0))
var south := _direction_angle(Vector2(10.0, 10.0), Vector2(10.0, 20.0))
assert_that(north != east).is_true()
assert_that(east != south).is_true()
assert_that(south != north).is_true()
func test_indicator_direction_magnitude_independent_of_distance() -> void:
## Direction angle must not vary with distance — only with angle from player.
## Source 5 tiles north vs 20 tiles north: same angle.
var near := _direction_angle(Vector2(10.0, 10.0), Vector2(10.0, 5.0))
var far := _direction_angle(Vector2(10.0, 10.0), Vector2(10.0, -10.0))
assert_that(near).is_equal_approx(far, 0.01)
# ==============================================================================
# Layer 4: Indicator Script API (graceful stub)
# Tests activate when Stig creates the indicator rendering script.
# Expected paths checked in order — first match wins.
# ==============================================================================
func _load_indicator_script() -> GDScript:
## Load the sound indicator renderer script (#126 implemented).
var path: String = "res://scripts/rendering/sound_indicator_renderer.gd"
if ResourceLoader.exists(path):
return load(path) as GDScript
return null
func test_indicator_script_exists() -> void:
## #126: sound_indicator_renderer.gd must exist at the canonical path.
var script: GDScript = _load_indicator_script()
assert_that(script != null).is_true()
func test_indicator_has_update_sound_events_method() -> void:
## #126: Indicator must expose update_sound_events(events: Array).
var script: GDScript = _load_indicator_script()
if script == null:
push_warning("TestSoundIndicators: sound_indicator_renderer.gd not found")
return
var method_names: Array = script.get_script_method_list().map(
func(m: Dictionary) -> String: return m.name
)
assert_that(method_names.has("update_sound_events")).is_true()
func test_indicator_does_not_crash_for_empty_events() -> void:
## Edge case: empty sound_events list must not crash update_sound_events.
var script: GDScript = _load_indicator_script()
if script == null:
push_warning("TestSoundIndicators: sound_indicator_renderer.gd not found")
return
var indicator := Node2D.new()
indicator.set_script(script)
add_child(indicator)
indicator.update_sound_events([])
# Empty update on fresh renderer — no indicators exist, no crash
assert_that(indicator._indicators.size()).is_equal(0)
indicator.queue_free()
func test_indicator_existing_survive_empty_update() -> void:
## Existing indicators persist through an empty update (expire via _process only).
var script: GDScript = _load_indicator_script()
if script == null:
push_warning("TestSoundIndicators: sound_indicator_renderer.gd not found")
return
var indicator := Node2D.new()
indicator.set_script(script)
add_child(indicator)
indicator.update_sound_events([{"x": 5.0, "y": 5.0, "event_type": "Voice"}])
assert_that(indicator._indicators.size()).is_equal(1)
indicator.update_sound_events([])
assert_that(indicator._indicators.size()).is_equal(1)
indicator.queue_free()
func test_indicator_stores_valid_medium_events() -> void:
## #126: update_sound_events must store events with x and y fields.
var script: GDScript = _load_indicator_script()
if script == null:
push_warning("TestSoundIndicators: sound_indicator_renderer.gd not found")
return
var indicator := Node2D.new()
indicator.set_script(script)
add_child(indicator)
indicator.update_sound_events([
{"x": 10.0, "y": 5.0, "event_type": "Footstep"},
{"x": 20.0, "y": 15.0, "event_type": "Voice"},
])
assert_that(indicator._indicators.size()).is_equal(2)
indicator.queue_free()
+11 -1
View File
@@ -28,7 +28,7 @@ const FADE_IN: float = 0.2
const FADE_OUT: float = 0.3 # D-064: 300ms fade on walk-away
const MAX_OPTIONS: int = 3 # D-061: max 3 response options visible
const MAX_HEIGHT_RATIO: float = 0.2 # D-061: max 20% viewport height
const MAX_WIDTH_PX: float = 832.0 # D-061: max-width cap (~65% of 1280)
const MAX_WIDTH_PX: float = Constants.DIALOGUE_MAX_WIDTH # D-076: 640px (OQ-29)
const CONFRONTATION_BEAT_DURATION: float = 1.5 # D-063: pause before sending
const CONFRONTATION_DIM_ALPHA: float = 0.7 # D-063: dialogue box dims during beat
const CONFRONTATION_MONOLOGUE_KEY: String = "dialogue.confrontation_beat"
@@ -139,6 +139,12 @@ func show_dialogue(npc_name: String, speech: String, options: Array = []) -> voi
_is_showing = true
GameState.dialogue_active = true # D-064: block movement while dialogue visible/fading
# D-069: Dialogue dip — reduce ambient noise to foreground conversation.
# Confrontation options REPLACE (not nest) this dip via apply_dip("confrontation")
# in _start_confrontation_beat(). hide_dialogue()'s clear_dip() restores to base
# volumes regardless of which profile was last active — intentional replacement semantics.
AudioManager.apply_dip("dialogue")
# D-061: auto-pause in single-player when dialogue opens
SimBridge.send_input({"action": InputMapper.Action.PAUSE, "timestamp_msec": Time.get_ticks_msec()})
@@ -156,6 +162,10 @@ func hide_dialogue() -> void:
_is_showing = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
# D-069: Clear dialogue/confrontation dip — restore bus volumes to player slider settings.
# Safe to call even if confrontation beat already cleared the dip (no-op when empty).
AudioManager.clear_dip()
# D-061: unpause when dialogue closes
SimBridge.send_input({"action": InputMapper.Action.UNPAUSE, "timestamp_msec": Time.get_ticks_msec()})
+2 -2
View File
@@ -20,9 +20,9 @@ anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -416.0
offset_left = -320.0
offset_top = -200.0
offset_right = 416.0
offset_right = 320.0
grow_horizontal = 2
grow_vertical = 0
+159
View File
@@ -0,0 +1,159 @@
extends Control
## #528: Audio settings dialog — 5-bus volume sliders.
## Opens on OPEN_MENU (ESC) from main.gd. Closes on OPEN_MENU again or CLOSE button.
## Volumes persist via AudioManager._save_prefs() on each slider change.
const BG_COLOR := Color(0.05, 0.05, 0.08, 0.90)
const BORDER_COLOR := Color("#4a9ebb")
const TEXT_COLOR := Color(0.878, 0.969, 0.98, 1)
const TITLE_COLOR := Color("#4a9ebb")
const FONT_SIZE := 14
const BOX_WIDTH := 460
const BOX_HEIGHT := 340
const PADDING := 20
const ROW_HEIGHT := 36
# Bus display labels → bus name strings (must match AudioManager BUS_* constants)
const BUS_ROWS: Array = [
["Music", "Music"],
["Ambient", "Ambient"],
["World SFX", "WorldSFX"],
["Player Actions", "PlayerActions"],
["UI Sounds", "UISounds"],
]
var _active: bool = false
var _container: VBoxContainer = null
signal closed
func _ready() -> void:
visible = false
mouse_filter = Control.MOUSE_FILTER_STOP
func open() -> void:
if _active:
return
_active = true
visible = true
_build_ui()
queue_redraw()
func close() -> void:
if not _active:
return
_active = false
visible = false
_destroy_ui()
queue_redraw()
closed.emit()
func is_open() -> bool:
return _active
func _build_ui() -> void:
var vp_size := get_viewport_rect().size
var box_pos := Vector2(
(vp_size.x - BOX_WIDTH) / 2.0,
(vp_size.y - BOX_HEIGHT) / 2.0
)
_container = VBoxContainer.new()
_container.position = box_pos + Vector2(PADDING, PADDING + 28)
_container.custom_minimum_size = Vector2(BOX_WIDTH - PADDING * 2, 0)
_container.add_theme_constant_override("separation", 4)
add_child(_container)
for row_data in BUS_ROWS:
var label_text: String = row_data[0]
var bus_name: String = row_data[1]
var hbox := HBoxContainer.new()
hbox.custom_minimum_size = Vector2(0, ROW_HEIGHT)
_container.add_child(hbox)
var label := Label.new()
label.text = label_text
label.custom_minimum_size = Vector2(150, 0)
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
label.add_theme_font_size_override("font_size", FONT_SIZE)
label.add_theme_color_override("font_color", TEXT_COLOR)
hbox.add_child(label)
var slider := HSlider.new()
slider.min_value = -40.0
slider.max_value = 0.0
slider.step = 0.5
slider.value = AudioManager.get_volume(bus_name)
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
hbox.add_child(slider)
var db_label := Label.new()
db_label.text = _format_db(slider.value)
db_label.custom_minimum_size = Vector2(70, 0)
db_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
db_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
db_label.add_theme_font_size_override("font_size", FONT_SIZE)
db_label.add_theme_color_override("font_color", TEXT_COLOR)
hbox.add_child(db_label)
slider.value_changed.connect(func(value: float) -> void:
AudioManager.set_volume(bus_name, value)
db_label.text = _format_db(value)
)
# Spacer
var spacer := Control.new()
spacer.custom_minimum_size = Vector2(0, 8)
_container.add_child(spacer)
# Close button
var close_btn := Button.new()
close_btn.text = "CLOSE"
close_btn.add_theme_font_size_override("font_size", FONT_SIZE)
close_btn.pressed.connect(close)
_container.add_child(close_btn)
func _destroy_ui() -> void:
if _container:
_container.queue_free()
_container = null
func _draw() -> void:
if not _active:
return
var viewport_size := get_viewport_rect().size
# Dim overlay
draw_rect(Rect2(Vector2.ZERO, viewport_size), BG_COLOR)
# Dialog box
var box_pos := Vector2(
(viewport_size.x - BOX_WIDTH) / 2.0,
(viewport_size.y - BOX_HEIGHT) / 2.0
)
var box_rect := Rect2(box_pos, Vector2(BOX_WIDTH, BOX_HEIGHT))
draw_rect(box_rect, Color(0.08, 0.08, 0.12, 0.95))
draw_rect(box_rect, BORDER_COLOR, false, 1.0)
# Title
var font := ThemeDB.fallback_font
draw_string(font,
box_pos + Vector2(PADDING, PADDING + 18),
"AUDIO SETTINGS",
HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE + 2, TITLE_COLOR)
static func _format_db(db: float) -> String:
if db <= -40.0:
return "mute"
return "%d dB" % int(db)
+14
View File
@@ -0,0 +1,14 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/settings_dialog.gd" id="1_settings"]
; #528: Audio settings dialog — 5-bus volume sliders, OPEN_MENU (ESC) to toggle
[node name="SettingsDialog" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_settings")
@@ -1,2 +1,379 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: ring-operative at Maintenance Corridors
# NPC: Generic ring operative (Tier 3 template — members without individual profiles)
# Voice: two registers. Surface: maintenance worker, brief, task-focused. Real: coded, precise, never explicit.
# The crucial thing: both registers sound identical to a casual listener.
# Ring-aware: full ring membership. Knows the route, the window, the players.
# Ring code vocabulary (from Sova texture appendix):
# "running maintenance" = cover for ring operation
# "the usual pickup" = ring cargo handoff
# "cans are ready" = containers cleared for movement
# "manifest is clean" = paperwork cover confirmed
# "Drin was asking" = warning, inspection proximity
# "dead air" = Meridian surveillance gaps
# "the gap" = maintenance corridors, the route itself
# Ticket: #192 | Sprint: 12
location: maintenance-corridors
role: ring-operative
lines:
# ==========================================
# SURFACE COVER — sounds like maintenance work
# ==========================================
- id: maintenance-corridors_d_100
text: "Running maintenance on the B-7 conduit. Could be a while."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, b7]
dual_lens:
smuggler: "Cover phrase. He's not doing maintenance."
detective: "Maintenance worker. Conduit repair. Routine."
notes: "'Running maintenance' is the canonical ring cover phrase."
- id: maintenance-corridors_d_101
text: "Bay four access from here is faster than going around. If you're in a hurry."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, bay-four, spatial]
dual_lens:
smuggler: "He's telling me the route is faster. This is spatial information, not just courtesy."
detective: "Ring operative casually establishes corridor-to-bay-four spatial relationship. Note it."
- id: maintenance-corridors_d_102
text: "Quiet in here tonight. Good working conditions."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, ambient]
dual_lens:
smuggler: "Dead air confirmed. Meridian's not picking this up."
detective: "Routine corridor comment."
- id: maintenance-corridors_d_103
text: "Pael was through here earlier. Seal replacement on the ventilation unit."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, colleague]
mood: [comfortable]
tags: [ring-operative, surface-cover, pael, corridor-traffic]
dual_lens:
smuggler: "Pael was through. Legitimate. Corridor's been active — that means traffic covers us."
detective: "References Pael's B-7 presence without suspicion. Maintenance workers know each other's routes."
- id: maintenance-corridors_d_104
text: "C-4 access is at the far end. About three minutes from here."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, c4, spatial]
dual_lens:
smuggler: "He's giving me the timing. Three minutes to C-4 from this point."
detective: "Operative establishes C-4 proximity to maintenance corridor. That's the link."
knowledge_grant:
fact_id: location.smuggling_route
confidence: suspects
# ==========================================
# OPERATIONAL COORDINATION — insider/real only
# ==========================================
- id: maintenance-corridors_d_105
text: "Cans are ready. Bay four, per the schedule."
role: ring-operative
access: [insider]
trust: real
situation: [routine, shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [ring-operative, ring-ops, coded, cans-ready]
dual_lens:
smuggler: "Containers cleared. Bay four. We're on."
detective: "Never hears this line."
- id: maintenance-corridors_d_106
text: "Manifest is clean. Window opens in eighteen minutes."
role: ring-operative
access: [insider]
trust: real
situation: [shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [ring-operative, ring-ops, coded, manifest-clean, window]
dual_lens:
smuggler: "Paperwork's set. Eighteen minutes. That's earlier than planned — adjust."
detective: "Never hears this line."
- id: maintenance-corridors_d_107
text: "Usual pickup. Route's the same."
role: ring-operative
access: [insider]
trust: real
situation: [shift_transition, routine]
topic: [cargo, routine]
mood: [focused]
tags: [ring-operative, ring-ops, coded, usual-pickup]
dual_lens:
smuggler: "Same route, same handoff. No variations."
detective: "Never hears this line."
- id: maintenance-corridors_d_108
text: "The gap's clear. Scan's done at 14:20. Next window's yours."
role: ring-operative
access: [insider]
trust: real
situation: [shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [ring-operative, ring-ops, coded, scan-gap, surveillance]
dual_lens:
smuggler: "14:20 scan, then the window opens. Twenty-five minutes clear."
detective: "Never hears this line."
knowledge_grant:
fact_id: location.surveillance_gaps
confidence: knows_details
- id: maintenance-corridors_d_109
text: "Drin was asking about B-7. Twice today."
role: ring-operative
access: [insider]
trust: real
situation: [routine, confrontation]
topic: [danger, colleague]
mood: [concerned]
tags: [ring-operative, ring-ops, coded, drin-asking, warning]
dual_lens:
smuggler: "Drin's circling B-7. That's a warning. Push the pickup or hold it."
detective: "Never hears this line."
knowledge_grant:
fact_id: investigation.drin_inspection_pattern
confidence: suspects
- id: maintenance-corridors_d_110
text: "Dead air through here to the C-4 junction. No coverage."
role: ring-operative
access: [insider]
trust: real
situation: [routine]
topic: [routine]
mood: [focused]
tags: [ring-operative, ring-ops, coded, dead-air, surveillance]
dual_lens:
smuggler: "Full confirmation. Meridian doesn't reach this section."
detective: "Never hears this line."
knowledge_grant:
fact_id: location.surveillance_gaps
confidence: knows_details
- id: maintenance-corridors_d_111
text: "Voss confirmed the hold clears at 1400. He'll run the bay himself."
role: ring-operative
access: [insider]
trust: real
situation: [shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [ring-operative, ring-ops, voss, bay-four, clearance]
dual_lens:
smuggler: "Voss is personally running bay four at 1400. That's the signal the window is clear."
detective: "Never hears this line."
- id: maintenance-corridors_d_112
text: "Kael's on dock. He knows."
role: ring-operative
access: [insider]
trust: real
situation: [routine, shift_transition]
topic: [colleague, routine]
mood: [focused]
tags: [ring-operative, ring-ops, kael, coordination]
dual_lens:
smuggler: "Kael's in position. Ring is coordinated."
detective: "Never hears this line."
# ==========================================
# PRESSURE / WARNING SIGNALS
# ==========================================
- id: maintenance-corridors_d_113
text: "Commission eyes on the terminal floor. Not here yet."
role: ring-operative
access: [insider]
trust: real
situation: [confrontation, investigation]
topic: [danger, institution]
mood: [concerned]
tags: [ring-operative, ring-ops, commission, warning]
dual_lens:
smuggler: "Detective is visible on the main floor. Corridors are still clear."
detective: "Never hears this line."
- id: maintenance-corridors_d_114
text: "Hold the run. Drin's in the corridor section."
role: ring-operative
access: [insider]
trust: real
situation: [confrontation]
topic: [danger]
mood: [concerned]
tags: [ring-operative, ring-ops, drin, abort-signal]
dual_lens:
smuggler: "Stop. Drin's in position. The window's closed."
detective: "Never hears this line."
- id: maintenance-corridors_d_115
text: "Maret filed again. Voss has it. Don't move anything tonight."
role: ring-operative
access: [insider]
trust: real
situation: [confrontation, routine]
topic: [danger, institution]
mood: [concerned]
tags: [ring-operative, ring-ops, maret, voss, caution]
dual_lens:
smuggler: "Maret flagged something. Voss is managing it. Stand down tonight."
detective: "Never hears this line."
- id: maintenance-corridors_d_116
text: "We're clean. Same time tomorrow."
role: ring-operative
access: [insider]
trust: real
situation: [shift_end, routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, ring-ops, run-complete, closing]
dual_lens:
smuggler: "Run's done. No issues. Tomorrow."
detective: "Never hears this line."
# ==========================================
# IF DETECTIVE SOMEHOW ENTERS — surface only
# ==========================================
- id: maintenance-corridors_d_117
text: "Restricted access back here. Service personnel only."
role: ring-operative
access: [authority, public]
trust: surface
situation: [investigation, confrontation]
topic: [institution, routine]
mood: [wary]
tags: [ring-operative, detective-path, access-denial]
- id: maintenance-corridors_d_118
text: "I'm running maintenance. If there's a Commission access request, file it through Voss."
role: ring-operative
access: [authority]
trust: surface
situation: [investigation]
topic: [institution, routine]
mood: [wary]
tags: [ring-operative, detective-path, voss-redirect]
dual_lens:
smuggler: "Never encounters this."
detective: "Operative redirects to Voss. Everyone redirects to Voss. That's structural, not coincidence."
- id: maintenance-corridors_d_119
text: "Nothing unusual back here. Conduit work. Same every cycle."
role: ring-operative
access: [authority, public]
trust: surface
situation: [investigation]
topic: [routine]
mood: [wary]
tags: [ring-operative, detective-path, deflection]
# ==========================================
# GENERATION PASS — surface cover variants
# Surface-only: covers routine corridor presence without operational tells.
# ==========================================
- id: maintenance-corridors_d_120
text: "Power relay's been cycling weird. I'm watching it."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, generation-pass]
- id: maintenance-corridors_d_121
text: "Pael was supposed to handle B-7. He didn't show."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, colleague]
mood: [comfortable]
tags: [ring-operative, surface-cover, pael, generation-pass]
- id: maintenance-corridors_d_122
text: "Watch the floor here. Drain panel's been loose three cycles."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, generation-pass]
- id: maintenance-corridors_d_123
text: "Airflow's better down the B-section. C-section's got the recycler issue."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, spatial, generation-pass]
- id: maintenance-corridors_d_124
text: "You want the service bay, it's left at the junction. Don't go right — that's the restricted access."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, navigation, generation-pass]
dual_lens:
smuggler: "He's directing people away from the route. Social traffic management."
detective: "Operative redirects me away from C-4 access junction instinctively."
- id: maintenance-corridors_d_125
text: "Long shift. These corridors look the same after a while."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [tired]
tags: [ring-operative, surface-cover, generation-pass]
- id: maintenance-corridors_d_126
text: "Scan's clear. I checked it an hour ago."
role: ring-operative
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [ring-operative, surface-cover, generation-pass]
@@ -0,0 +1,77 @@
# Dialogue: transit-worker at Maintenance Corridors
# NPC: Tev Osel (Tier 3, NOBODY/CIVILIAN)
# Voice: patient, mildly distracted, confused why anyone is asking him anything.
# KEY LINE: maintenance-corridors_d_001 — sounds like surveillance positioning inquiry
# Actually: Tev missed Kosse at the corridor junction and is looking for them
# Ticket: #307 | Sprint: 12
location: maintenance-corridors
role: transit-worker
lines:
# ==========================================
# "SEEMS IMPORTANT BUT ISN'T" LINE
# Sounds like Tev is checking who came through a surveillance point.
# Actually: Tev missed his partner at the regular meeting spot.
# ==========================================
- id: maintenance-corridors_d_001
text: "You see which way Kosse went? Should've been through here twenty minutes ago."
role: transit-worker
access: [public]
trust: surface
situation: [routine, observation]
topic: [colleague, personal]
mood: [comfortable]
tags: [tev, seems-important-but-isnt, waiting, kosse]
dual_lens:
smuggler: "Someone asking who came through the corridor. Tev. Harmless — checking for a partner."
detective: "NPC at corridor junction, tracking who passed through. Possible lookout. Follow up: who is Kosse?"
notes: >
THE key flat NPC line for Tev. Sounds like Tev monitors corridor traffic
for operational reasons. Is someone who missed a meeting time with their
partner. The detective's suspicion is reasonable; it resolves to nothing.
# ==========================================
# ROUTINE LINES
# ==========================================
- id: maintenance-corridors_d_002
text: "They shifted the freight route again. Always forget which junction."
role: transit-worker
access: [public]
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [tev, atmospheric]
- id: maintenance-corridors_d_003
text: "Just on break. Out of your way in a minute."
role: transit-worker
access: [public]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [tev, incidental]
- id: maintenance-corridors_d_004
text: "Late freight route runs long. Can't do anything about it."
role: transit-worker
access: [public]
trust: surface
situation: [routine, night_shift]
topic: [routine, cargo]
mood: [comfortable]
tags: [tev, atmospheric]
- id: maintenance-corridors_d_005
text: "Waiting for someone. Shouldn't be long."
role: transit-worker
access: [public]
trust: surface
situation: [routine, observation]
topic: [personal]
mood: [comfortable]
tags: [tev, waiting]
@@ -1,2 +1,382 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: bar-owner at The Last Shift
# NPC: Lera Osk (Tier 2, SYSTEM/OPERATOR)
# Voice: direct, dry. Runs her bar like a territory. Not political — protective.
# Ring-aware: knows it exists in her bar. Doesn't ask. Doesn't interfere. Not complicit.
# Torek arrangement: she gives him a tab specifically — Commission visibility is useful.
# Ticket: #191 | Sprint: 12
location: the-last-shift
role: bar-owner
lines:
# ==========================================
# GREETINGS — territorial welcome
# ==========================================
- id: the-last-shift_d_100
text: "Usual?"
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting, economy-of-words]
- id: the-last-shift_d_101
text: "Long shift?"
role: bar-owner
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting]
- id: the-last-shift_d_102
text: "Bar's clean, drink's cold. That's the deal."
role: bar-owner
access: [public]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting, house-rules]
- id: the-last-shift_d_103
text: "Sess'll get you. I've got something in the back."
role: bar-owner
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting, redirection]
# ==========================================
# BAR MANAGEMENT — house rules, operations
# ==========================================
- id: the-last-shift_d_104
text: "Fights go outside. First and last warning."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [confrontation, routine]
topic: [routine]
mood: [comfortable]
tags: [lera, house-rules, authority]
- id: the-last-shift_d_105
text: "Tab closes at end of cycle. Not at end of your memory."
role: bar-owner
access: [public, peer]
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [comfortable]
tags: [lera, tab, house-rules]
- id: the-last-shift_d_106
text: "Torek's running a tab. That's fine. Don't ask me why."
role: bar-owner
access: [peer]
trust: surface
situation: [routine]
topic: [routine, institution]
mood: [comfortable]
tags: [lera, torek, tab-arrangement]
dual_lens:
smuggler: "Lera chose to give Torek credit. Commission man. She's keeping him visible and comfortable. Smart."
detective: "Bar owner specifically maintains a tab for a Commission officer. Useful relationship or leverage?"
notes: "Lera's Torek arrangement: she keeps him comfortable because knowing where the Commission officer is drinking is better than not knowing."
- id: the-last-shift_d_107
text: "Lattice stays off at the bar. Sign's on the wall."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [routine]
topic: [routine, institution]
mood: [comfortable]
tags: [lera, lattice-free, house-rules]
notes: "Not legally enforceable — Lera knows this. It's about atmosphere, not compliance."
- id: the-last-shift_d_108
text: "Grain spirit's back. Limited run. Don't make it last two nights."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [lera, drinks, community]
# ==========================================
# PEOPLE — Lera knows everyone
# ==========================================
- id: the-last-shift_d_109
text: "Kael's in the back. If you're looking."
role: bar-owner
access: [peer, insider]
trust: surface
situation: [arrival, routine]
topic: [colleague, routine]
mood: [comfortable]
tags: [lera, kael, spatial-information]
dual_lens:
smuggler: "Good. She keeps track. That's what she does."
detective: "Lera knows where Kael Davan is without being asked. She tracks her regulars."
- id: the-last-shift_d_110
text: "Naia's been in early the last few nights. Doesn't say much."
role: bar-owner
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [lera, naia, observation]
dual_lens:
smuggler: "Lera noticed Naia's off. She notices everything. She's saying it because she wants someone to do something."
detective: "Bar owner volunteers behavioral change in Naia Tamm. Lera observes her regulars carefully."
- id: the-last-shift_d_111
text: "Ren? Been here two months. Quiet, pays their tab. That's all I can tell you."
role: bar-owner
access: [public, peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [lera, ren, drifter]
dual_lens:
smuggler: "That's Lera's version of a clean assessment. She'd say more if she suspected something."
detective: "Lera's read on Ren: quiet, no trouble. If Lera's not worried, the baseline is probably accurate."
- id: the-last-shift_d_112
text: "Tev? Good worker. Three years. Minds their own."
role: bar-owner
access: [public, peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [lera, tev, endorsement]
dual_lens:
smuggler: "Lera vouching for Tev. She doesn't do that for people she doesn't trust."
detective: "Lera's read on Tev is straightforward. 'Minds their own' from Lera means she's tested it."
# ==========================================
# RING-ADJACENT — Lera's knowledge edge
# ==========================================
- id: the-last-shift_d_113
text: "Whatever's happening in the back corner isn't my business. Don't make it my business."
role: bar-owner
access: [insider]
trust: real
situation: [confrontation, alone]
topic: [danger, routine]
mood: [focused]
tags: [lera, ring-ops, willful-ignorance]
dual_lens:
smuggler: "She knows. She's drawing her line. That's the deal."
detective: "Never hears this line."
notes: "Lera's arrangement: she doesn't ask, she doesn't see, and she keeps things stable. Pragmatic, not complicit."
- id: the-last-shift_d_114
text: "Kael's got my number. He's been a regular long enough that that means something."
role: bar-owner
access: [peer]
trust: real
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [lera, kael, loyalty]
dual_lens:
smuggler: "Lera's loyal to Kael. Not to the ring. There's a difference and it matters."
detective: "Lera's personal loyalty to Kael Davan is explicit. She'll protect him over process."
# ==========================================
# DETECTIVE ENCOUNTER — neutral, unwelcoming
# ==========================================
- id: the-last-shift_d_115
text: "Commission's welcome. Like everyone else. Bar rules apply."
role: bar-owner
access: [authority, public]
trust: surface
situation: [arrival, investigation]
topic: [institution, routine]
mood: [comfortable]
tags: [lera, detective-path, neutral-welcome]
- id: the-last-shift_d_116
text: "If someone in my bar has something to say to Commission, they'll say it. I don't push that."
role: bar-owner
access: [authority]
trust: surface
situation: [investigation, routine]
topic: [institution, routine]
mood: [comfortable]
tags: [lera, detective-path, institutional-independence]
- id: the-last-shift_d_117
text: "I've run this bar twelve years without a Commission complaint. I'd like to keep it that way."
role: bar-owner
access: [authority]
trust: surface
situation: [investigation, confrontation]
topic: [institution, routine]
mood: [wary]
tags: [lera, detective-path, authority-assertion]
- id: the-last-shift_d_118
text: "I see a lot. I don't always remember all of it."
role: bar-owner
access: [authority]
trust: surface
situation: [investigation]
topic: [institution, routine]
mood: [comfortable]
tags: [lera, detective-path, careful-language]
dual_lens:
smuggler: "Never encounters this line."
detective: "Lera is very deliberately declining to volunteer what she knows. That's not ignorance — that's a position."
# ==========================================
# ELEVATED TRUST — what Lera actually says
# ==========================================
- id: the-last-shift_d_119
text: "Naia's been asking questions I can't answer. Someone should."
role: bar-owner
access: [peer, insider]
trust: real
situation: [routine, alone]
topic: [colleague, danger]
mood: [concerned]
tags: [lera, naia, threshold-line]
dual_lens:
smuggler: "Lera's telling me to do something about Naia. She's right."
detective: "Lera is flagging Naia Tamm as someone who's looking for answers. That's a conversation I need to have."
# ==========================================
# SIGN-OFF / CLOSING TIME
# ==========================================
- id: the-last-shift_d_120
text: "Last call. Finish up."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [lera, closing]
- id: the-last-shift_d_121
text: "Bar's closed. Come back next cycle."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [lera, closing]
- id: the-last-shift_d_122
text: "Good night. Stay out of trouble."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [lera, closing]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-last-shift_d_123
text: "You look tired. Sit."
role: bar-owner
access: [peer, insider]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting, generation-pass]
- id: the-last-shift_d_124
text: "Same as yesterday?"
role: bar-owner
access: [peer, insider]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting, generation-pass]
- id: the-last-shift_d_125
text: "New face. Bar rules are on the wall."
role: bar-owner
access: [public]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, greeting, newcomer, generation-pass]
- id: the-last-shift_d_126
text: "No trouble tonight. I mean it."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [lera, house-rules, generation-pass]
- id: the-last-shift_d_127
text: "Sess'll get you settled. I'm in the back."
role: bar-owner
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [lera, redirect, generation-pass]
- id: the-last-shift_d_128
text: "We close at 2300. Don't make me say it twice."
role: bar-owner
access: [public, peer]
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [comfortable]
tags: [lera, house-rules, generation-pass]
- id: the-last-shift_d_129
text: "Good crowd tonight. Quiet enough."
role: bar-owner
access: [peer, insider]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [lera, ambient, generation-pass]
- id: the-last-shift_d_130
text: "Bar's yours. I've got accounts."
role: bar-owner
access: [public, peer, insider]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [lera, ambient, generation-pass]
@@ -1,2 +1,360 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: bar-regular at The Last Shift
# NPC: Generic regular (Tier 3 template role — transit district workers, post-shift)
# Voice: off-duty, community-minded. Complains, gossips, opinionates. Belongs here.
# Ring-aware: varies. Most know something's off at the terminal. None know specifics.
# Ticket: #191 | Sprint: 12
location: the-last-shift
role: bar-regular
lines:
# ==========================================
# GREETINGS — familiar, community register
# ==========================================
- id: the-last-shift_d_300
text: "Long one today?"
role: bar-regular
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, greeting]
- id: the-last-shift_d_301
text: "You're in early. Good shift or bad shift?"
role: bar-regular
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, greeting]
- id: the-last-shift_d_302
text: "Grain spirit's back. I already had two. Don't judge."
role: bar-regular
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, greeting, drinks]
# ==========================================
# TERMINAL FLOOR — off-duty opinions
# ==========================================
- id: the-last-shift_d_303
text: "Push today. Bay six was backed up until 2000. Voss wasn't happy."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [bar-regular, terminal, voss]
- id: the-last-shift_d_304
text: "Maret flagged another one. Nobody knows what Voss does with them."
role: bar-regular
access: [peer]
trust: surface
situation: [routine]
topic: [colleague, cargo]
mood: [wary]
tags: [bar-regular, maret, voss, discrepancy]
dual_lens:
smuggler: "They're talking about Maret's flags as general knowledge. That's a wider problem than I thought."
detective: "Maret's pattern is common knowledge on the floor. Multiple witnesses, informal. That can be confirmed."
- id: the-last-shift_d_305
text: "Bay four's been held all week. Three cycles. Nobody's said what's in it."
role: bar-regular
access: [peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [wary]
tags: [bar-regular, bay-four, observation]
dual_lens:
smuggler: "They've noticed bay four. It's becoming visible. Not good."
detective: "Bay four hold is common knowledge at worker level. That means it predates any routine explanation."
- id: the-last-shift_d_306
text: "Transition window's the worst part of the job. Twenty minutes to hand off and nothing ever lines up."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, transition-window, complaint]
dual_lens:
smuggler: "The window is their biggest frustration. And their biggest frustration is the ring's biggest asset."
detective: "Workers discuss the transition window as a systematic failure. The gap is known."
- id: the-last-shift_d_307
text: "Commission audit's coming cycle fifteen. Everyone's running around double-checking everything."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [wary]
tags: [bar-regular, commission, audit]
dual_lens:
smuggler: "Cycle fifteen. That's the deadline. Get clean before then."
detective: "Workers are responding to the audit announcement. Behavioral change in the workforce. Note who isn't anxious."
- id: the-last-shift_d_308
text: "Voss has been on the floor more than usual this cycle. Watching everything."
role: bar-regular
access: [peer]
trust: surface
situation: [routine]
topic: [institution, colleague]
mood: [wary]
tags: [bar-regular, voss, observation]
dual_lens:
smuggler: "Voss is visible more. Is that heat from above, or is he managing something on the floor?"
detective: "Voss behavioral change: increased floor presence. Defensive positioning or active oversight?"
# ==========================================
# COMMUNITY — the full life of this district
# ==========================================
- id: the-last-shift_d_309
text: "Drifters lost again. Four-nil this time. I'm done watching until they rebuild."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, sports, drifters]
- id: the-last-shift_d_310
text: "Ventilation's still the same in Sector 3. Seven years of 'under discussion.' I've stopped expecting anything."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [comfortable]
tags: [bar-regular, infrastructure, grievance]
- id: the-last-shift_d_311
text: "You see the missing person notice on the board? Vessels Tamm. I don't know a Vessels Tamm."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [community, routine]
mood: [comfortable]
tags: [bar-regular, vessels-tamm, notice]
dual_lens:
smuggler: "Vessels Tamm. Naia's surname is Tamm. Don't chase it — but don't ignore it either."
detective: "Vessels Tamm notice is circulating casually. People haven't connected it to Naia Tamm yet."
- id: the-last-shift_d_312
text: "Re-em clinic's backed up four cycles for non-urgent. Main station if it's serious, apparently."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [community, routine]
mood: [comfortable]
tags: [bar-regular, clinic, community]
- id: the-last-shift_d_313
text: "Lattice upgrades are up eighteen percent this cycle. That's more than my raise."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, community]
mood: [comfortable]
tags: [bar-regular, lattice, economy]
dual_lens:
smuggler: "That's why the waiting list keeps growing. They're providing a service."
detective: "Economic grievance expressed casually. Price pressure is general knowledge. Confirms motive context."
- id: the-last-shift_d_314
text: "Workers' Association is pushing the handoff protocol again. Voss won't budge."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, community]
mood: [wary]
tags: [bar-regular, workers-association, voss, transition-window]
dual_lens:
smuggler: "Voss is defending the twenty-minute window against Association pressure. He's protecting the gap."
detective: "Voss is actively resisting worker-led protocol improvement. Institutional reason for the gap isn't mechanical."
# ==========================================
# DETECTIVE ENCOUNTER — community wariness
# ==========================================
- id: the-last-shift_d_315
text: "You new? Haven't seen you before."
role: bar-regular
access: [public]
trust: surface
situation: [arrival, investigation]
topic: [routine]
mood: [wary]
tags: [bar-regular, detective-path, recognition]
- id: the-last-shift_d_316
text: "Commission? I just drink here. Whatever you're after, I probably can't help."
role: bar-regular
access: [authority]
trust: surface
situation: [investigation, routine]
topic: [institution, routine]
mood: [wary]
tags: [bar-regular, detective-path, deflection]
- id: the-last-shift_d_317
text: "Ask Lera. She knows what happens in this bar better than anyone."
role: bar-regular
access: [authority, public]
trust: surface
situation: [investigation]
topic: [routine]
mood: [wary]
tags: [bar-regular, detective-path, lera-redirect]
# ==========================================
# SIGN-OFF
# ==========================================
- id: the-last-shift_d_318
text: "Another cycle done. Same tomorrow."
role: bar-regular
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, closing]
- id: the-last-shift_d_319
text: "Safe home. Watch the corridors after 2200."
role: bar-regular
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, closing, community]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-last-shift_d_320
text: "Pull up a chair. Floor's full otherwise."
role: bar-regular
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, greeting, generation-pass]
- id: the-last-shift_d_321
text: "You're in late. Voss kept you?"
role: bar-regular
access: [peer]
trust: surface
situation: [arrival]
topic: [routine, institution]
mood: [comfortable]
tags: [bar-regular, greeting, voss, generation-pass]
- id: the-last-shift_d_322
text: "Just got off. Bay six all night. My back knows it."
role: bar-regular
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [tired]
tags: [bar-regular, greeting, generation-pass]
- id: the-last-shift_d_323
text: "Drifters are playing Mirren next cycle. Home game. Might actually go."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, sports, community, generation-pass]
- id: the-last-shift_d_324
text: "Sector 3 meeting went nowhere. Same as last time. Same as the time before that."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, community]
mood: [comfortable]
tags: [bar-regular, infrastructure, grievance, generation-pass]
- id: the-last-shift_d_325
text: "New hire's asking questions on the floor again. Good. Someone should."
role: bar-regular
access: [peer]
trust: surface
situation: [routine]
topic: [colleague, routine]
mood: [comfortable]
tags: [bar-regular, new-hire, community, generation-pass]
- id: the-last-shift_d_326
text: "Span gate was loud today. Three cycles now. That's not normal."
role: bar-regular
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [wary]
tags: [bar-regular, infrastructure, generation-pass]
- id: the-last-shift_d_327
text: "Kael's not in tonight. Unusual."
role: bar-regular
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [bar-regular, kael, observation, generation-pass]
dual_lens:
smuggler: "Kael's absence is visible. Other people notice too."
detective: "Kael Davan's routine absence is noted socially. Behavioral change is general knowledge."
- id: the-last-shift_d_328
text: "Lera keeps the grain spirit back until she's sure you're a regular. You must've made the grade."
role: bar-regular
access: [peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [bar-regular, lera, humor, generation-pass]
- id: the-last-shift_d_329
text: "Good night. Don't let the Commission ruin your morning."
role: bar-regular
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine, institution]
mood: [comfortable]
tags: [bar-regular, closing, generation-pass]
@@ -1,2 +1,311 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: bartender at The Last Shift
# NPC: Sess (Tier 3 template role — Lera's staff)
# Voice: practical, pleasant, socially adept. Passes information by reflex, not design.
# Ring-aware: none. Knows regulars, patterns, who drinks with whom.
# Ticket: #191 | Sprint: 12
location: the-last-shift
role: bartender
lines:
# ==========================================
# GREETINGS — service register
# ==========================================
- id: the-last-shift_d_200
text: "What'll it be?"
role: bartender
access: [public, peer, insider]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [sess, greeting]
- id: the-last-shift_d_201
text: "Back early tonight. Long shift?"
role: bartender
access: [public, peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [sess, greeting, social]
- id: the-last-shift_d_202
text: "Grain spirit's back. Lera's running it at cycle price for two more days."
role: bartender
access: [public, peer, insider]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [sess, greeting, drinks]
# ==========================================
# BAR OBSERVATIONS — social intelligence
# ==========================================
- id: the-last-shift_d_203
text: "Kael's been in most nights this week. Quiet though. Not his usual."
role: bartender
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [sess, kael, observation]
dual_lens:
smuggler: "Sess noticed Kael's off. She's reading it as personal. She doesn't know what's underneath."
detective: "Bartender flagging behavioral change in Kael Davan. Consistent with other observations."
- id: the-last-shift_d_204
text: "Torek tipped well again tonight. Always does. Nicer than you'd expect for Commission."
role: bartender
access: [peer, public]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [comfortable]
tags: [sess, torek, observation]
dual_lens:
smuggler: "Torek's making friends with the staff. Not good."
detective: "Bartender views Torek favorably. Torek is cultivating the service staff deliberately."
- id: the-last-shift_d_205
text: "Naia was in earlier. Didn't stay. Left something on the table — looked like a note. Lera has it."
role: bartender
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [sess, naia, note, observation]
dual_lens:
smuggler: "Naia left something. Lera has it. Go to Lera."
detective: "Naia Tamm left written material at the bar. Lera is holding it. That's a material lead."
- id: the-last-shift_d_206
text: "Ren? Yeah, they're always in the morning crowd. Don't say much. Good for the morning."
role: bartender
access: [public, peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [sess, ren, drifter]
- id: the-last-shift_d_207
text: "Tev watches the door. Every shift. Lera likes it that way."
role: bartender
access: [peer]
trust: surface
situation: [routine]
topic: [colleague, routine]
mood: [comfortable]
tags: [sess, tev, door, observation]
dual_lens:
smuggler: "Tev watches the door for Lera. Not for anyone else. That's the setup."
detective: "Tev's entrance-monitoring is confirmed as Lera's request. Not operational surveillance."
- id: the-last-shift_d_208
text: "Sera was in earlier. She usually stays later. She left when Torek came in."
role: bartender
access: [peer]
trust: surface
situation: [routine]
topic: [colleague, institution]
mood: [comfortable]
tags: [sess, sera, torek, observation]
dual_lens:
smuggler: "Commission in, Commission contact out. Makes sense."
detective: "Third data point. Sera leaves when Torek arrives. Bartender confirms the pattern."
# ==========================================
# SOCIAL / COMMUNITY
# ==========================================
- id: the-last-shift_d_209
text: "Drifters lost again. Tev took it hard. Drank two too many."
role: bartender
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [sess, tev, sports, community]
- id: the-last-shift_d_210
text: "Workers' Association meeting's Thursday. Half the regulars will be here Friday in a mood either way."
role: bartender
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [comfortable]
tags: [sess, workers-association, community]
- id: the-last-shift_d_211
text: "Quiet night. Good for everyone."
role: bartender
access: [public, peer, insider]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [sess, ambient]
# ==========================================
# DETECTIVE ENCOUNTER — helpful, unfocused
# ==========================================
- id: the-last-shift_d_212
text: "Commission comes in sometimes. Not usually this early in the cycle."
role: bartender
access: [authority, public]
trust: surface
situation: [arrival, investigation]
topic: [institution, routine]
mood: [comfortable]
tags: [sess, detective-path, observation]
- id: the-last-shift_d_213
text: "I know most of the regulars. Ask me anything. I probably know something."
role: bartender
access: [authority, public]
trust: surface
situation: [investigation, routine]
topic: [routine]
mood: [comfortable]
tags: [sess, detective-path, open]
dual_lens:
smuggler: "Never encounters this line."
detective: "Bartender is cooperative. Useful — but she'll answer questions she doesn't know are questions."
- id: the-last-shift_d_214
text: "Lera's the one who knows the real history of this place. I've only been here four cycles."
role: bartender
access: [authority, public]
trust: surface
situation: [investigation]
topic: [institution, routine]
mood: [comfortable]
tags: [sess, detective-path, lera-redirect]
# ==========================================
# SIGN-OFF
# ==========================================
- id: the-last-shift_d_215
text: "Last round's on the board. After that, Lera calls it."
role: bartender
access: [public, peer, insider]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [sess, closing]
- id: the-last-shift_d_216
text: "Good night. Safe walk."
role: bartender
access: [public, peer, insider]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [sess, closing]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-last-shift_d_217
text: "The usual? Or trying something different tonight?"
role: bartender
access: [peer, insider]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [sess, greeting, generation-pass]
- id: the-last-shift_d_218
text: "Busy one tonight. Give me a second."
role: bartender
access: [public, peer, insider]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [sess, greeting, generation-pass]
- id: the-last-shift_d_219
text: "Good shift or bad shift? Your face says one thing, your order'll confirm it."
role: bartender
access: [peer]
trust: surface
situation: [arrival]
topic: [routine]
mood: [comfortable]
tags: [sess, greeting, social, generation-pass]
- id: the-last-shift_d_220
text: "Lera says the specials board is final. Don't ask me to substitute."
role: bartender
access: [public, peer]
trust: surface
situation: [routine, arrival]
topic: [routine]
mood: [comfortable]
tags: [sess, lera, house-rules, generation-pass]
- id: the-last-shift_d_221
text: "Torek's in his usual spot. Corner table. Second drink already."
role: bartender
access: [peer]
trust: surface
situation: [routine]
topic: [institution, colleague]
mood: [comfortable]
tags: [sess, torek, observation, generation-pass]
- id: the-last-shift_d_222
text: "Ren's been here since this morning. Still on the same drink."
role: bartender
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [sess, ren, observation, generation-pass]
- id: the-last-shift_d_223
text: "Drifters game is on the Meridian relay tonight. If anyone's got a feed, that'll be the crowd."
role: bartender
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [sess, sports, community, generation-pass]
- id: the-last-shift_d_224
text: "Slow night for a change. Enjoy it."
role: bartender
access: [public, peer, insider]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [sess, ambient, generation-pass]
- id: the-last-shift_d_225
text: "Tab's cleared. Lera'll be pleased."
role: bartender
access: [peer]
trust: surface
situation: [shift_end, routine]
topic: [routine]
mood: [comfortable]
tags: [sess, closing, generation-pass]
@@ -0,0 +1,90 @@
# Dialogue: day-worker at The Last Shift
# NPC: Ren Tosse (Tier 3, NOBODY/CIVILIAN)
# Voice: casual-observant, asks questions without social friction. Just drifting.
# KEY LINE: the-last-shift_d_001 — sounds like intel gathering on night operations
# Actually: asking about after-hours freight work availability
# Ticket: #307 | Sprint: 12
location: the-last-shift
role: day-worker
lines:
# ==========================================
# "SEEMS IMPORTANT BUT ISN'T" LINE
# Sounds like Ren is casing night shift operations.
# Actually: asking if there's paid after-hours freight work available.
# ==========================================
- id: the-last-shift_d_001
text: "Night rotation still short-handed? Heard the after-hours freight pays decent."
role: day-worker
access: [public]
trust: surface
situation: [bar_evening, social]
topic: [money, cargo]
mood: [comfortable]
tags: [ren, seems-important-but-isnt, job-seeking, night-shift]
dual_lens:
smuggler: "Someone asking about the night rotation. Probably nothing. Watch them anyway."
detective: "Interest in night shift freight operations. Drifter, irregular schedule. Logging."
notes: >
THE key flat NPC line for Ren. Sounds like Ren is interested in the
night operation window for operational reasons. Is standard day-worker
job-seeking: which shift has openings, which shift pays best.
# ==========================================
# ROUTINE LINES
# ==========================================
- id: the-last-shift_d_002
text: "Bay seven any better this cycle? The super was a problem last time."
role: day-worker
access: [public]
trust: surface
situation: [social, bar_evening]
topic: [routine, colleague]
mood: [comfortable]
tags: [ren, job-seeking]
- id: the-last-shift_d_003
text: "Sova's decent. Station layout makes sense once you learn it."
role: day-worker
access: [public]
trust: surface
situation: [social, bar_evening]
topic: [routine]
mood: [comfortable]
tags: [ren, atmospheric]
- id: the-last-shift_d_004
text: "Just passing through. Maybe not. Depends on the work."
role: day-worker
access: [public]
trust: surface
situation: [social]
topic: [personal, routine]
mood: [comfortable]
tags: [ren, drifter]
- id: the-last-shift_d_005
text: "Cheap drink here's better than most stations I've stopped at."
role: day-worker
access: [public]
trust: surface
situation: [bar_evening, social]
topic: [personal]
mood: [comfortable]
tags: [ren, atmospheric]
- id: the-last-shift_d_006
text: "Any idea when the manifest deadline runs? Trying to time when the work window opens."
role: day-worker
access: [public]
trust: surface
situation: [social, bar_evening]
topic: [cargo, routine]
mood: [comfortable]
tags: [ren, seems-important-but-isnt, job-seeking]
notes: >
Second "seems important" line. Asking about manifest deadlines sounds
like operational intelligence. Is logistics scheduling for day work.
@@ -1,2 +1,269 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: courier at The Terminal
# NPC: Renn Harsk (Tier 2, THRESHOLD/GATEKEEPER)
# Voice: operational minimalism. Brief. Route-focused. Never volunteers more than needed.
# Ring-aware: works adjacent — carries documentation, not cargo. Knows what he carries.
# Cover: licensed freight courier. The documentation is the cover.
# Ticket: #190 | Sprint: 12
location: the-terminal
role: courier
lines:
# ==========================================
# ARRIVALS — checking in
# ==========================================
- id: the-terminal_d_500
text: "Route's clear. Moving on schedule."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, greeting, operational]
- id: the-terminal_d_501
text: "Manifest for bay three. Needs a scheduler stamp."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, greeting, procedural]
- id: the-terminal_d_502
text: "Inbound from Havara. Three cans, all matched."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, greeting, cargo]
# ==========================================
# SURFACE ROUTINE — low-information, on-script
# ==========================================
- id: the-terminal_d_503
text: "Not staying. Just the drop."
role: courier
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [renn, operational, brief]
- id: the-terminal_d_504
text: "Paperwork's clean. You can check."
role: courier
access: [public, peer, authority]
trust: surface
situation: [routine, investigation]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, procedural, documentation]
- id: the-terminal_d_505
text: "Pickup's scheduled at 1600. I'll be back."
role: courier
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, operational]
- id: the-terminal_d_506
text: "Corridor's clear from my end. No delays."
role: courier
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [renn, operational]
dual_lens:
smuggler: "Clean phrase. Public-safe. He'd say it either way."
detective: "Courier references corridor access unprompted. Note it."
# ==========================================
# RING-ADJACENT — insider/real trust only
# ==========================================
- id: the-terminal_d_507
text: "Documentation's handled. Bay four, per the note."
role: courier
access: [insider]
trust: real
situation: [routine, shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [renn, ring-ops, documentation]
dual_lens:
smuggler: "Renn's confirming the paperwork trail. Bay four, as arranged."
detective: "Never hears this line."
- id: the-terminal_d_508
text: "Transit stamp's current. Nothing flagged at the gate."
role: courier
access: [insider]
trust: real
situation: [routine, shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [renn, ring-ops, documentation]
- id: the-terminal_d_509
text: "Pickup window is the same as last cycle. Twenty minutes."
role: courier
access: [insider]
trust: real
situation: [shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [renn, ring-ops, transition-window]
dual_lens:
smuggler: "Twenty minutes. Transition window confirmation."
detective: "Never hears this line."
- id: the-terminal_d_510
text: "Tell Voss the hold note clears at 1400."
role: courier
access: [insider]
trust: real
situation: [routine, shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [renn, ring-ops, voss, bay-four]
dual_lens:
smuggler: "1400. The hold clears on schedule. Pass it up."
detective: "Never hears this line."
# ==========================================
# DETECTIVE ENCOUNTER — surface cooperation
# ==========================================
- id: the-terminal_d_511
text: "Licensed freight courier. Manifest's available on request."
role: courier
access: [authority]
trust: surface
situation: [investigation, routine]
topic: [institution, cargo]
mood: [comfortable]
tags: [renn, detective-path, procedural]
- id: the-terminal_d_512
text: "I drop documentation. What's inside the containers isn't my business."
role: courier
access: [authority]
trust: surface
situation: [investigation]
topic: [cargo, institution]
mood: [comfortable]
tags: [renn, detective-path, deflection]
dual_lens:
smuggler: "That's the answer. Clean and correct."
detective: "Classic courier deflection. Legally accurate. Operationally suspicious."
- id: the-terminal_d_513
text: "I've been running this route for two years. Nothing unusual."
role: courier
access: [authority, public]
trust: surface
situation: [investigation, confrontation]
topic: [cargo, institution]
mood: [comfortable]
tags: [renn, detective-path, authority-assertion]
# ==========================================
# DEPARTURES
# ==========================================
- id: the-terminal_d_514
text: "That's the drop. I'm out."
role: courier
access: [public, peer]
trust: surface
situation: [shift_end, routine]
topic: [routine]
mood: [comfortable]
tags: [renn, closing]
- id: the-terminal_d_515
text: "Next run's tomorrow. Early."
role: courier
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [renn, closing]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-terminal_d_516
text: "Two stops today. First one's done."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, greeting, generation-pass]
- id: the-terminal_d_517
text: "Outbound for Havara. Needs a bay assignment."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, greeting, procedural, generation-pass]
- id: the-terminal_d_518
text: "Gate's backed up. Running twenty minutes late."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [renn, delay, generation-pass]
- id: the-terminal_d_519
text: "Paperwork's in order. Same as always."
role: courier
access: [public, peer, authority]
trust: surface
situation: [routine, investigation]
topic: [routine, cargo]
mood: [comfortable]
tags: [renn, procedural, generation-pass]
- id: the-terminal_d_520
text: "Quick turnaround today. Back out by 1300."
role: courier
access: [public, peer]
trust: surface
situation: [arrival, routine]
topic: [routine]
mood: [comfortable]
tags: [renn, operational, generation-pass]
- id: the-terminal_d_521
text: "Signed off on bay three. That's me done here."
role: courier
access: [public, peer]
trust: surface
situation: [shift_end, routine]
topic: [routine]
mood: [comfortable]
tags: [renn, closing, generation-pass]
@@ -1,2 +1,397 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: dock-worker at The Terminal
# NPC: Generic dock worker (Tier 3 template role — multiple workers share this pool)
# Voice: short, practical, trade jargon. Community over institution. Not a lead.
# Ring-aware: none. Floor is their whole world.
# Ticket: #190 | Sprint: 12
location: the-terminal
role: dock-worker
lines:
# ==========================================
# GREETINGS — shift arrival
# ==========================================
- id: the-terminal_d_200
text: "Push today. Bay six's backed up."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_start, arrival]
topic: [routine, cargo]
mood: [comfortable]
tags: [dock-worker, greeting]
- id: the-terminal_d_201
text: "Late night last cycle. Bay eight ran until 2300."
role: dock-worker
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine]
mood: [tired]
tags: [dock-worker, greeting]
- id: the-terminal_d_202
text: "Voss is on already. Floor's locked down tight."
role: dock-worker
access: [peer]
trust: surface
situation: [arrival, shift_start]
topic: [institution, routine]
mood: [wary]
tags: [dock-worker, voss, caution]
dual_lens:
smuggler: "Voss is in early. If the schedule's tight, the window shifts. Check it."
detective: "Workers treat the supervisor as a weather system — something to navigate around, not toward."
# ==========================================
# FLOOR TALK — routine, cargo, observation
# ==========================================
- id: the-terminal_d_203
text: "Bay four's been on hold three days. Nobody says why."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [dock-worker, bay-four, procedural]
dual_lens:
smuggler: "He doesn't know what's in it. Just annoyed at the backup."
detective: "Worker notices Bay 4 hold as unusual. Not suspicious — just a worker tracking the floor."
- id: the-terminal_d_204
text: "Manifest looks clean. Three inbounds, two outbounds. Light day."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [cargo, routine]
mood: [comfortable]
tags: [dock-worker, manifest]
- id: the-terminal_d_205
text: "Maret's been at that terminal all morning. Never good."
role: dock-worker
access: [peer]
trust: surface
situation: [routine]
topic: [colleague, routine]
mood: [wary]
tags: [dock-worker, maret, observation]
dual_lens:
smuggler: "Maret reviewing something. If she flags a discrepancy, Voss'll sit on it. He always does."
detective: "Workers have noticed Maret's behavior. Whatever she found, it's not small."
- id: the-terminal_d_206
text: "Drin ran bay six again last cycle. Third inspection this week."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, colleague]
mood: [comfortable]
tags: [dock-worker, drin, inspection]
dual_lens:
smuggler: "Drin on six while bay four stays held. Scheduled or arranged?"
detective: "Workers notice inspection distribution. Drin on six, bay four untouched."
- id: the-terminal_d_207
text: "Transition window's twice as long with Commission eyes on the floor."
role: dock-worker
access: [peer]
trust: surface
situation: [shift_transition, routine]
topic: [institution, routine]
mood: [wary]
tags: [dock-worker, commission, transition-window]
dual_lens:
smuggler: "Commission presence during transition. Window's compromised."
detective: "Workers slow down when Commission is visible. Normal — or trained."
- id: the-terminal_d_208
text: "Good crew this cycle. Linn and Pael are both on."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [colleague, routine]
mood: [comfortable]
tags: [dock-worker, pael, community]
- id: the-terminal_d_209
text: "Cargo mark's off by forty, according to Maret. Voss logged it and moved on."
role: dock-worker
access: [peer]
trust: surface
situation: [routine]
topic: [cargo, colleague]
mood: [wary]
tags: [dock-worker, maret, voss, discrepancy]
dual_lens:
smuggler: "Forty kilos. Maret flagged it. Voss buried it. Same as last time."
detective: "Worker knows about a flagged discrepancy that was suppressed. That's a witness."
knowledge_grant:
fact_id: investigation.oversight_gap_pattern
confidence: suspects
- id: the-terminal_d_210
text: "C-4 containers haven't been checked in three days. Not our problem, apparently."
role: dock-worker
access: [peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [wary]
tags: [dock-worker, c4-storage, observation]
dual_lens:
smuggler: "C-4's getting casual attention. Note it."
detective: "C-4 storage. Three days, no verification. That's the anomaly window."
# ==========================================
# COMMUNITY / CASUAL
# ==========================================
- id: the-terminal_d_211
text: "You heading to the Last Shift after? Lera's got the grain spirit back."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_end, routine]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, social, the-last-shift]
- id: the-terminal_d_212
text: "Workers' Association meeting's cycle twenty-two. Handoff protocol's back on the agenda."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [comfortable]
tags: [dock-worker, workers-association, community]
- id: the-terminal_d_213
text: "Drifters lost again. Four-three. Pael seemed to take it personally."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, social, pael]
# ==========================================
# DETECTIVE ENCOUNTER — deflection, redirect
# ==========================================
- id: the-terminal_d_214
text: "Manifest questions go to the scheduler. I'm floor-side."
role: dock-worker
access: [authority, public]
trust: surface
situation: [investigation, routine]
topic: [institution, routine]
mood: [wary]
tags: [dock-worker, detective-path, redirect]
- id: the-terminal_d_215
text: "Commission's asked before. Same answers. We run a clean floor."
role: dock-worker
access: [authority]
trust: surface
situation: [investigation, confrontation]
topic: [institution, routine]
mood: [wary]
tags: [dock-worker, detective-path, deflection]
- id: the-terminal_d_216
text: "Voss handles discrepancies. That's above my level."
role: dock-worker
access: [authority, peer]
trust: surface
situation: [investigation]
topic: [institution, routine]
mood: [wary]
tags: [dock-worker, detective-path, voss, redirect]
# ==========================================
# PEER CANDOR — elevated trust
# ==========================================
- id: the-terminal_d_217
text: "Maret's going to push it. She doesn't leave a number sitting."
role: dock-worker
access: [peer]
trust: real
situation: [routine]
topic: [colleague]
mood: [wary]
tags: [dock-worker, maret, peer-candor]
dual_lens:
smuggler: "Maret's not going to drop it. That's the problem."
detective: "Workers think Maret will force the issue. She may be the break."
- id: the-terminal_d_218
text: "Voss runs a tight floor. Maybe too tight, if you take my meaning."
role: dock-worker
access: [peer]
trust: real
situation: [routine]
topic: [institution, colleague]
mood: [wary]
tags: [dock-worker, voss, peer-candor]
dual_lens:
smuggler: "He suspects Voss is shaving something. Doesn't know what. Doesn't want to."
detective: "Workers sense something off about Voss's management without naming it."
# ==========================================
# SIGN-OFF
# ==========================================
- id: the-terminal_d_219
text: "Bay's clear. Good shift."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, closing]
- id: the-terminal_d_220
text: "Night crew's here. We're done."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_end, shift_transition]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, closing]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-terminal_d_221
text: "Early start. Bay two's already loaded."
role: dock-worker
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [dock-worker, greeting, generation-pass]
- id: the-terminal_d_222
text: "Air recyclers are loud this morning. Must be the outer ring issue again."
role: dock-worker
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, station, generation-pass]
- id: the-terminal_d_223
text: "Pael's in early. Something must be broken in B-7 again."
role: dock-worker
access: [peer]
trust: surface
situation: [routine, shift_start]
topic: [colleague, routine]
mood: [comfortable]
tags: [dock-worker, pael, generation-pass]
- id: the-terminal_d_224
text: "Running light today. Maybe I'll actually make it to the Last Shift before close."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, social, generation-pass]
- id: the-terminal_d_225
text: "Bay three's got a scan hold. Not my problem. Not my bay."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [dock-worker, scan-hold, generation-pass]
- id: the-terminal_d_226
text: "New hire's back. Asking questions again. Good kid."
role: dock-worker
access: [peer]
trust: surface
situation: [routine]
topic: [colleague, routine]
mood: [comfortable]
tags: [dock-worker, new-hire, generation-pass]
- id: the-terminal_d_227
text: "Handoff's in ten. Don't disappear on me."
role: dock-worker
access: [peer]
trust: surface
situation: [shift_transition]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, handoff, generation-pass]
- id: the-terminal_d_228
text: "Span gate cycled early. All inbounds are going to be late. Plan around it."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [wary]
tags: [dock-worker, span-gate, generation-pass]
- id: the-terminal_d_229
text: "Linn's not in today. We're running two short on bay six."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [colleague, routine]
mood: [tired]
tags: [dock-worker, staffing, generation-pass]
- id: the-terminal_d_230
text: "Lunch is in the break room. Don't touch the one with Pael's name on it. He will notice."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, colleague]
mood: [comfortable]
tags: [dock-worker, pael, humor, generation-pass]
- id: the-terminal_d_231
text: "Overtime's posted if you want it. Bay eight, 1800 to 2200."
role: dock-worker
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, overtime, generation-pass]
- id: the-terminal_d_232
text: "Off on time today. First time this cycle."
role: dock-worker
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [dock-worker, closing, generation-pass]
@@ -0,0 +1,86 @@
# Dialogue: maintenance-tech at The Terminal
# NPC: Pael Varren (Tier 3, NOBODY/CIVILIAN)
# Voice: tired-competent, minimal, dry. Knows the infrastructure. Knows nothing else.
# KEY LINE: the-terminal_d_040 — sounds like surveillance knowledge, is a maintenance complaint
# Ticket: #307 | Sprint: 12
location: the-terminal
role: maintenance-tech
lines:
# ==========================================
# "SEEMS IMPORTANT BUT ISN'T" LINE
# Sounds like Pael monitors corridor traffic for a reason.
# Actually: the B-7 seal fails when people use the corridor too much. Pael notices load.
# ==========================================
- id: the-terminal_d_040
text: "B-7's had three seal failures this cycle. Someone's been through there more than they should."
role: maintenance-tech
access: [public]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [pael, seems-important-but-isnt, maintenance, b7]
dual_lens:
smuggler: "Pael knows the corridor's been used. But Pael knows nothing — this is an HVAC complaint."
detective: "Corridor B-7 traffic. A maintenance tech noticing irregular use. Unreliable but worth noting."
notes: >
THE key flat NPC line for Pael. Sounds like environmental intelligence.
Is a complaint about a recurring seal failure caused by foot traffic.
Pael doesn't know what the traffic is from. Never will.
# ==========================================
# ROUTINE LINES
# ==========================================
- id: the-terminal_d_041
text: "Air recycler's running hot again. Third deferred ticket this week."
role: maintenance-tech
access: [public]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [pael, atmospheric]
- id: the-terminal_d_042
text: "Loading arm three's been on my list for a month. Management keeps deferring it."
role: maintenance-tech
access: [public]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [pael, atmospheric]
- id: the-terminal_d_043
text: "If the gate tone shifts, stay clear of bay six. She needs recalibration."
role: maintenance-tech
access: [public]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [pael, warning, helpful]
- id: the-terminal_d_044
text: "Not me. I just fix them."
role: maintenance-tech
access: [public]
trust: surface
situation: [routine, social]
topic: [routine]
mood: [comfortable]
tags: [pael, response]
- id: the-terminal_d_045
text: "Gate hum's been three cycles off-spec. Nobody cares until it stops completely."
role: maintenance-tech
access: [public]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [pael, atmospheric]
@@ -1,2 +1,264 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: new-hire at The Terminal
# NPC: Generic new hire (Tier 3 template role — recently arrived worker)
# Voice: enthusiastic, still reading the room. Asks what veterans stopped asking.
# Ring-aware: none. Fresh eyes are the danger — they notice what regulars stopped seeing.
# Ticket: #190 | Sprint: 12
location: the-terminal
role: new-hire
lines:
# ==========================================
# GREETINGS — early days
# ==========================================
- id: the-terminal_d_400
text: "Still figuring out the bay numbering. Six and eight are in the wrong order."
role: new-hire
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine]
mood: [comfortable]
tags: [new-hire, greeting, orientation]
- id: the-terminal_d_401
text: "Voss told me to check in with you. Is that the standard thing?"
role: new-hire
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine, institution]
mood: [comfortable]
tags: [new-hire, greeting, voss]
- id: the-terminal_d_402
text: "First full cycle on the floor. Trying to stay out of the way."
role: new-hire
access: [public, peer]
trust: surface
situation: [arrival, shift_start]
topic: [routine]
mood: [comfortable]
tags: [new-hire, greeting]
# ==========================================
# FRESH OBSERVATIONS — the dangerous innocence
# ==========================================
- id: the-terminal_d_403
text: "Does bay four usually get held that long? The other bays turned over twice already."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [new-hire, bay-four, observation]
dual_lens:
smuggler: "New eyes on bay four. He doesn't know what he's looking at. But he noticed."
detective: "New hire asking exactly the right question without knowing it."
- id: the-terminal_d_404
text: "I saw someone go into C-4 last night without a scan log. Is that normal?"
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [new-hire, c4-storage, observation, inadvertent-witness]
dual_lens:
smuggler: "He saw someone in C-4 without a log. Does he know who? This is a problem."
detective: "Unlogged C-4 access. New hire, noticed it, mentioned it casually. He doesn't know what he saw."
knowledge_grant:
fact_id: investigation.cargo_anomaly
confidence: suspects
notes: "High-value inadvertent witness line. Operationally dangerous for the ring."
- id: the-terminal_d_405
text: "Why does the transition window only get twenty minutes? The stations I trained at ran thirty."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine, shift_transition]
topic: [routine]
mood: [comfortable]
tags: [new-hire, transition-window, comparison]
dual_lens:
smuggler: "He's asking about the window. Doesn't know why it matters. Answer carefully."
detective: "New hire confirms the window length is unusually short. File that."
- id: the-terminal_d_406
text: "Do containers usually get rerouted mid-hold? One went out through bay four while I thought it was still held."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [new-hire, bay-four, inadvertent-witness]
dual_lens:
smuggler: "He saw movement through bay four during the hold. This is a liability."
detective: "Container movement through bay four during official hold. Inadvertent eyewitness. Confirm timing."
knowledge_grant:
fact_id: investigation.manifest_discrepancy
confidence: suspects
- id: the-terminal_d_407
text: "Maintenance corridor access is just off C-4, right? I took the wrong turn twice."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [new-hire, maintenance-corridors, orientation]
dual_lens:
smuggler: "He found the corridor entrance by accident. Normal for new workers. Noted."
detective: "New hire maps spatial relationship between C-4 storage and maintenance corridor access. That's the route."
# ==========================================
# LEARNING / QUESTIONS
# ==========================================
- id: the-terminal_d_408
text: "What's the Commission kiosk actually for? Nobody seems to use it."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [comfortable]
tags: [new-hire, commission, curiosity]
- id: the-terminal_d_409
text: "Maret seems stressed. Is that always the case or is something going on?"
role: new-hire
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [new-hire, maret, observation]
dual_lens:
smuggler: "He noticed Maret. Good that he's asking, not reporting. Manage this."
detective: "New hire reads Maret's stress without context. Confirms it's visible."
- id: the-terminal_d_410
text: "Voss seems really serious. Is he always like that?"
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [new-hire, voss, curiosity]
- id: the-terminal_d_411
text: "How long before Voss trusts you to run a bay on your own?"
role: new-hire
access: [peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [comfortable]
tags: [new-hire, voss, career]
# ==========================================
# SIGN-OFF
# ==========================================
- id: the-terminal_d_412
text: "That was a full shift. I'll have the bay numbers down by next cycle."
role: new-hire
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [new-hire, closing]
- id: the-terminal_d_413
text: "Good day. Lot to learn. But good."
role: new-hire
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [new-hire, closing]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-terminal_d_414
text: "Is there a trick to the manifest terminal or does it always lag like this?"
role: new-hire
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine]
mood: [comfortable]
tags: [new-hire, orientation, generation-pass]
- id: the-terminal_d_415
text: "How long has Maret been here? She seems like she knows everything."
role: new-hire
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [new-hire, maret, curiosity, generation-pass]
- id: the-terminal_d_416
text: "Nobody told me about the span gate frequency. I kept thinking something was wrong."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine, arrival]
topic: [routine]
mood: [comfortable]
tags: [new-hire, orientation, generation-pass]
- id: the-terminal_d_417
text: "The Last Shift — is that actually a bar? I haven't been yet."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [new-hire, social, generation-pass]
- id: the-terminal_d_418
text: "I thought the scanner on bay three was broken. Turns out you have to hold the badge longer."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [new-hire, orientation, humor, generation-pass]
- id: the-terminal_d_419
text: "Who's Drin? I keep hearing the name but haven't met them."
role: new-hire
access: [public, peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [new-hire, drin, curiosity, generation-pass]
- id: the-terminal_d_420
text: "Three cycles in and I still can't tell when Voss is satisfied. Is there a tell?"
role: new-hire
access: [peer]
trust: surface
situation: [routine]
topic: [colleague]
mood: [comfortable]
tags: [new-hire, voss, humor, generation-pass]
@@ -1,2 +1,371 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: scheduler at The Terminal
# NPC: Maret (Tier 2, WITNESS/SKEPTIC)
# Voice: precise, institutional. Numbers and containers, not people. Unease she keeps professional.
# Arc: flags discrepancies → gets suppressed by Voss → suspicion hardens → potential detective source
# Ring-aware: not involved. Seeing the pattern. Hasn't named it yet.
# Ticket: #190 | Sprint: 12
location: the-terminal
role: scheduler
lines:
# ==========================================
# GREETINGS — scheduler's opening read
# ==========================================
- id: the-terminal_d_300
text: "Manifest's current. Bay assignments are posted."
role: scheduler
access: [public, peer]
trust: surface
situation: [shift_start, arrival]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, procedural, greeting]
- id: the-terminal_d_301
text: "Container 4471 is still in C-4. Scheduled for bay six by 1400. Check with Voss if the hold's lifted."
role: scheduler
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [maret, container-4471, procedural]
dual_lens:
smuggler: "She's tracking 4471 as a scheduling issue. Not flagging it as a discrepancy yet."
detective: "Maret references container 4471 as an unresolved routing issue. She knows the number."
knowledge_grant:
fact_id: investigation.manifest_discrepancy
confidence: suspects
- id: the-terminal_d_302
text: "Three inbounds today. All verified against the manifest. No gaps so far."
role: scheduler
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [maret, procedural, manifest]
# ==========================================
# DISCREPANCY PATTERN — the core arc
# ==========================================
- id: the-terminal_d_303
text: "Container 4474 shows a forty-seven kilo discrepancy. I've flagged it twice."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine, investigation]
topic: [cargo, routine]
mood: [concerned]
tags: [maret, discrepancy, flagging]
dual_lens:
smuggler: "Which container? Not 4471. She's flagging a different one. Two flags, no escalation."
detective: "Maret has flagged container 4474 twice. If Voss suppressed it both times, there's a paper trail."
knowledge_grant:
fact_id: investigation.oversight_gap_pattern
confidence: suspects
- id: the-terminal_d_304
text: "It was logged as administrative. I don't know what that means for a weight discrepancy."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [concerned]
tags: [maret, discrepancy, institutional-response]
dual_lens:
smuggler: "She doesn't know what 'administrative' means either. Voss's answer covered nothing."
detective: "Maret knows the answer she got wasn't a real answer. She's still sitting with that."
- id: the-terminal_d_305
text: "The transition window anomalies cluster at shift change. I've run the numbers four times. It's not random."
role: scheduler
access: [peer]
trust: surface
situation: [routine, investigation]
topic: [cargo, routine]
mood: [concerned]
tags: [maret, pattern, transition-window]
dual_lens:
smuggler: "She's found the pattern. Not the cause. Not yet."
detective: "Maret has independently confirmed the transition window cluster. She's already halfway to the conclusion."
knowledge_grant:
fact_id: investigation.manifest_discrepancy
confidence: knows_details
- id: the-terminal_d_306
text: "Three separate manifest entries show weight variations under ten kilos. Within rounding, technically."
role: scheduler
access: [peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [concerned]
tags: [maret, pattern, manifest]
dual_lens:
smuggler: "She's tracking the small ones too. But 'within rounding' is what Voss told her. She's not satisfied."
detective: "Sub-threshold variations. Small enough to dismiss individually. Maret's running aggregate analysis."
- id: the-terminal_d_307
text: "I filed a formal notice with Voss. He reviewed it and told me it was resolved. It's not resolved."
role: scheduler
access: [peer]
trust: surface
situation: [routine, confrontation]
topic: [cargo, institution]
mood: [concerned]
tags: [maret, voss, suppression, frustration]
dual_lens:
smuggler: "She filed a formal notice. Voss sat on it. She knows. This is the pressure point."
detective: "Formal notice filed, suppressed by supervisor. If that notice exists in the system, it's evidence."
knowledge_grant:
fact_id: investigation.oversight_gap_pattern
confidence: knows_details
# ==========================================
# ROUTINE SCHEDULING — grounded work
# ==========================================
- id: the-terminal_d_308
text: "Bay eight's running on reduced capacity until 1700. Route inbounds through six."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, operational]
- id: the-terminal_d_309
text: "Drin's running the inspection on bay six this cycle. Bay four's on hold per Voss's directive."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, drin, bay-four, scheduling]
dual_lens:
smuggler: "Maret confirmed it: bay four is Voss's directive. Not a system hold."
detective: "The bay four hold comes directly from Voss, not from the system. Discretionary hold."
- id: the-terminal_d_310
text: "Overtime requests go through Meridian. I can't process them on paper anymore."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, institution]
mood: [comfortable]
tags: [maret, administrative]
- id: the-terminal_d_311
text: "Manifest update at 1300. If your containers aren't posted by then, they don't make the cycle."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, procedural, manifest]
# ==========================================
# DETECTIVE ENCOUNTER — access and resistance
# ==========================================
- id: the-terminal_d_312
text: "Commission manifest access requires a formal request through Voss. I can tell you what's on the public record."
role: scheduler
access: [authority]
trust: surface
situation: [investigation, routine]
topic: [institution, cargo]
mood: [comfortable]
tags: [maret, detective-path, procedural]
- id: the-terminal_d_313
text: "The flagged entries are in the system. They were logged as reviewed and closed. That's what I can confirm."
role: scheduler
access: [authority]
trust: surface
situation: [investigation]
topic: [cargo, institution]
mood: [concerned]
tags: [maret, detective-path, discrepancy-disclosure]
dual_lens:
smuggler: "Never encounters this line."
detective: "Maret confirms the flags exist in the system. And she's using careful language: 'what I can confirm.' There's more."
- id: the-terminal_d_314
text: "I'm not in a position to speak to why certain reviews were closed the way they were."
role: scheduler
access: [authority]
trust: surface
situation: [investigation, confrontation]
topic: [institution, cargo]
mood: [concerned]
tags: [maret, detective-path, careful-language]
dual_lens:
smuggler: "Never encounters this line."
detective: "She's being very precise about what she will and won't say. 'Not in a position' means she has a position. Come back when she trusts me more."
# ==========================================
# ELEVATED TRUST — Maret breaks professional distance
# ==========================================
- id: the-terminal_d_315
text: "The pattern I'm seeing doesn't make sense for equipment failure. It's too consistent."
role: scheduler
access: [peer, insider]
trust: real
situation: [routine, investigation]
topic: [cargo, investigation]
mood: [concerned]
tags: [maret, peer-candor, pattern]
dual_lens:
smuggler: "She's figured it out. She hasn't said what 'it' is. But she knows the numbers aren't random."
detective: "Maret has crossed from anomaly-logging into active pattern recognition. She's running an informal parallel investigation."
knowledge_grant:
fact_id: investigation.manifest_discrepancy
confidence: knows_details
- id: the-terminal_d_316
text: "I filed a second formal notice two cycles ago. It's been under review since then. By Voss."
role: scheduler
access: [peer]
trust: real
situation: [routine, confrontation]
topic: [cargo, institution]
mood: [concerned]
tags: [maret, voss, suppression, peer-candor]
dual_lens:
smuggler: "Two formal notices. Both sitting with Voss. He's managing her output directly."
detective: "Second formal notice. Both under Voss's review. That's documented suppression with a timestamp."
knowledge_grant:
fact_id: behavioral.voss_awareness
confidence: knows_details
- id: the-terminal_d_317
text: "I don't think I should be the one telling you this."
role: scheduler
access: [insider]
trust: real
situation: [confrontation, alone]
topic: [investigation]
mood: [concerned]
tags: [maret, detective-path, threshold-line]
notes: "Gate line. Opens when detective has built enough trust. What follows depends on the conversation state."
# ==========================================
# SIGN-OFF
# ==========================================
- id: the-terminal_d_318
text: "Manifest posted for the evening shift. I'm off at 1500."
role: scheduler
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [maret, closing]
- id: the-terminal_d_319
text: "Numbers don't lie. People lie about numbers."
role: scheduler
access: [peer]
trust: real
situation: [shift_end, routine]
topic: [routine]
mood: [concerned]
tags: [maret, closing, character-line]
notes: "Low-access line. Maret's professional philosophy. Can fire as ambient or as a closing beat."
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-terminal_d_320
text: "Bay assignments updated as of 0900. Check before you touch anything."
role: scheduler
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_321
text: "Inbound from the Havara corridor is delayed six hours. Adjust your bay queue."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [maret, operational, generation-pass]
- id: the-terminal_d_322
text: "Scan log for bay six is still open from last cycle. Close it before end of shift."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_323
text: "Overtime requests need scheduler approval now. Voss changed the process."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, institution]
mood: [comfortable]
tags: [maret, voss, administrative, generation-pass]
- id: the-terminal_d_324
text: "If a container's not logged by 1300, it doesn't make the cycle. That's the rule."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_325
text: "End-of-cycle reconciliation is at 1430. Be available."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [comfortable]
tags: [maret, procedural, generation-pass]
- id: the-terminal_d_326
text: "Weight tolerance is plus or minus five kilos. Anything outside that gets a flag."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [maret, procedural, manifest, generation-pass]
- id: the-terminal_d_327
text: "Commission audit prep starts next cycle. Get your documentation current."
role: scheduler
access: [public, peer]
trust: surface
situation: [routine]
topic: [institution, routine]
mood: [wary]
tags: [maret, commission, audit, generation-pass]
@@ -1,2 +1,356 @@
# TODO: Revise structure when automatic dialogue/monologue generators mature (D-028 Tier 2).
# Dialogue: shift-supervisor at The Terminal
# NPC: Voss (Tier 2, SYSTEM/OPERATOR)
# Voice: terse, procedural, institutional. Uses authority as armor.
# Ring-complicit: provides scheduling cover. Never shows it on surface.
# Ticket: #190 | Sprint: 12
location: the-terminal
role: shift-supervisor
lines:
# ==========================================
# GREETINGS — shift arrival
# ==========================================
- id: the-terminal_d_100
text: "You're late. Don't be late."
role: shift-supervisor
access: [public, peer, insider]
trust: surface
situation: [arrival, shift_start]
topic: [routine]
mood: [comfortable]
tags: [voss, greeting, authority]
- id: the-terminal_d_101
text: "Manifest's posted. Bay assignments are final — don't renegotiate them with me."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [voss, procedural]
- id: the-terminal_d_102
text: "Bay 4's running a review hold. Nobody touches that freight until I say."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [voss, authority]
dual_lens:
smuggler: "Review hold on Bay 4. Voss knows the container's in the queue. Is he covering it or flagging it?"
detective: "Supervisor flagging Bay 4 himself. Either due diligence or containment."
- id: the-terminal_d_103
text: "Commission liaison's in the district this cycle. Keep the floor clean."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [institution, routine]
mood: [concerned]
tags: [voss, warning, commission]
dual_lens:
smuggler: "Voss knows the detective's here. That warning's not just about tidiness."
detective: "Supervisor aware of my presence. Either routine caution or he's got something to protect."
# ==========================================
# SHIFT AUTHORITY — routine management
# ==========================================
- id: the-terminal_d_104
text: "Drin was supposed to run the bay six inspection. Tell him I'm asking."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, colleague]
mood: [comfortable]
tags: [voss, drin, authority]
- id: the-terminal_d_105
text: "Transition window's twenty minutes. Not twenty-five, not thirty. Twenty."
role: shift-supervisor
access: [public, peer, insider]
trust: surface
situation: [shift_transition]
topic: [routine]
mood: [comfortable]
tags: [voss, procedural, transition-window]
- id: the-terminal_d_106
text: "Overtime requests go through the Meridian form. Don't bring them to me."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine, shift_end]
topic: [routine]
mood: [comfortable]
tags: [voss, procedural]
- id: the-terminal_d_107
text: "Bay eight's going to downtime at 1300. Plan around it."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine]
mood: [comfortable]
tags: [voss, operational]
- id: the-terminal_d_108
text: "Maret flagged a discrepancy. I've reviewed it. It's logged. Move on."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine, investigation]
topic: [cargo, routine]
mood: [comfortable]
tags: [voss, maret, discrepancy, cover]
dual_lens:
smuggler: "Voss reviewed it and logged it. Not escalating. He's managing it."
detective: "Supervisor reviewed a manifest discrepancy and chose not to escalate. That's not nothing."
knowledge_grant:
fact_id: investigation.oversight_gap_pattern
confidence: suspects
- id: the-terminal_d_109
text: "Freight's moving. That's what matters. Questions slow things down."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine]
topic: [cargo, routine]
mood: [comfortable]
tags: [voss, deflection, philosophy]
# ==========================================
# SCHEDULING COVER — insider, real trust
# ==========================================
- id: the-terminal_d_110
text: "Bay four is clear for transition. I've adjusted the inspection schedule."
role: shift-supervisor
access: [insider]
trust: real
situation: [shift_transition]
topic: [cargo, routine]
mood: [focused]
tags: [voss, ring-ops, scheduling-cover]
dual_lens:
smuggler: "He adjusted the schedule. The window is actually clear. Voss is part of this."
detective: "Never hears this line."
- id: the-terminal_d_111
text: "Drin's on bay six. Bay four's mine tonight."
role: shift-supervisor
access: [insider]
trust: real
situation: [shift_transition, night_shift]
topic: [routine, cargo]
mood: [focused]
tags: [voss, ring-ops, scheduling-cover, drin]
- id: the-terminal_d_112
text: "The hold note is administrative. Don't treat it as anything else."
role: shift-supervisor
access: [insider]
trust: real
situation: [shift_transition]
topic: [cargo]
mood: [focused]
tags: [voss, ring-ops, coded]
notes: "Signals to ring members that the 'hold' on Bay 4 is cover, not genuine review."
# ==========================================
# DETECTIVE ENCOUNTER — authority tier
# ==========================================
- id: the-terminal_d_113
text: "You'll need to submit a formal access request through Commission channels for any restricted data."
role: shift-supervisor
access: [authority]
trust: surface
situation: [investigation, routine]
topic: [institution, routine]
mood: [comfortable]
tags: [voss, detective-path, institutional-resistance]
- id: the-terminal_d_114
text: "The manifest's available to any Commission officer. Ask the scheduler."
role: shift-supervisor
access: [authority]
trust: surface
situation: [investigation]
topic: [cargo, institution]
mood: [comfortable]
tags: [voss, detective-path, redirect]
- id: the-terminal_d_115
text: "My workers perform their duties. If there's an irregularity, it'll show in the log."
role: shift-supervisor
access: [authority]
trust: surface
situation: [investigation, confrontation]
topic: [routine, institution]
mood: [comfortable]
tags: [voss, detective-path, deflection]
- id: the-terminal_d_116
text: "I've been running this floor for six years. The numbers work out."
role: shift-supervisor
access: [authority, public]
trust: surface
situation: [investigation, confrontation]
topic: [routine, institution]
mood: [comfortable]
tags: [voss, detective-path, authority-assertion]
# ==========================================
# PRESSURE / CONFRONTATION
# ==========================================
- id: the-terminal_d_117
text: "Close the door."
role: shift-supervisor
access: [insider, peer]
trust: real
situation: [confrontation, alone]
topic: [danger]
mood: [concerned]
tags: [voss, confrontation, private]
- id: the-terminal_d_118
text: "Maret keeps flagging the discrepancies. That needs to stop being her problem."
role: shift-supervisor
access: [insider]
trust: real
situation: [confrontation, alone]
topic: [colleague, danger]
mood: [concerned]
tags: [voss, ring-ops, maret, caution]
knowledge_grant:
fact_id: behavioral.voss_awareness
confidence: knows_of
- id: the-terminal_d_119
text: "If you've got a problem with how I run this floor, that's a conversation for another time."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [confrontation]
topic: [routine]
mood: [concerned]
tags: [voss, deflection, shutdown]
# ==========================================
# SIGN-OFF / END OF SHIFT
# ==========================================
- id: the-terminal_d_120
text: "Sign-off is at 1400. Be done."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [voss, procedural]
- id: the-terminal_d_121
text: "Night shift takes over at 2200. I don't want to hear about the handoff tomorrow."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_transition, shift_end]
topic: [routine]
mood: [comfortable]
tags: [voss, procedural]
- id: the-terminal_d_122
text: "Good shift. Tomorrow at 0600."
role: shift-supervisor
access: [public, peer, insider]
trust: surface
situation: [shift_end]
topic: [routine]
mood: [comfortable]
tags: [voss, closing]
# ==========================================
# GENERATION PASS — ambient variants
# ==========================================
- id: the-terminal_d_123
text: "Assignments don't change once they're posted. Read the board."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_start, routine]
topic: [routine]
mood: [comfortable]
tags: [voss, procedural, generation-pass]
- id: the-terminal_d_124
text: "If there's a discrepancy, it goes to the scheduler. Not to me directly."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine, cargo]
mood: [comfortable]
tags: [voss, procedural, maret, generation-pass]
- id: the-terminal_d_125
text: "Span gate's cycling at 1100. Clear the bay six access before then."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [voss, operational, generation-pass]
- id: the-terminal_d_126
text: "I don't repeat myself on safety calls. If you missed it, ask someone who was listening."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [voss, authority, generation-pass]
- id: the-terminal_d_127
text: "End-of-day log goes to Meridian before you leave. No exceptions."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [shift_end, routine]
topic: [routine, institution]
mood: [comfortable]
tags: [voss, procedural, generation-pass]
- id: the-terminal_d_128
text: "Inbounds from Havara are on the revised schedule. Check the update."
role: shift-supervisor
access: [public, peer]
trust: surface
situation: [routine, shift_start]
topic: [routine, cargo]
mood: [comfortable]
tags: [voss, operational, generation-pass]
- id: the-terminal_d_129
text: "Clean floor, clean manifest. That's the job."
role: shift-supervisor
access: [public, peer, insider]
trust: surface
situation: [routine]
topic: [routine]
mood: [comfortable]
tags: [voss, philosophy, generation-pass]
@@ -0,0 +1,195 @@
# Environmental Text: Maintenance Corridors
# Signs, terminals, documents, readable objects.
# Investigation-heavy. This is where the operational evidence lives.
# Ticket: #262 | Sprint: 12
location: maintenance-corridors
items:
# ==========================================
# ACCESS RESTRICTION SIGN — corridor entrance
# ==========================================
- id: env_corridor_001
type: sign
label: "Restricted Access Notice"
visibility: always
text: >
RESTRICTED ACCESS — MAINTENANCE PERSONNEL ONLY
B-CORRIDOR SECTOR — AUTHORIZED ENTRY REQUIRED
Unauthorized entry subject to Sova Station Authority review.
Commission Order 7741-B applies.
Report unauthorized personnel to: Station Security (Lattice: SovaStation/Security)
dual_lens:
smuggler: "Nobody calls Station Security for corridor access. Everyone goes through here."
detective: >
Restricted zone, active posting. Unauthorized entry is a violation
of Commission Order 7741-B. If the ring is using this corridor,
they're committing a documented infraction on every run.
tags: [atmospheric, b7-corridor]
# ==========================================
# MERIDIAN COVERAGE SCHEDULE — posted on corridor wall
# Most investigation-relevant item in the game.
# ==========================================
- id: env_corridor_002
type: document
label: "Meridian Scan Schedule"
visibility: examine_only
text: >
MERIDIAN SURVEILLANCE COVERAGE — SOVA TRANSIT DISTRICT
MAINTENANCE CORRIDOR SECTOR — CURRENT CYCLE
Active coverage windows (full corridor monitoring):
06:00-07:30 | 10:00-11:00 | 14:00-15:30 | 22:00-23:00
Reduced coverage (perimeter only):
07:30-10:00 | 11:00-14:00 | 15:30-22:00 | 23:00-06:00
NOTE: Span gate maintenance cycles create signal interference.
Coverage gaps during gate calibration: approx 14:20-14:45.
Posted by: Sova Meridian Network Maintenance Division
dual_lens:
smuggler: >
14:20-14:45. That's the window. Twenty-five minutes of clean dead zone
during the shift transition. The ring didn't find this. It's posted.
detective: >
Posted surveillance schedule with documented gap. 14:20-14:45 during
shift transition. The ring isn't sophisticated — they're using
publicly posted maintenance data. That means anyone could have
identified this gap.
tags: [investigation-relevant, meridian, surveillance-gap, critical]
knowledge_grant:
fact_id: "location.surveillance_gaps"
confidence: knows_details
# ==========================================
# MAINTENANCE LOG SHEET — handwritten clipboard at corridor junction
# ==========================================
- id: env_corridor_003
type: document
label: "Maintenance Log (B-7 Corridor)"
visibility: examine_only
text: >
B-7 CORRIDOR — MAINTENANCE LOG
Logged by: Pael Varren
[Month 11, Day 4]
Air scrubber pressure variance. Reset. Source unclear.
[Month 11, Day 12]
Seal failure B-7 junction. Third occurrence. Replaced again.
Noted: floor wear pattern inconsistent with posted traffic.
Possible deferred maintenance backlog creating additional load.
[Month 11, Day 22]
Recurring seal failure. Escalated to deferred queue.
NOTE: Pressure sensors reading elevated load during 14:00-15:00
window. Does not match shift transition foot traffic.
Filed maintenance anomaly report — awaiting management review.
dual_lens:
smuggler: >
Pael's logged the pressure anomaly. It'll sit in the queue.
Management never reviews the anomaly reports.
detective: >
Maintenance anomaly report filed, awaiting management review.
The report documents irregular pressure load during exact operation
window. If management reviews it, the window closes.
tags: [investigation-relevant, b7-corridor, pael, critical]
knowledge_grant:
fact_id: "location.corridor_b7_restricted"
confidence: knows_of
# ==========================================
# ROUTING DIAGRAM — cargo flow map posted at junction
# ==========================================
- id: env_corridor_004
type: sign
label: "Cargo Routing Diagram"
visibility: always
text: >
SOVA TRANSIT — MAINTENANCE CORRIDOR ROUTING
[Technical diagram showing freight flow paths]
Standard freight path: Bay > Terminal > Surface Storage
Emergency reroute: Bay > B-corridor > Temporary Hold (C-4)
Maintenance access: B-corridor > Service Lift > Environmental Systems
[Someone has drawn a thin pencil line from Bay 4 to C-4 to
the service lift, bypassing the standard terminal route]
dual_lens:
smuggler: "Someone mapped the reroute. That's not how we were shown it. That's cleaner."
detective: >
Pencil annotation on the official diagram. Whoever added it knows
the layout and took time to trace an alternative route. Not vandalism.
Operational planning.
tags: [investigation-relevant, b7-corridor, smuggling-route]
knowledge_grant:
fact_id: "location.smuggling_route"
confidence: suspects
# ==========================================
# ENVIRONMENTAL WARNING — recycled air quality
# ==========================================
- id: env_corridor_005
type: sign
label: "Environmental Quality Notice"
visibility: always
text: >
CORRIDOR ENVIRONMENTAL STATUS — CURRENT READINGS
CO2: 0.12% [ACCEPTABLE — upper threshold 0.15%]
Particulate: ELEVATED — see maintenance queue
Temperature: 19.4°C [nominal]
Recycled air freshness: 73% [minimum recommended: 80%]
If recycled air quality falls below 70%, report to maintenance.
Current status: Monitor.
dual_lens:
smuggler: "Recycled air's worse than the terminal. You get used to it."
detective: "Air quality below recommended. Maintenance deferred. Nobody's prioritizing this corridor."
tags: [atmospheric, environmental]
# ==========================================
# TEMPORARY STORAGE MANIFEST SHEET
# At the C-4 temporary hold point
# ==========================================
- id: env_corridor_006
type: document
label: "Temporary Storage Log (C-4)"
visibility: examine_only
text: >
C-4 TEMPORARY HOLD — CONTENTS LOG
[Printed manifest, partial, two pages visible]
Container 4466 — Cleared — removed Month 11 Day 1
Container 4467 — Cleared — removed Month 11 Day 3
Container 4468 — Cleared — removed Month 11 Day 3
Container 4469 — [NO ENTRY]
Container 4470 — Cleared — removed Month 11 Day 8
Container 4471 — [PENDING — entered Month 11 Day 15, no removal logged]
Container 4472 — Cleared — removed Month 11 Day 18
[Gap in numbering between 4469-4470 has no explanation column]
dual_lens:
smuggler: >
4471 is still logged as pending. It should have been moved during
last transition. Either it wasn't, or the log wasn't updated.
Either is a problem.
detective: >
Container 4471 in the C-4 hold log, entered Month 11, no removal
logged. Same container flagged in the terminal manifest. And 4469
is a gap with no explanation. This is the thread.
tags: [investigation-relevant, container-4471, critical]
knowledge_grant:
fact_id: "investigation.cargo_anomaly"
confidence: knows_of
@@ -0,0 +1,209 @@
# Environmental Text: The Last Shift (Bar)
# Signs, terminals, documents, readable objects.
# Atmosphere-heavy. Some investigation relevance through social texture.
# Ticket: #262 | Sprint: 12
location: the-last-shift
items:
# ==========================================
# DRINK MENU — handwritten, scratched-out prices
# Above the bar. Visible to anyone.
# ==========================================
- id: env_bar_001
type: sign
label: "Drink Menu"
visibility: always
text: >
THE LAST SHIFT
(Lera's)
Krenn grain spirit — 4c
Pale station brew — 3c ~~2c~~
Dark station brew — 3c
Import (off-station) — 8c
Sova standard water — 1c
Meal (hot, whatever's running) — 6c
No tabs. No Commission business at the bar.
House rules posted.
dual_lens:
smuggler: "Import's doubled since last year. Everything's tighter."
detective: >
"No Commission business at the bar." That's Lera's personal policy.
She'll have opinions about why.
tags: [atmospheric]
# ==========================================
# LERA'S RULES — above the bar
# ==========================================
- id: env_bar_002
type: sign
label: "House Rules"
visibility: always
text: >
LERA'S — HOUSE RULES
1. Pay before you're too drunk to remember.
2. Arguments outside. Always.
3. What's said at the bar stays at the bar.
4. No job talk after 20:00.
5. Torek Lintar runs his own tab. Don't ask.
dual_lens:
smuggler: "Rule 5. Lera's been running Torek's tab for years. Old arrangement."
detective: >
Rule 5 is specific. Named. Torek Lintar has a standing arrangement
here. That's the third time his name has come up without context.
tags: [investigation-relevant, torek-lintar, lera]
knowledge_grant:
fact_id: "relationship.torek_bar_arrangement"
confidence: suspects
# ==========================================
# NOTICE BOARD — general community notices
# Examine to read individual notices.
# ==========================================
- id: env_bar_003
type: readable_object
label: "Notice Board"
visibility: always
text: >
[NOTICE BOARD — various papers pinned over each other]
MISSING: Vessels Tamm, last seen Sova Transit District, Month 11.
Contact: Lattice Commission Sova liaison or speak to Lera.
CARGO WORKERS' ASSOCIATION — Monthly meeting, back room, Month 12,
evening shift. All Sova Transit workers welcome. Bring your own.
ROOM AVAILABLE: Residential pod B-3, lower level. Quiet. Short-term.
Speak to Harek at the bar.
[handwritten, pinned over the others:]
ANYONE KNOW WHO LEFT A TRANSIT BAG IN THE STORAGE ROOM?
IT'S BEEN THREE WEEKS.
dual_lens:
smuggler: >
Vessels Tamm. Missing person notice. That name's from the Tamm family —
Naia's connection? Or coincidence. Probably coincidence.
detective: >
Missing persons notice. Vessels Tamm. Sova Transit District. Cross-reference
Tamm with case file — possible Naia Tamm relation. Flag for follow-up.
tags: [investigation-relevant, naia-tamm-adjacent, harek]
knowledge_grant:
fact_id: "relationship.tamm_family"
confidence: suspects
# ==========================================
# GRAFFITI — bathroom wall
# Examine to read.
# ==========================================
- id: env_bar_004
type: readable_object
label: "Graffiti (Bathroom Wall)"
visibility: examine_only
text: >
[scratched into the wall surface over several years]
Voss doesn't check bay 4. Never has. Never will.
The Commission sees what it wants to see.
Lera knows.
[someone added in different handwriting:]
Lera doesn't know anything. She just sells drinks.
[original hand, below:]
Keep telling yourself that.
dual_lens:
smuggler: "Graffiti's been here since before I started. The Voss line is right, though."
detective: >
"Lera knows." Someone believes the bar owner has information.
Someone else pushes back. The debate is years old.
tags: [atmospheric, voss, lera, investigation-adjacent]
# ==========================================
# TONIGHT'S SPECIALS — chalkboard near the kitchen
# ==========================================
- id: env_bar_005
type: sign
label: "Tonight's Specials"
visibility: always
text: >
TONIGHT:
Hot grain porridge (Velen style) — 5c
Station fish fry — 7c
Ask Lera what Velen style means.
She'll tell you about it for twenty minutes.
dual_lens:
smuggler: "Lera's from Velen originally. She makes everyone ask."
detective: "Lera Sessik. Originally from Velen surface. Personal detail noted."
tags: [atmospheric, lera-backstory]
# ==========================================
# LATTICE-FREE ZONE SIGN — near the entrance
# ==========================================
- id: env_bar_006
type: sign
label: "Lattice-Free Zone Notice"
visibility: always
text: >
VOLUNTARY LATTICE-FREE ZONE
By Lera's request: please disable active lattice scanning
and recording functions while at the bar.
Conversation here is conversation. Not data.
This is not a Commission-recognized designation.
Lera doesn't care.
dual_lens:
smuggler: >
Everyone honors this. It's one of the reasons the bar works for
what it works for.
detective: >
Not legally enforceable. But anyone who honors it voluntarily is
actively choosing to reduce their surveillance footprint here.
Who honors it and who doesn't is worth noting.
tags: [investigation-relevant, lera, lattice-free]
# ==========================================
# CARGO WORKERS' ASSOCIATION FLYER — pinned to the bar
# ==========================================
- id: env_bar_007
type: document
label: "Association Meeting Flyer"
visibility: examine_only
text: >
SOVA TRANSIT CARGO WORKERS' ASSOCIATION
Monthly Meeting
Topic: Shift change protocols — the thirty-minute handoff window
and why it isn't long enough.
Also on agenda: Updated manifest compliance requirements from
the Commission (new reporting window: 6 hours, down from 8).
Back room. Evening shift. All welcome.
Bring your concerns, bring a drink.
dual_lens:
smuggler: >
Commission shortened the reporting window to six hours. That's
tighter than the eight-hour buffer. Someone noticed the gap.
detective: >
Agenda item: thirty-minute handoff window. Workers complaining
about it publicly means the window is known beyond the ring.
Or is being discussed for reasons that are less innocent.
tags: [investigation-relevant, shift-transition, commission]
knowledge_grant:
fact_id: "world.shift_schedule"
confidence: knows_of
@@ -0,0 +1,199 @@
# Environmental Text: The Terminal (Logistics Hub)
# Signs, terminals, documents, readable objects.
# Both characters can examine these. Interpretation differs.
# Ticket: #262 | Sprint: 12
location: the-terminal
items:
# ==========================================
# SHIFT BOARD — posted schedule
# Always visible. High-traffic reference point.
# ==========================================
- id: env_terminal_001
type: sign
label: "Shift Schedule Board"
visibility: always
text: >
SOVA TRANSIT DISTRICT — LOGISTICS HUB
CURRENT ROTATION — CYCLE 47
BAY 1-4: Day Shift 06:00-14:00 (Voss, K. — Supervisor)
BAY 5-8: Day Shift 06:00-14:00 (Maret, K. — Scheduler)
TRANSITION: 13:45-14:15 (30-min handoff window)
Night Shift 22:00-06:00 — ROSTER PENDING
[Bay 6: MAINTENANCE HOLD — see Pael Varren]
dual_lens:
smuggler: "Thirty-minute transition window. That's the window. Posted for everyone to see."
detective: "Night roster blank. Either unassigned or deliberately obscured. Logging the gap."
# ==========================================
# MANIFEST TRACKING TERMINAL — cargo status display
# Active terminal. Can be read from appropriate distance in investigation mode.
# ==========================================
- id: env_terminal_002
type: terminal
label: "Cargo Tracking Terminal"
visibility: always
text: >
ACTIVE MANIFEST QUEUE — SOVA LOGISTICS
Last updated: 06:23
Container 4471 — [PENDING REVIEW] — Bay 4
Container 4472 — CLEARED — Bay 2
Container 4473 — CLEARED — Bay 2
Container 4474 — [WEIGHT DISCREPANCY — 47kg] — Bay 4
Container 4475 — CLEARED — Bay 3
Container 4476 — PROCESSING — Bay 7
[8 additional items — scroll to continue]
dual_lens:
smuggler: "4471 is still in the queue. Kael hasn't cleared it yet. On schedule."
detective: >
4471 pending review with a 4474 weight discrepancy in the same bay.
Two anomalies, same location. Not random.
tags: [investigation-relevant, manifest-discrepancy]
knowledge_grant:
fact_id: "investigation.manifest_discrepancy"
confidence: suspects
# ==========================================
# MAINTENANCE REQUEST CLIPBOARD
# Physical clipboard near loading arm 3. Examine to read.
# ==========================================
- id: env_terminal_003
type: document
label: "Maintenance Request Log"
visibility: examine_only
text: >
DEFERRED MAINTENANCE — CURRENT QUEUE
(Submitted: Pael Varren, Maintenance)
Loading arm 3 — Grinding during lift cycle — DEFERRED x4
Bay 6 environmental seal — Recurring failure — DEFERRED x3 (HOLD)
B-7 corridor air scrubber — Pressure variance — DEFERRED x1
Gate calibration bay 5-6 — Off-spec hum — DEFERRED x2
Recycling unit C-4 — Thermal spike — DEFERRED x1
Note: B-7 corridor showing irregular pressure load on sensor.
Traffic heavier than logged shift count explains. Investigating.
— P. Varren
dual_lens:
smuggler: "Pael's noticed the B-7 pressure log doesn't match posted traffic. Nothing they can do about it."
detective: >
Maintenance log records irregular pressure load in B-7.
The tech attributes it to unlogged foot traffic. That's exactly right.
tags: [investigation-relevant, b7-corridor]
knowledge_grant:
fact_id: "location.corridor_b7_restricted"
confidence: suspects
# ==========================================
# COMMISSION COMPLIANCE NOTICE
# Standard posting. Required by regulation. Two years old.
# ==========================================
- id: env_terminal_004
type: sign
label: "Commission Compliance Notice"
visibility: always
text: >
LATTICE COMMISSION — FREIGHT OPERATIONS COMPLIANCE
All manifest discrepancies exceeding 15kg must be reported
within 8 hours of discovery.
All cargo flagged PENDING REVIEW must be cleared by assigned
inspector before shift transition.
Unauthorized access to restricted maintenance corridors
will result in immediate suspension pending review.
Posted: Lattice Commission, Sova Station Authority
Reference: Commission Order 7741-B
dual_lens:
smuggler: >
Eight-hour window on discrepancy reporting. In practice, the window
to move anything is the thirty-minute shift transition.
detective: >
8-hour reporting window. If discrepancy 4474 was logged at 06:23,
compliance deadline is 14:23. I have time.
tags: [atmospheric, institutional]
# ==========================================
# WORK ORDER NOTE — handwritten, coffee-stained
# Personal communication between dock workers. Examine to read.
# ==========================================
- id: env_terminal_005
type: document
label: "Handwritten Note (Coffee-Stained)"
visibility: examine_only
text: >
Kael —
Bay 4 queue backed up again. Maret says it's a weight issue
but she's not flagging it officially. If it's still there when
you come in, just route it through 7 like last time. Voss
won't check.
— D.
dual_lens:
smuggler: >
"D." is Drin. Route it through 7 like last time — dock-side
workaround, happens constantly. Probably nothing operational.
detective: >
Unsigned note, initial D. Maret not flagging a weight discrepancy
officially. Voss complicit or uninformed. Follow up with Maret.
tags: [investigation-relevant, maret, drin, voss]
knowledge_grant:
fact_id: "investigation.drin_inspection_pattern"
confidence: suspects
# ==========================================
# SAFETY PROCEDURES PLACARD — standard, faded
# ==========================================
- id: env_terminal_006
type: sign
label: "Emergency Procedures"
visibility: always
text: >
IN CASE OF EMERGENCY:
1. Activate hull breach alarm (red panel, every 20m)
2. Proceed to nearest designated shelter (yellow markers)
3. Do not use freight lifts during emergency protocols
4. Await station authority clearance before resuming operations
Last safety review: Month 4, Year 312 (Krenn calendar)
dual_lens:
smuggler: "Two years out of date. Nobody bothers."
detective: "Last review was two cycles ago. Standard compliance lag for a district this size."
tags: [atmospheric]
# ==========================================
# BAY STATUS BOARD — current bay availability
# ==========================================
- id: env_terminal_007
type: sign
label: "Bay Status Board"
visibility: always
text: >
BAY STATUS — CURRENT CYCLE
BAY 1: ACTIVE
BAY 2: ACTIVE
BAY 3: ACTIVE
BAY 4: ACTIVE — PENDING REVIEW (2 items)
BAY 5: ACTIVE
BAY 6: MAINTENANCE HOLD — Est. clear: next cycle
BAY 7: ACTIVE
BAY 8: SCHEDULED DOWNTIME 13:00-15:00
dual_lens:
smuggler: "Bay 8 scheduled downtime 13:00-15:00. That overlaps the transition window. Useful."
detective: "Bay 8 downtime during transition window. Reduced oversight at peak movement time."
tags: [investigation-relevant, operational]
@@ -0,0 +1,225 @@
# Diegetic Insert Flavor Text — Detective Character
# Neural lattice interface strings: POI labels, notifications, system status.
# Ticket: #331 | Sprint: 12 | Owner: Mellanie + Araminta
#
# Voice principle: the detective's insert is issued Commission equipment.
# Institutional, precise, surname-first. The vocabulary is the job.
# When personal breaks through, it's notable — the first name appears
# only for allies. Shift to first name IS a relationship marker.
character: detective
# ==========================================
# POI LABELS — displayed in perception mode overlay
# Appears on NPCs when viewed through insert interface.
# State-dependent: shifts as relationship changes.
# ==========================================
poi_labels:
defaults:
unknown: "Unknown subject"
known: "{surname}, {initial}."
friendly: "{first_name}"
person_of_interest: "{surname} — POI"
hostile: "{surname} — AVOID"
npc_overrides:
kael_davan:
phase_1_unknown: "Unknown subject"
phase_1_known: "Davan, K."
person_of_interest: "Davan, K. — POI"
flagged: "Davan, K. — FLAGGED"
sera_venn:
friendly: "Sera"
phase_3_poi: "Sera — ?"
phase_4_poi: "Venn — REVIEW"
voss:
default: "Voss, K. — Supervisor"
cooperative: "Voss — cooperative"
torek_lintar:
default: "Lintar, T."
flagged: "Lintar, T. — FLAG"
commission_personnel:
default: "{surname} — Commission"
ring_member_suspected:
default: "{surname} — suspect"
# ==========================================
# NOTIFICATIONS — status strings, message previews
# Displayed as brief overlay text. 40 char max per line.
# ==========================================
notifications:
case_file:
- id: insert_d_001
trigger: case_file_update
template: "Case update: {subject_name}"
example: "Case update: Davan, K."
tags: [investigation]
- id: insert_d_002
trigger: evidence_logged
text: "Evidence logged"
tags: [investigation]
- id: insert_d_003
trigger: manifest_discrepancy_filed
text: "Discrepancy filed"
tags: [investigation, manifest]
- id: insert_d_004
trigger: knowledge_updated
template: "KG updated: {fact_category}"
example: "KG updated: relationship"
tags: [investigation]
- id: insert_d_005
trigger: poi_status_change
template: "{surname} — status updated"
example: "Davan — status updated"
tags: [investigation]
- id: insert_d_006
trigger: tell_logged
template: "{surname} — behavior logged"
example: "Davan — behavior logged"
tags: [investigation, tell]
- id: insert_d_007
trigger: pattern_confirmed
template: "{surname} — pattern confirmed"
example: "Venn — pattern confirmed"
tags: [investigation, pattern]
commission:
- id: insert_d_008
trigger: commission_advisory
text: "Commission advisory"
tags: [institutional]
- id: insert_d_009
trigger: commission_advisory_urgent
text: "Commission advisory — urgent"
tags: [institutional, priority]
- id: insert_d_010
trigger: clearance_required
template: "{zone} — clearance required"
example: "Sector B — clearance required"
tags: [institutional]
- id: insert_d_011
trigger: clearance_confirmed
template: "{zone} — clearance confirmed"
example: "B-7 — clearance confirmed"
tags: [institutional]
- id: insert_d_012
trigger: case_status_active
template: "Case #{case_id}: active"
example: "Case #4471: active"
tags: [institutional, investigation]
field:
- id: insert_d_013
trigger: observation_mode_active
text: "Observation: logging"
tags: [field]
- id: insert_d_014
trigger: restricted_area_entered
text: "Restricted zone — logged"
tags: [field, institutional]
- id: insert_d_015
trigger: anomaly_detected
template: "Anomaly: {location}"
example: "Anomaly: B-7 corridor"
tags: [field, investigation]
- id: insert_d_016
trigger: no_anomaly
text: "Sector: nominal"
tags: [field]
# ==========================================
# SYSTEM STATUS — persistent interface text
# Small-print labels in perception mode corners.
# ==========================================
system_status:
- id: insert_d_100
context: default
text: "Analytical lattice: active"
- id: insert_d_101
context: perception_mode_active
text: "Perception mode: on"
- id: insert_d_102
context: fog_layer_present
text: "Reduced visibility"
- id: insert_d_103
context: meridian_active
text: "Meridian: nominal"
- id: insert_d_104
context: meridian_inactive
text: "Meridian: offline"
- id: insert_d_105
context: investigation_active
text: "Case: active"
- id: insert_d_106
context: case_status_flagged
text: "Case: flagged subjects present"
- id: insert_d_107
context: case_status_critical
text: "Case: escalation threshold"
- id: insert_d_108
context: lattice_scan_complete
text: "Scan: complete"
# ==========================================
# EXAMINATION LABELS — text shown when examining environmental objects
# ==========================================
examination_labels:
- id: insert_d_200
object_type: document
text: "Examine"
- id: insert_d_201
object_type: terminal
text: "Query manifest"
- id: insert_d_202
object_type: container_flagged
text: "Container 4471 — discrepancy logged"
- id: insert_d_203
object_type: restricted_area
text: "Restricted — B-7 sector"
- id: insert_d_204
object_type: surveillance_gap
text: "Coverage gap — logged"
- id: insert_d_205
object_type: evidence_item
text: "Log as evidence"
- id: insert_d_206
object_type: known_meeting_point
text: "Meeting point — flagged"
notes: >
Voice: institutional, precise, Commission equipment. Surname-first for
targets and unknowns. First name appears only for allies (Sera).
That shift IS the relationship marker — the player notices when
"Venn" becomes "Sera" and knows it means something.
Araminta coordinates on visual presentation constraints (#331 co-owner).
@@ -0,0 +1,193 @@
# Diegetic Insert Flavor Text — Smuggler Character
# Neural lattice interface strings: POI labels, notifications, system status.
# Ticket: #331 | Sprint: 12 | Owner: Mellanie + Araminta
#
# Voice principle: the smuggler's insert is slightly informal, modified hardware.
# It's a tool that's been lived with, not issued equipment. Short labels.
# Uses first names and operational shorthand, not institutional vocabulary.
character: smuggler
# ==========================================
# POI LABELS — displayed in perception mode overlay
# Appears on NPCs when viewed through insert interface.
# State-dependent: shifts as relationship changes.
# ==========================================
poi_labels:
defaults:
unknown: "Unknown"
known: "{first_name}"
friendly: "{first_name} — OK"
person_of_interest: "{first_name} — WATCH"
hostile: "{first_name} — AVOID"
npc_overrides:
kael_davan:
phase_1: "Kael"
phase_2: "Kael — ?"
phase_3: "Kael — WATCH"
phase_4: "Kael — LYING"
phase_5: "Kael — COMPROMISED"
voss:
default: "Voss"
hostile: "Voss — PROBLEM"
commission_personnel:
default: "Commission"
investigation_active: "Commission — ACTIVE"
ring_member:
default: "{first_name}"
operational: "{first_name} — RING"
# ==========================================
# NOTIFICATIONS — status strings, message previews
# Displayed as brief overlay text. 40 char max per line.
# ==========================================
notifications:
messages:
- id: insert_s_001
trigger: message_from_contact
template: "Msg: {contact_name}"
example: "Msg: Kael"
tags: [ring-operational]
- id: insert_s_002
trigger: message_from_contact_urgent
template: "Msg: {contact_name} — urgent"
example: "Msg: Nils — urgent"
tags: [ring-operational, priority]
- id: insert_s_003
trigger: drop_point_update
text: "Drop point updated"
tags: [ring-operational]
- id: insert_s_004
trigger: drop_point_confirmed
text: "Drop point confirmed — proceed"
tags: [ring-operational]
- id: insert_s_005
trigger: meridian_window_opening
template: "Window: {duration}m"
example: "Window: 25m"
tags: [operational, timing]
- id: insert_s_006
trigger: meridian_window_closing
template: "Window closing — {time_remaining}m"
example: "Window closing — 3m"
tags: [operational, timing, priority]
- id: insert_s_007
trigger: ring_checkin_overdue
text: "Check-in overdue"
tags: [ring-operational, concern]
- id: insert_s_008
trigger: contact_unavailable
template: "{contact_name} — no response"
example: "Kael — no response"
tags: [ring-operational, concern]
- id: insert_s_009
trigger: commission_patrol_detected
text: "Commission in sector"
tags: [caution, operational]
- id: insert_s_010
trigger: commission_patrol_cleared
text: "Sector clear"
tags: [operational]
evidence:
- id: insert_s_011
trigger: observation_logged
text: "Logged"
tags: [observation]
- id: insert_s_012
trigger: tell_observed
template: "{npc_name} — tell logged"
example: "Kael — tell logged"
tags: [observation, tell]
- id: insert_s_013
trigger: contradiction_observed
template: "{npc_name} — anomaly"
example: "Kael — anomaly"
tags: [observation, contradiction]
# ==========================================
# SYSTEM STATUS — persistent interface text
# Small-print labels in perception mode corners.
# ==========================================
system_status:
- id: insert_s_100
context: default
text: "Lattice: Sova Transit"
- id: insert_s_101
context: perception_mode_active
text: "Perception: active"
- id: insert_s_102
context: fog_layer_present
text: "Fog layer"
- id: insert_s_103
context: meridian_dead_zone
text: "Meridian: inactive"
- id: insert_s_104
context: meridian_active
text: "Meridian: active — caution"
- id: insert_s_105
context: ring_status_stable
text: "Status: stable"
- id: insert_s_106
context: ring_status_elevated
text: "Status: elevated"
- id: insert_s_107
context: ring_status_compromised
text: "Status: compromised"
# ==========================================
# EXAMINATION LABELS — text shown when examining environmental objects
# ==========================================
examination_labels:
- id: insert_s_200
object_type: document
text: "Read"
- id: insert_s_201
object_type: terminal
text: "Check manifest"
- id: insert_s_202
object_type: container_flagged
text: "4471 — flagged"
- id: insert_s_203
object_type: restricted_area
text: "B-7 — restricted"
- id: insert_s_204
object_type: dead_drop
text: "Drop point"
- id: insert_s_205
object_type: safe_location
text: "Safe"
notes: >
Voice: informal, operational shorthand. This is a modified civilian
lattice, not issued equipment. First names, not titles. Short.
Labels should feel like something you'd put on your own interface.
Araminta coordinates on visual presentation constraints (#331 co-owner).
@@ -0,0 +1,62 @@
# NPC Profile: Pael Varren
# Tier 3 — NOBODY / CIVILIAN
# Flat NPC — noise floor. Knows nothing about the ring.
# Observable presence near B-corridor is legitimate maintenance, not surveillance.
# Ticket: #307 | Sprint: 12
canonical_id: "npc:pael"
display_name: "Pael Varren"
tier: 3
pattern: "NOBODY"
motivation: "CIVILIAN"
description: >
Station maintenance tech, early 40s. Has worked Sova Transit District for
fifteen years. Knows every duct, every failing seal, every air recycler
running off-spec. Appears throughout the district because maintenance is
everywhere. Carries a scanner everywhere; hands are perpetually stained with
lubricant. Pael has no ring connection, no secrets beyond a quiet resentment
of deferred maintenance tickets. Is frequently near B-corridor because the
environmental seal there fails on a cycle of roughly three weeks, without
fail, regardless of what Pael does about it.
want:
primary: "Happiness"
description: >
To get through a single shift without a new deferred ticket appearing on
the queue. Fifteen years of Sova has narrowed the definition of a good day.
routine:
summary: >
Arrives before any shift (05:30 entry), does a diagnostic walk of the
full district, starts on the highest-priority deferred tickets during
low-traffic windows. Reappears at the B-corridor junction around mid-shift
because the environmental seal there is always the problem. Leaves when
the shift cycle turns over. Pattern is completely predictable. That's the
point — Pael is never where they shouldn't be, they're just always where
the problems are.
personality:
demeanor: "tired-competent, dry humor, no interest in drama"
social_style: "minimal — answers when addressed, doesn't linger"
stress_response: "files a maintenance ticket and moves on"
tells: []
skills:
combat_trained: false
skills:
maintenance: 9
environmental_systems: 8
trust_levels:
surface: >
Fifteen years of Sova maintenance. "If it breaks, I've seen it break
before. Usually the same three things."
notes: >
NOBODY + CIVILIAN: pure noise floor. The "seems important but isn't" quality
is positional — frequent presence near B-corridor for genuine maintenance
reasons. One memorable observable trait: scanner always open, lubricant
stains on both hands even at shift start. Paula owns the trait design.
Mellanie owns the lines (#307).
@@ -0,0 +1,63 @@
# NPC Profile: Ren Tosse
# Tier 3 — NOBODY / CIVILIAN
# Flat NPC — noise floor. Drifter with no ring connection.
# Asks too many questions about cargo schedules. Is just looking for work.
# Ticket: #307 | Sprint: 12
canonical_id: "npc:ren"
display_name: "Ren Tosse"
tier: 3
pattern: "NOBODY"
motivation: "CIVILIAN"
description: >
Day worker, early 30s. Comes through Sova Transit on irregular rotations —
picks up freight work when it's available, moves on when it isn't. Knows the
station the way drifters do: which bays are short-handed, which supervisors
ask fewer questions, what the bar's cheap nights are. Has a worn transit bag
and a slightly too-casual awareness of exits — not operational awareness,
just the low-level situational alertness of someone who has moved through
too many unfamiliar stations. Genuinely unremarkable. No ring connection,
no secrets. Asks too many questions about night rotations and cargo schedules
because that's how you find short-handed bays.
want:
primary: "Wealth"
description: >
Regular work. Enough to stop moving for a while. Sova's a decent stop —
the freight work is steady when it comes through.
routine:
summary: >
Arrives after 10:00 when the day shift is settled. Checks the job board
at the terminal entrance. Wanders to The Last Shift for a drink and
whatever bar conversation yields useful shift information. Leaves by late
afternoon unless day work came through. Pattern is irregular by design —
no fixed schedule because there's no fixed role.
personality:
demeanor: "casual, observant, asks questions as if entitled to answers"
social_style: "sociable enough to get information, quiet when there's nothing useful"
stress_response: "moves on. There's always another station."
tells: []
skills:
combat_trained: false
skills:
cargo_handling: 6
freight_routing: 5
trust_levels:
surface: >
Passing through, looking for work. "Sova seems decent. Depends on
the freight situation."
notes: >
NOBODY + CIVILIAN: pure noise floor. The "seems important but isn't" quality
comes from drifter behavior patterns that mimic operative reconnaissance:
asking about night rotations, checking cargo schedules, casual interest in
specific bays. All genuine job-seeking. One memorable observable trait:
worn transit bag with tags from three different station systems, slightly
too-casual awareness of exits. Paula owns the trait design. Mellanie owns
the lines (#307).
@@ -0,0 +1,58 @@
# NPC Profile: Tev Osel
# Tier 3 — NOBODY / CIVILIAN
# Flat NPC — noise floor. Stands like a lookout. Is waiting for his partner.
# No ring connection. The resemblance to surveillance posture is accidental.
# Ticket: #307 | Sprint: 12
canonical_id: "npc:tev"
display_name: "Tev Osel"
tier: 3
pattern: "NOBODY"
motivation: "CIVILIAN"
description: >
Cargo staging worker, late 20s. Has an unfortunate habit of positioning
himself exactly where someone running corridor surveillance would stand —
at junctions, in doorways, facing the approach. This is where he waits for
Kosse, his partner, who works the late freight bay and takes the same route
back through the maintenance corridors. Tev times his breaks to the late
route schedule. He has no idea how this looks to someone tracking corridor
traffic. No ring connection. No secrets.
want:
primary: "Connection"
description: >
For Kosse to finish shift on time for once. It never happens on time.
routine:
summary: >
Works cargo staging 06:00-14:00. During break windows, drifts to corridor
junction C-4 to wait for Kosse on the late freight route home. Returns to
staging when Kosse doesn't appear within the break window. After shift:
walks home with Kosse when schedules align, alone otherwise. The junction
positioning is consistent and timing-predictable.
personality:
demeanor: "patient, unassuming, mildly distracted when waiting"
social_style: "polite if addressed, slightly confused why anyone is talking to him"
stress_response: "checks lattice. Waits longer."
tells: []
skills:
combat_trained: false
skills:
cargo_staging: 6
trust_levels:
surface: >
Waiting for someone. "Kosse's running late again. You know how the freight
bay is."
notes: >
NOBODY + CIVILIAN: pure noise floor. The "seems important but isn't" quality
is entirely positional — standing at corridor junctions, watching approach
traffic, checking the time, stationing himself in threshold positions. All
of this is to meet a partner after shift. One memorable observable trait:
weight always on the wrong foot, perpetually glancing the same direction.
Paula owns the trait design. Mellanie owns the lines (#307).
@@ -1,6 +1,9 @@
# News ticker headline pool — The Last Shift bar
# Meridian Feed (Krenn System local news network)
# 30 lines total | Categories: freight (9), politics (4), infrastructure (5), sports (3),
# commission (5), community (4) — see dual_lens notes on each line
# Dual-lens content: each headline reads differently to smuggler vs detective.
# Ticket: #306 | Sprint: 12 | Owner: Mellanie + Miri
# v0.1: static background text. v0.2: cycling ticker system.
location: the-last-shift
@@ -155,3 +158,85 @@ headlines:
dual_lens:
smuggler: "'Routine assessment.' Is that the detective? Is that why he's here?"
detective: "That's my cover story. Or headquarters' way of telling me the clock is running."
# --- Local Community (4) — added Sprint 12, Mellanie ---
- id: ticker_021
text: "SECTOR 3: Sova Station Admin acknowledges ventilation review — timeline 'under discussion'"
category: infrastructure
dual_lens:
smuggler: "Under discussion. Same thing they said last cycle. Seven years of under discussion."
detective: "Deferred maintenance, seven-year backlog, workers frustrated. That's a community with grievances. File that."
- id: ticker_022
text: "WORKERS: Cargo Workers' Association calls for review of thirty-minute shift handoff protocols"
category: community
dual_lens:
smuggler: "Workers pushing back on the transition window. If they shorten it, we lose our gap."
detective: "Workers know the window is too short. And they're right. The question is who else knows it."
- id: ticker_023
text: "SOVA LOCAL: Missing persons — Vessels Tamm, Sova Transit District, Month 11. Contact Sova liaison."
category: community
dual_lens:
smuggler: "Vessels Tamm. That name — any connection to Naia's family? Don't chase ghosts."
detective: "Tamm. Naia Tamm is the name in the case file as Kael Davan's partner. Related? Flag."
notes: >
Connects to notice board in the-last-shift environmental text (env_bar_003).
Detective's reaction sets up the Naia-Tamm connection thread.
- id: ticker_024
text: "HEALTH: Re-embodiment wait times at Sova Clinic — non-emergency cases: 4-6 cycles. Main station for urgent."
category: community
dual_lens:
smuggler: "Four cycles. If something goes wrong on a run, four cycles at the clinic before you're back. Remember that."
detective: "Healthcare routing. Non-urgent cases handled locally, urgent cases moved off-station. Operational detail."
# --- Lattice Economy (3) — added Sprint 12, Mellanie ---
- id: ticker_025
text: "LATTICE: Commission-approved upgrade costs rise — standard lattice enhancement up 18% this cycle"
category: freight
dual_lens:
smuggler: "Eighteen percent. That's why the waiting list for off-book components keeps growing. We're providing a service."
detective: "Price pressure on approved upgrades. Economic driver for the unlicensed market. Motive confirmed."
- id: ticker_026
text: "LATTICE COMMISSION: Public access modification registry now mandatory — self-report deadline cycle 20"
category: commission
dual_lens:
smuggler: "Self-report. Right. The people who bought off-book components are definitely filing that form."
detective: "Mandatory registry. Anyone who doesn't file either doesn't have mods or is hiding them. Narrow that list."
- id: ticker_027
text: "HEALTHCARE: Medical-grade lattice component recall — batch 44xx — contact nearest clinic for assessment"
category: community
dual_lens:
smuggler: "Batch 44xx. Kael's last shipment was 4471 series components. Are they in that range? Check."
detective: "Batch 44xx recall. Container 4471 in the manifest anomaly. Cross-reference. This could be the thread."
notes: >
High-value dual-lens trigger. Smuggler worries about operational risk; detective spots the
evidence connection. The 44xx range is intentionally ambiguous — might match, might not.
# --- Krenn System / Setting (2) — added Sprint 12, Mellanie ---
- id: ticker_028
text: "VELEN WEATHER: Coastal fog advisories in effect — Sova orbital approach window reduced, gate scheduling adjusted"
category: infrastructure
dual_lens:
smuggler: "Fog on Velen. Gate scheduling adjusted — means transit timing shifts. The window might move."
detective: "Environmental factors affecting gate operations. Adjust surveillance timing accordingly."
- id: ticker_029
text: "REACH ECONOMY: Krenn System trade volume up 7% — analysts attribute to 'informal market activity'"
category: freight
dual_lens:
smuggler: "Informal market activity. They know. The whole Reach knows. And still the demand keeps climbing."
detective: "'Informal market.' The analysts see the pattern but can't name it. I can."
- id: ticker_030
text: "ASSEMBLY: Severance technology moratorium extended — counter-surveillance devices remain Category 2 restricted"
category: politics
dual_lens:
smuggler: "Moratorium extended. Good. Severance equipment costs keep the Commission from blanketing everything."
detective: "Counter-surveillance devices still legally restricted. Anyone running them is committing a documented infraction."
+42
View File
@@ -0,0 +1,42 @@
# Fact catalog: awareness
# Situational awareness — who's present, active threats, operational status
# Source: docs/design/knowledge-vocabulary-v01.md §4.2 (#368)
facts:
- fact_id: awareness.detective_presence
description: A Commission detective is operating on Sova Station
typical_confidence: knows_of
characters: [smuggler]
notes: "Smuggler learns through rumor or direct observation. Detective does not hold this fact about themselves."
- fact_id: awareness.detective_investigating_ring
description: The detective is specifically targeting the smuggling ring, not a routine audit
typical_confidence: knows_details
characters: [smuggler]
- fact_id: awareness.commission_audit_scheduled
description: A Commission audit is coming — officially scheduled
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: awareness.ring_route_compromise
description: The current drop route may be compromised or under observation
typical_confidence: suspects
characters: [smuggler]
- fact_id: awareness.surveillance_change
description: Surveillance patterns in the maintenance corridors have changed recently
typical_confidence: suspects
characters: [smuggler]
- fact_id: awareness.ring_tension_escalation
description: Something is wrong inside the ring — someone is scared or making moves
typical_confidence: suspects
characters: [smuggler]
- fact_id: awareness.inspection_sweep_active
description: Active inspection sweep in progress right now
typical_confidence: direct
characters: [smuggler, detective]
notes: "direct confidence — set by environmental observation, not investigation"
+42
View File
@@ -0,0 +1,42 @@
# Fact catalog: behavioral
# Behavioral patterns recognized through repeated observation across multiple scenes
# Source: docs/design/knowledge-vocabulary-v01.md §4.3 (#368)
facts:
- fact_id: behavioral.kael_evasion
description: Kael looks left when lying — personal tell recognized post-conversation
typical_confidence: direct
characters: [smuggler]
notes: "Set during a conversation where evasion is detected. direct confidence = just confirmed."
- fact_id: behavioral.kael_behavioral_change
description: Kael's behavior has noticeably changed — quieter, distracted, different routine
typical_confidence: suspects
characters: [smuggler]
- fact_id: behavioral.sera_avoidance_pattern
description: Sera consistently avoids Torek Lintar — leaves when he arrives, repositions
typical_confidence: suspects
characters: [smuggler, detective]
notes: "One observation = suspects; three observations = knows_of (pattern confirmed for detective)"
- fact_id: behavioral.sera_topic_deflection
description: Sera deflects specific topics — Commission work, manifest audits, freight
typical_confidence: suspects
characters: [smuggler, detective]
- fact_id: behavioral.sera_kiosk_pattern
description: Sera habitually stops at the same Commission kiosk at the same time
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: behavioral.sera_knowledge_detail
description: Sera's offhand comments reveal detailed knowledge of manifest routing she shouldn't have
typical_confidence: knows_of
characters: [smuggler]
- fact_id: behavioral.voss_awareness
description: Voss is tracking people — situational awareness beyond normal supervisor behavior
typical_confidence: suspects
characters: [detective]
+38
View File
@@ -1 +1,39 @@
# Fact catalog: contraband
# Smuggling ring world facts — existence, components, operations, supply chain
# Source: docs/design/knowledge-vocabulary-v01.md §4.5 (#368)
facts:
- fact_id: contraband.ring_exists
description: A smuggling operation is running through Sova Transit District
typical_confidence: suspects
characters: [smuggler, detective]
notes: "Smuggler starts at knows_details (Background — they ARE in the ring). Detective starts at suspects."
- fact_id: contraband.ring_lattice_components
description: The ring moves unlicensed lattice components — aftermarket neural mods
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Smuggler starts at knows_details (Background). Detective discovers through investigation."
- fact_id: contraband.ring_medical_components
description: The ring also diverts medical-grade lattice replacements from Syndic supply chains
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Deep investigation only — the moral dimension of the operation"
- fact_id: contraband.supply_chain
description: Understanding of the ring's supply chain — where components come from
typical_confidence: knows_details
characters: [smuggler, detective]
- fact_id: contraband.severance_tech_presence
description: Severance counter-surveillance technology is in play on the station
typical_confidence: suspects
characters: [detective]
- fact_id: contraband.drop_tonight
description: A drop is scheduled for tonight specifically
typical_confidence: knows_details
characters: [smuggler]
notes: "Seed-dependent — only relevant in seeds where a drop is scheduled during play session"
+126
View File
@@ -1 +1,127 @@
# Fact catalog: investigation
# Case-specific findings — events witnessed, evidence discovered, NPC behaviors confirmed
# Source: docs/design/knowledge-vocabulary-v01.md §4.1 (#368)
facts:
- fact_id: investigation.kael_corridor_meeting
description: Kael was seen in corridor B-7 off-shift, apparently meeting someone
typical_confidence: suspects
characters: [smuggler, detective]
notes: "Shared fact ID; smuggler perspective = operational security breach; detective perspective = initial observation"
- fact_id: investigation.kael_corridor_sighting
description: Detective's initial B-7 observation — Kael spotted in maintenance corridors at unusual hour
typical_confidence: suspects
characters: [detective]
notes: "Detective-only initial ID; may upgrade to kael_corridor_meeting after corroboration"
- fact_id: investigation.kael_unknown_contact
description: The person Kael met in B-7 is unknown — not from the regular rotation
typical_confidence: suspects
characters: [smuggler, detective]
- fact_id: investigation.kael_ring_membership
description: Kael is a member of the smuggling ring
typical_confidence: suspects
characters: [smuggler, detective]
- fact_id: investigation.kael_exit_attempt
description: Kael is actively trying to leave the ring
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: investigation.manifest_discrepancy
description: Cargo manifests show irregularities — containers, weights, routing anomalies
typical_confidence: suspects
characters: [smuggler, detective]
notes: "Detective starts with this at suspects (Background). Smuggler may encounter it during operational checks."
- fact_id: investigation.cargo_anomaly
description: A specific cargo anomaly — wrong container, wrong weight, wrong route
typical_confidence: suspects
characters: [detective]
- fact_id: investigation.shift_mismatch
description: Shift records don't match observed personnel — someone is covering for someone
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: investigation.voss_ring_involvement
description: Voss manages schedule changes to cover drops — involved in ring operations
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: investigation.torek_spending_pattern
description: Torek is spending beyond his dock-worker means — visible at bar
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: investigation.drin_inspection_pattern
description: Drin's inspections are conspicuously lax — clearing containers without real checking
typical_confidence: knows_of
characters: [detective]
- fact_id: investigation.drin_gambling_debt
description: Drin has a gambling debt — leverage for whoever is running him
typical_confidence: knows_details
characters: [detective]
- fact_id: investigation.oversight_gap_pattern
description: Inspection gaps are systematic, not accidental — someone designed the blind spots
typical_confidence: knows_of
characters: [detective]
- fact_id: investigation.surveillance_gaps
description: Specific surveillance camera blind spots in the maintenance corridors
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Smuggler knows from Background (operational necessity). Detective discovers through investigation."
- fact_id: investigation.smuggling_route
description: The specific smuggling route through the station
typical_confidence: knows_details
characters: [smuggler, detective]
- fact_id: investigation.ring_existence
description: A smuggling ring is definitely operating through Sova Transit (detective's confirmation)
typical_confidence: suspects
characters: [detective]
notes: "Detective starts with suspects from manifest anomalies. Smuggler knows via Background (knows_details)."
- fact_id: investigation.renn_courier_role
description: Renn operates as a courier in the ring — runs packages between drop sites
typical_confidence: knows_of
characters: [detective]
- fact_id: investigation.corridor_regular_use
description: The maintenance corridors are regularly used for smuggling, not just incidentally
typical_confidence: knows_of
characters: [detective]
- fact_id: investigation.container_delay
description: A specific container has been held in temporary storage longer than normal
typical_confidence: suspects
characters: [smuggler]
- fact_id: investigation.sera_evidence_held
description: Sera has unreported evidence about manifest irregularities — sitting on it deliberately
typical_confidence: suspects
characters: [detective]
notes: "suspects from avoidance pattern; knows_details only if Sera discloses or evidence found independently"
- fact_id: investigation.naia_lattice_condition
description: Naia's neural lattice is degrading — the medical components are for her
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Requires Kael disclosure or independent medical evidence"
- fact_id: investigation.ring_leadership
description: Who directs ring operations — late-game discovery
typical_confidence: knows_details
characters: [smuggler, detective]
- fact_id: investigation.drin_inspection_pattern
description: Drin's inspection patterns protect specific containers at specific shift windows
typical_confidence: knows_of
characters: [detective]
+32
View File
@@ -0,0 +1,32 @@
# Fact catalog: knowledge
# Operational knowledge — ring procedures, social dynamics, institutional mechanics
# Source: docs/design/knowledge-vocabulary-v01.md §4.4 (#368)
facts:
- fact_id: knowledge.ring_routing_knowledge
description: Full knowledge of the ring's drop routes and timing protocols
typical_confidence: knows_details
characters: [smuggler]
notes: "Smuggler starts at knows_details (Background). Not available to detective through normal investigation paths."
- fact_id: knowledge.ring_bar_operations
description: The bar (The Last Shift) has a role in ring communications or social cover
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: knowledge.bar_back_room_significance
description: The bar's back room is significant — meeting space, handoff point, or storage
typical_confidence: suspects
characters: [detective]
- fact_id: knowledge.bar_ring_awareness
description: Bar regulars have varying levels of awareness of the ring — open secret at some level
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: knowledge.kael_exit_plan
description: Kael has a specific exit plan — timing, conditions, contact
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Only available through Kael's deep trust disclosure or independent investigation"
+50
View File
@@ -1 +1,51 @@
# Fact catalog: location
# Location-specific access, restrictions, and significance
# Source: docs/design/knowledge-vocabulary-v01.md §4.6 (#368)
facts:
- fact_id: location.corridor_b7_restricted
description: Corridor B-7 is a restricted access zone — requires credentials
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Both characters have this from Background (signage, institutional knowledge)"
- fact_id: location.corridor_b7_drop_site
description: B-7 is an active drop site for the ring
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Smuggler from Background. Detective through investigation."
- fact_id: location.maintenance_corridors_access
description: Character knows the maintenance corridor layout and can navigate it
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Smuggler at knows_details (Background — daily operational space). Detective at knows_of after investigation."
- fact_id: location.bay4_sealed
description: Bay 4 is sealed — inspection or deliberate cover
typical_confidence: suspects
characters: [smuggler, detective]
- fact_id: location.span_gate_schedule_gap
description: The span gate schedule has a window that enables unseen cargo drops
typical_confidence: knows_details
characters: [smuggler]
- fact_id: location.smuggling_route
description: The complete physical route the ring uses through the station
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Smuggler from Background. Detective from deep investigation."
- fact_id: location.surveillance_gaps
description: Specific camera blind spots in the maintenance corridor network
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Smuggler from Background. Detective must discover independently."
- fact_id: location.dead_drop_location
description: Specific dead drop location(s) used for handoffs
typical_confidence: knows_details
characters: [smuggler]
notes: "Smuggler operational knowledge"
+35
View File
@@ -1 +1,36 @@
# Fact catalog: progress
# Narrative milestone flags
# Note: progress.* facts are NOT recommended for direct authoring in prerequisite blocks.
# Use investigation fact chains and relationship states instead.
# These exist for potential storyteller/arc-tracking use.
# Source: docs/design/knowledge-vocabulary-v01.md §3.2 (#368)
facts:
- fact_id: progress.friend_arc_phase_1
description: FRIEND arc Phase 1 warmth established (baseline positive relationship)
notes: "Set by relationship state transition to Friendly. Do not author directly — use relationship checks."
- fact_id: progress.friend_arc_phase_2
description: FRIEND arc Phase 2 trust built (deep rapport + initial social context)
notes: "Set by trust tier advancement. Do not author directly — use investigation + relationship checks."
- fact_id: progress.friend_arc_contradiction
description: FRIEND contradiction discovered (KnowledgeState → Contradicted)
notes: "Set by contradiction detection system. Use relationship state person_of_interest + investigation fact instead."
- fact_id: progress.friend_arc_confrontation
description: FRIEND confronted about contradiction
notes: "Set post-confrontation by dialogue system."
- fact_id: progress.opening_complete
description: Opening tutorial sequence completed (first 5 minutes)
notes: "Set by storyteller after initial objective sequence."
- fact_id: progress.first_ring_contact
description: Smuggler completed first ring operation during play session
characters: [smuggler]
- fact_id: progress.first_evidence_filed
description: Detective filed first manifest discrepancy evidence
characters: [detective]
@@ -1 +1,33 @@
# Fact catalog: relationship
# Inter-NPC connections — what one character knows about relationships between others
# Source: docs/design/knowledge-vocabulary-v01.md §4.7 (#368)
facts:
- fact_id: relationship.kael_naia_connection
description: Kael and Naia are romantically partnered — not just colleagues
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Smuggler may know from background or Kael mentioning Naia. Detective through social observation."
- fact_id: relationship.ring_membership
description: Character has identified specific ring members
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "knows_of = partial list; knows_details = full ring membership identified"
- fact_id: relationship.trust_network
description: Understanding of who trusts whom inside the ring — operational reliability mapping
typical_confidence: knows_details
characters: [smuggler]
notes: "Smuggler-specific operational knowledge"
- fact_id: relationship.tamm_family
description: Connection between Naia Tamm and her family situation
typical_confidence: knows_of
characters: [smuggler, detective]
- fact_id: relationship.torek_bar_arrangement
description: Torek has a financial arrangement related to the bar or its operations
typical_confidence: knows_of
characters: [smuggler, detective]
+11
View File
@@ -0,0 +1,11 @@
# Fact catalog: social
# Social positioning facts — standing in a specific social context
# Source: docs/design/knowledge-vocabulary-v01.md §4.8 (#368)
facts:
- fact_id: social.bar_regular_status
description: Character is an established regular at The Last Shift — recognized, trusted in that context
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Smuggler may have this from Background. Detective builds it through multiple bar visits."
+34
View File
@@ -1 +1,35 @@
# Fact catalog: world
# Faction and institutional knowledge — Commission, lattice regulation, broader context
# Source: docs/design/knowledge-vocabulary-v01.md §4.6 (#368)
facts:
- fact_id: world.lattice_regulation_active
description: Commission actively regulates neural lattice component access in this system
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Both characters have this from Background"
- fact_id: world.unlicensed_lattice_demand
description: High demand for unlicensed lattice upgrades on working-class stations like Sova
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "The moral context of the operation — smuggler knows it as justification; detective may learn it as complication"
- fact_id: world.commission_sova_jurisdiction
description: Commission has jurisdiction over Sova Transit District
typical_confidence: knows_details
characters: [detective]
notes: "Detective Background knowledge — institutional mandate"
- fact_id: world.ring_cross_system
description: The ring operates across multiple star systems — Sova is one node
typical_confidence: knows_details
characters: [smuggler, detective]
notes: "Late-game discovery — changes scope of the operation"
- fact_id: world.shift_schedule
description: Sova Transit District shift schedule — who works when
typical_confidence: knows_of
characters: [smuggler, detective]
notes: "Smuggler from Background (colleague). Detective through investigation."
+1 -1
View File
@@ -11,7 +11,7 @@ Cross-domain decisions live in one file with cross-reference notes in related fi
| File | Domain | Decisions |
|------|--------|-----------|
| [architecture.md](architecture.md) | Technical foundation | D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066, D-068, D-073 |
| [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072 |
| [perception.md](perception.md) | Player observation | D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043, D-044, D-045, D-046, D-047, D-048, D-049, D-052, D-056, D-057, D-058, D-059, D-060, D-061, D-067, D-069, D-070, D-071, D-072, D-076, D-077, D-078 |
| [content.md](content.md) | NPC, dialogue, templates | D-023, D-024, D-025, D-028, D-029, D-032, D-034, D-035, D-036, D-037, D-050, D-062, D-063, D-064, D-074 |
| [scope.md](scope.md) | Game concept, prototype | D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065 |
| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040 |
+2 -2
View File
@@ -178,8 +178,8 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Amends:** [D-041](architecture.md#d-041-knowledge-graph-data-model) (confirms confidence-to-trust mapping; supersedes the preliminary 1:1 sketch in D-041 "Key design choices" bullet 3 with the layered model above), [D-028](#d-028-dialogue-architecture--tagged-line-pools-with-four-relational-layers) (Layer 3 trust now requires both relationship AND confidence)
- **Cross-reference:** [D-028](#d-028-dialogue-architecture--tagged-line-pools-with-four-relational-layers), [D-035](#d-035-converged-tag-taxonomy-for-dialogue-and-monologue-line-pools), [D-041](architecture.md#d-041-knowledge-graph-data-model), [D-062](#d-062-invisible-locked-dialogue-options) (confidence progression naturally unlocks new trust tiers, creating the "new options appearing" reward)
- **Raised by:** Tyre (technical analysis, architecture synthesis)
- **Dissent:** Pending review. Sprint briefing flags Gestalt and Nigel for archetype dimension input.
- **Dissent:** Gestalt endorses D-075 (reviewed 2026-02-19). The emergent archetype distinction is sufficient: access tier tags already encode "authority figure lines" vs "insider lines" in content; starting knowledge differentials produce different dialogue gate timings per character; adding an archetype filter would create per-character content maintenance burden and dilute the "two keyholes on the same world" experience (D-027). Knowledge vocabulary doc (#368) confirms this works in practice — same fact IDs, different starting confidence levels, different gate-open timing per character. *Nigel's input still pending.*
---
*16 decisions. Last updated: 2026-02-19*
*16 decisions. Last updated: 2026-02-19 (D-075 dissent updated)*
+51 -1
View File
@@ -345,6 +345,56 @@ How the player observes and interacts with the world: camera, fog, line-of-sight
- **Raised by:** Paula (zone-conspicuousness model), Inigo (scoping to future sprint)
- **Dissent:** None
### D-076: Dialogue box max-width — 640px (OQ-29 resolution)
- **Date:** 2026-02-19
- **Decision:** `DIALOGUE_MAX_WIDTH = 640px`. Dialogue box is max 640px wide, centered on screen.
- **Derivation:** 640px = 20 × TILE_SIZE (32px) — grid-aligned. ~33% of target 1920px viewport width. Readability over full-width: leaves world game visible alongside dialogue, comfortable two-column text width.
- **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 note:** Initial resolution was 1920px (full viewport width) per D-061 Lead directive. Tyre architecture review (2026-02-19) revised to 640px for readability.
- **Implementation:** `Constants.DIALOGUE_MAX_WIDTH` in `client/scripts/constants.gd`.
- **Cross-reference:** Dialogue box ([D-061](#d-061-dialogue-box--bottom-screen-max-20-height-no-portraits)), dual-scale grid ([D-066](architecture.md#d-066-dual-scale-grid--05m-simulation-1m-visual-2x-retina-factor))
- **Amends:** [D-061](#d-061-dialogue-box--bottom-screen-max-20-height-no-portraits) (adds pixel value for max-width)
- **Raised by:** Stig (OQ-29), revised per Tyre architecture review
### 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](#d-059-fog--shader-based-five-layers-knowledge-graph-driven) 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](architecture.md#d-010-multiplayer-ready-architectural-baseline) 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](architecture.md#d-073-zone-crossfade-approach--hard-boundary-soft-audio-transition)).
- **Cross-reference:** Fog layers ([D-059](#d-059-fog--shader-based-five-layers-knowledge-graph-driven)), zone crossfade ([D-073](architecture.md#d-073-zone-crossfade-approach--hard-boundary-soft-audio-transition)), ObserverSnapshot ([D-020](architecture.md#d-020-engine-and-architecture-selection--godot-client--rust-simulation-via-subprocessipc)), deterministic simulation ([D-010](architecture.md#d-010-multiplayer-ready-architectural-baseline))
- **Raised by:** Tyre (architecture review, #523)
- **Dissent:** None
### D-078: Overheard NPC conversation — passive dialogue panel with occlusion filter
- **Date:** 2026-02-19
- **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](#d-061-dialogue-box--bottom-screen-max-20-height-no-portraits)), eavesdropping ([D-071](#d-071-no-ambient-dip-for-eavesdropping--listeningfocus-boost)), conversation murmur ([D-072](#d-072-universal-event-driven-conversation-murmur)), monologue system ([D-016](#d-016-internal-monologue-as-core-perceptionatmosphere-system)), knowledge confidence ([D-041](architecture.md#d-041-knowledge-graph-data-model))
- **Raised by:** Lead (passive panel directive, occlusion filter, monologue separation)
- **Dissent:** None
---
*29 decisions. Last updated: 2026-02-19 (OQ-07 resolved: D-056/D-057 amendment)*
*32 decisions. Last updated: 2026-02-19 (D-078: overheard NPC conversation — passive dialogue panel with occlusion filter)*
+12 -1
View File
@@ -15,6 +15,7 @@ Mixed methods depending on duration and character:
|----|----------|--------|-----|--------|----------|-------------|--------|
| SFX-001 | `sfx_footstep_metal_walk.ogg` | done | Player Actions | SAO hybrid | 0.3s | Single footstep on metal grating. Walk pace. SAO 5s/100 steps/CFG 8, best transient trimmed at 2.42s, LUFS -16 normalized. | S10 #327 |
| SFX-002 | `sfx_footstep_metal_run.ogg` | done | Player Actions | SAO hybrid | 0.3s | Faster footstep, sprint pace. Louder — player is broadcasting position (D-053 acoustic footprint). SAO 5s/100 steps/CFG 8, best transient trimmed at 1.03s, LUFS -16 normalized. | S10 #327 |
| SFX-003 | `sfx_npc_murmur.ogg` | done | World SFX | SAO | 5s | NPC proximity conversation murmur. Human voices in adjacent room, direction and cadence distinguishable, words not. Organic family. SAO 5s/100 steps/CFG 8, LUFS -16 normalized. Selected from 3 candidates (richest texture). | S13 #532 |
## Detailed Entries
@@ -25,6 +26,17 @@ Mixed methods depending on duration and character:
- **Integration:** Triggered by player movement tick. Positional (AudioStreamPlayer2D at player position) — maps to camera-relative stereo per D-018.
- **Cross-reference:** D-038 (#5), D-053 (movement stances as acoustic decisions)
### SFX-003: sfx_npc_murmur
- **Sonic family:** organic (physical world — human warmth, natural decay)
- **Bus rationale:** World SFX — this is information about other people in the world, not player feedback. Positional (AudioStreamPlayer2D at NPC cluster position).
- **Design intent (D-072):** Universal asset — single murmur plays at all NPC proximity events. Zone ambient determines conspicuousness naturally: bar's dense ambient buries it, corridor's sparse ambient exposes it. No zone-specific variants needed.
- **Aesthetic (D-074):** Organic family. Warm, lived-in, no lattice processing. Mid-freq 200-800Hz texture per station palette "functional warmth" (D-074 conversation murmur band).
- **Propagation (D-018):** Close range: intelligible as "voices, nearby." Medium range: muffled. Long range: lost in ambient floor.
- **Eavesdrop interaction (D-071):** Player ListeningFocus boost increases effective reception range. Same asset, confidence varies with proximity and skill.
- **Generation:** SAO 5s / 100 steps / CFG 8. Prompt: "Warm organic audio, natural sound recording, slight room ambience, lived-in industrial space, realistic acoustic properties, authentic environmental sound. Single isolated sound effect, clean recording, 5 seconds duration, clear attack and natural decay, no background noise, no reverb tail beyond natural. Human voices murmuring through a thin wall or partition, two or three people in low conversation in the adjacent room, words muffled and indistinct, direction and gentle cadence audible, warm mid-frequency vocal texture 200-800Hz, soft intimate conversation murmur, no music, no intelligible words." 3 candidates generated (5s/CFG8, 6s/CFG7, 4s/CFG9). Candidate 1 selected (richest OGG texture, clearest spatial framing). Post-processed: trim, LUFS -16, OGG.
- **Integration:** Event-driven — fires when player enters NPC cluster proximity range. Not looped continuously; retriggered on proximity events. Loop-capable (5s has natural loop structure) if client opts to sustain during eavesdrop.
- **Cross-reference:** D-072 (universal murmur decision), D-071 (ListeningFocus eavesdrop), D-074 (organic aesthetic), D-018 (sound propagation model)
## Future Expansion
SFX will grow significantly with:
@@ -32,7 +44,6 @@ SFX will grow significantly with:
- Door open/close, machinery activation → World SFX bus
- Combat impacts, weapon sounds → Player Actions bus
- Environmental events per planet/biome
- NPC conversation murmur (event-driven, point-source) → World SFX bus
When this file exceeds ~50 entries, split by domain:
- `sfx-movement.md` (footsteps, stance variants)
Binary file not shown.
+461
View File
@@ -0,0 +1,461 @@
# Character Build: The Detective
**Ticket:** #181 | **Priority:** Critical | **Sprint:** 12
**Status:** v0.1 Draft
**Blocked by:** #179 (Character definition schema)
**Blocks:** #182 (Divergent starting knowledge), #183 (Divergent relationships)
**Decisions referenced:** D-005, D-016, D-027, D-028, D-032, D-034, D-035, D-037, D-041, D-053, D-065, D-075
---
## identity
```yaml
identity:
canonical_id: "pc:detective"
display_name: "[Player-set name]"
archetype: "detective"
occupation: "Commission Liaison Officer, Manifest Irregularity Investigation"
posting_duration: "3 days (newly arrived)"
faction: "Commission"
lattice_tier: "professional"
description: >
Commission Liaison Officer posted to Sova Transit District three days ago.
Official mandate: routine compliance review. Actual mandate: investigate
manifest irregularities suggesting organized freight smuggling. The distinction
between "official" and "actual" is itself the first lesson about how
Sova works.
Trained investigator. Professional analytical lattice. Institutional
authority that opens doors and closes conversations. An outsider in a
community that has learned, over forty years of prefab-modular life,
to handle outsiders with polite minimal disclosure.
Stands to lose: professional credibility (if the case goes cold), Sera
(the investigation, if pressed too hard, could compromise the only
trusted relationship in the district), and the clean institutional
clarity that makes the job feel justified — which the community's
moral complexity is already beginning to erode.
```
---
## starting_knowledge
```yaml
starting_knowledge:
confidence_floor: "KnowsOf"
known_facts:
# Investigation data
- key: "investigation.manifest_discrepancies"
confidence: "KnowsDetails"
source: "institutional"
notes: >
Case file from Commission records. Containers logged but not reconciled
with incoming manifests. Pattern suggests systematic manipulation, not
clerical error. Three clusters of anomalies over the past six months.
- key: "investigation.freight_pattern"
confidence: "KnowsDetails"
source: "institutional"
notes: >
Anomalies cluster around shift transition windows. Same container series
recurring across discrepant manifests. Pattern analysis flagged this
posting as warranting investigation.
# Persons of interest (case file, not personal knowledge)
- key: "person.voss_poi"
confidence: "KnowsOf"
source: "institutional"
notes: >
Voss, shift supervisor. Scheduling authority. In position to create the
oversight gaps that explain the manifest anomalies. Name from case file.
Not met yet.
- key: "person.maret_poi"
confidence: "KnowsOf"
source: "institutional"
notes: >
Maret Korr, freight scheduler. Access to manifest data. Could be source
of the falsification or could be covering for someone else. Name from
case file.
- key: "person.drin_poi"
confidence: "KnowsOf"
source: "institutional"
notes: >
Drin Rosta, dock inspector. Responsible for container verification.
Failure to flag discrepancies suggests either incompetence or complicity.
Name from case file.
# Institutional context
- key: "world.previous_liaison"
confidence: "KnowsDetails"
source: "institutional"
notes: >
The previous Commission liaison rotated out 3 months ago. Rotation
was described as routine. The detective's case file notes that the previous
liaison filed no irregularity reports despite the same pattern being
detectable in their tenure. This warrants investigation.
- key: "world.commission_presence"
confidence: "KnowsDetails"
source: "institutional"
notes: >
Sera Venn, Commission field tech. Not an investigator. Has been on Sova
for 1 year. Pre-existing relationship — same institutional background,
different postings. Reliable. Social anchor in unfamiliar district.
- key: "world.district_geography"
confidence: "KnowsOf"
source: "institutional"
notes: >
The Terminal (logistics hub), The Last Shift (bar, social nexus),
maintenance corridors (off-books access routes noted in district layout
documentation). Three days in — still orienting to physical space.
- key: "world.torek_compliance_connection"
confidence: "KnowsOf"
source: "institutional"
notes: >
Torek Lintar is a Commission compliance officer on Sova. Known from
institutional channels. Not a friend, but a professional known name.
# Sera knowledge
- key: "person.sera_background"
confidence: "KnowsDetails"
source: "direct"
notes: >
Sera Venn: same career path, different posting. Reliable under pressure.
Transferred to Sova after a stressful posting elsewhere. Has been the
district's Commission presence for a year. The detective's one genuinely
friendly face in Sova.
unknown_facts:
- key: "contraband.ring_exists"
notes: >
Does not know a ring exists. Suspects organized smuggling from the manifest
pattern, but "ring" implies structure, membership, coordination — the
detective doesn't know that yet.
- key: "contraband.ring_members"
notes: >
Does not know any ring members by their ring membership. Kael Davan
is not in the case file. The smuggler is not in the case file.
- key: "contraband.cargo_type"
notes: >
Does not know what the cargo is. The manifests show discrepancies;
they don't reveal what's being moved. The "what" is a layer below
the "that something is wrong."
- key: "person.kael_exit_attempt"
notes: >
Completely unknown. Kael's unauthorized contact and exit attempt are
not visible to any institutional data source.
- key: "person.sera_unreported_evidence"
notes: >
CRITICAL unknown. Sera has run an unofficial compliance check on Kael's
work patterns and is sitting on unreported evidence. The detective
trusts Sera completely. This gap is the structural basis for THE FRIEND arc.
- key: "person.naia_confided_in_sera"
notes: >
Naia Tamm's worry about Kael, confided to Sera, is the emotional engine
behind Sera's concealment. The detective doesn't know Naia exists as
anything except a district resident.
- key: "relationship.ring_membership"
notes: >
Who is in the ring, who is complicit, who is protecting whom. The detective
has names (Voss, Maret, Drin) without understanding the social web that
holds them together.
- key: "person.smuggler_identity"
notes: >
Does not know the smuggler is in the ring. If the smuggler is in the case
file at all, it's as a name on a shift roster — no significance attached.
- key: "world.previous_liaison_arranged"
notes: >
The detective wonders whether the previous Commission liaison's rotation
was routine or arranged. This suspicion is in the case file as a note;
it remains unconfirmed. (Canonical answer: arranged. But the detective
doesn't know this yet.)
```
---
## starting_relationships
```yaml
starting_relationships:
- target: "npc:sera-venn"
relationship_state: "Friendly"
access_tier: [peer]
trust_level: "trusted"
relationship_type: "friend"
known_attributes:
name: "Sera Venn"
role: "Commission field technician"
faction: "Commission"
notes: >
Pre-existing friend. Same institutional background, different posting.
Transferred to Sova a year ago. Social anchor. The detective's guide
to an unfamiliar place — Sera fills in background on the district,
introduces the detective to regulars. Completely trusted at session start.
- target: "npc:voss"
relationship_state: "Known"
access_tier: [authority]
trust_level: "uncertain"
relationship_type: "contact"
known_attributes:
name: "Voss"
role: "shift supervisor"
faction: "civilian"
notes: >
Person of interest. Name from case file, scheduling authority. Has
met once for credentials review. Stiffens when the detective enters
the hub. Professional, terse. No warmth. Institutional interaction only.
- target: "npc:maret-korr"
relationship_state: "Known"
access_tier: [authority]
trust_level: "uncertain"
relationship_type: "contact"
known_attributes:
name: "Maret Korr"
role: "freight scheduler"
faction: "civilian"
notes: >
Person of interest. Manifest access. Has cooperated professionally with
data requests — provided clean set of schedules. Slightly nervous.
Cooperation is compliance, not friendliness.
- target: "npc:drin"
relationship_state: "Known"
access_tier: [authority]
trust_level: "uncertain"
relationship_type: "contact"
known_attributes:
name: "Drin Rosta"
role: "dock inspector"
faction: "civilian"
notes: >
Person of interest. Responsible for container verification. Has met
once. More evasive than Voss or Maret. The detective notes the evasion
without yet understanding the cause.
- target: "npc:torek-lintar"
relationship_state: "Known"
access_tier: [public]
trust_level: "uncertain"
relationship_type: "colleague"
known_attributes:
name: "Torek Lintar"
role: "Commission compliance officer"
faction: "Commission"
notes: >
Commission compliance officer on Sova — professional known name, not a
friend. Has seen him at the bar with Sera. Slightly off in a way the
detective can't yet articulate.
- target: "npc:lera-sessik"
relationship_state: "Unknown"
access_tier: [public]
trust_level: "uncertain"
relationship_type: null
known_attributes:
name: "Lera Sessik"
role: "bar owner"
faction: null
notes: >
Sera mentioned The Last Shift. The detective has visited once or twice.
Lera is polite, guarded, watching. Not hostile. Not warm.
- target: "npc:kael-davan"
relationship_state: "Unknown"
access_tier: [public]
trust_level: "uncertain"
relationship_type: null
known_attributes:
name: null
role: "dock worker"
faction: null
notes: >
Unknown at session start. Name might appear on a shift roster without
significance. No personal knowledge. No case file entry. The detective
has no reason to flag Kael — yet.
- target: "npc:naia-tamm"
relationship_state: "Unknown"
access_tier: [public]
trust_level: "uncertain"
relationship_type: null
known_attributes:
name: null
role: null
faction: null
notes: "No connection at session start. Exists as a district resident without individual identity."
- target: "npc:pc-smuggler"
relationship_state: "Unknown"
access_tier: [public]
trust_level: "uncertain"
relationship_type: null
known_attributes:
name: null
role: "dock worker"
faction: null
notes: >
One of many dock workers. No individual significance at session start.
Knowledge accumulates through observation and tell accumulation.
May shift to PersonOfInterest if tells compound and manifest
discrepancy connection is made.
```
---
## access_permissions
```yaml
access_permissions:
can_unlock:
- tier: "public"
condition: "Always available"
max_npcs: "unlimited"
- tier: "authority"
condition: "RelationshipState is Unknown, Known, or PersonOfInterest — credential presentation"
max_npcs: "unlimited for institutional NPCs; does not work with hostile or fully closed NPCs"
- tier: "peer"
condition: "RelationshipState reaches Friendly"
max_npcs: "limited — Sera at session start; others require sustained positive interaction"
structurally_locked:
- tier: "insider"
reason: >
The detective can never access insider-tier ring dialogue. Not because
institutional authority is insufficient — authority and insider are
different axes. Insider requires social belonging within the ring network.
The ring recognizes the detective as Commission. The ring closes, not opens,
when institutional authority is applied. Pressing harder does not unlock
insider content; it triggers defensive behavior and hostile tone. This is
architecturally fundamental to the two-keyhole design.
- tier: "authority (failed)"
reason: >
Authority access fails when RelationshipState is Hostile. Once an NPC
has turned against the detective's investigation, credential presentation
produces refusal or evasion, not compliance. The detective cannot
authority-tier their way through a hostile source.
```
---
## skills
```yaml
skills:
lattice_tier: "professional"
movement_default: "walk" # D-053 MovementProfile
inventory_slots: 2 # D-065
evidence_display: "case_file"
capability_flags:
- "authority_credentials" # Badge/ID; unlocks authority tier with Unknown/Known NPCs
- "manifest_access" # Can formally request freight manifests from institutional NPCs
- "analytical_lattice" # Pattern recognition overlay; flags behavioral anomalies at Careful stance
starting_items:
- item_id: "detective_case_file"
description: "Institutional case file: manifest discrepancy analysis, persons of interest, previous liaison notes."
leverage_type: "proof"
notes: >
The detective's starting information in physical form. Can be referenced
in dialogue to trigger authority-tier responses — "according to case file X."
Contains the manifest discrepancy data that links to Kael's work patterns,
though the detective doesn't know this yet.
- item_id: "detective_commission_credentials"
description: "Commission identification and liaison officer authorization."
leverage_type: "access"
notes: >
Credential presentation triggers authority tier access with Unknown/Known NPCs.
Does not work with ring-network NPCs in insider mode — they recognize
Commission credentials as a threat, not an access grant. Carried on person;
visible as a status marker to any NPC with institutional awareness.
```
---
## presentation
```yaml
presentation:
monologue_pool: "detective"
evidence_display: "case_file"
voice_register:
sentence_length: "complete sentences, occasional fragments for emphasis"
vocabulary: "institutional layer over plain speech — investigative habit without jargon"
emotional_mode: "analysis-first, personal second"
relationship_mode: "surname-first for targets, first-name for trusted"
emotional_baseline:
state: "professional-cautious"
description: >
Trained to observe, analyze, withhold judgment until evidence accumulates.
But also human — isolated in an unfamiliar community, reliant on Sera for
social grounding, aware that non-cooperation is the district default.
The analytical habit runs in the background like an operating system.
Counts things. Tracks frequencies. Flags inconsistencies. Internal bookkeeping
is automatic: "filing it," "noted," "coincidence?" The institutional training
doesn't turn off; sometimes the human overrides it. When it does, the crack
in the professional surface IS the emotional payload.
Believes in the work. Smuggling isn't victimless — unlicensed lattice
components can cause neural degradation, medical replacements diverted from
Syndic supply chains mean someone else doesn't get what they need. The
investigation is justified. But the moral clarity is starting to erode —
the community is beginning to read as more complex than "crime scene."
opening_monologue:
lines:
- "Sova Transit District. Third day on station. The briefing said routine compliance review."
- "The briefing is always wrong about something."
- "Manifest discrepancies clustered around the freight hub. Start there. Watch the shift change."
- "Good to know Sera's here. At least someone in this district speaks my language."
emotional_payload: >
Isolation, professional caution, guarded optimism about Sera. The detective
is an outsider. The district doesn't want them here. The player should feel
the gap between institutional authority and social belonging.
```
---
## Authoring Notes
**What makes the detective's experience distinct:**
- The district is a CRIME SCENE. Every NPC is a potential witness, informant, or subject. The detective categorizes before connecting. Kael is "dock worker, Davan, K." before he's Kael. The shift to first names — if it happens — is itself a relationship marker.
- The monologue is ANALYTICAL and complete-sentence. Counts things. Notices deviations. The emotional register leaks through the analytical frame rather than leading it.
- The emotional journey of the vertical slice moves from professional isolation to specific, personal betrayal — but the betrayal is different: not "Kael lied to me," but "Sera, who I trusted completely, is compromised by something she did to protect someone I've never met."
**What the detective's authority access buys:** Conversations happen faster. NPCs answer direct questions. But answers are framed for compliance, not candor. The detective gets facts from people who don't want to give them — which means incomplete facts, deflected facts, facts that look complete but aren't. Authority access is a key that opens the door. It doesn't make what's inside visible.
**Voice anchors (mandatory reference for all detective monologue):**
1. *"Logistics hub. Standard prefab, heavy foot traffic. Let's see what the shift change tells me."*
2. *"That's the third time. Sera leaves when Torek arrives. Three different excuses, same result. That's not coincidence."*
3. *"Good to see Sera. She always was the reliable one. At least someone in this district speaks my language."*
4. *"Same booth. Same warm smile. Same offer to buy me a drink. ...Everything except whatever she's not saying."*
5. *"Grain spirit and low conversation. Only place in this district that doesn't smell like freight lubricant."*
---
*Ticket #181 — Detective character build. v0.1 scope.*
+490
View File
@@ -0,0 +1,490 @@
# Character Build: The Smuggler
**Ticket:** #180 | **Priority:** Critical | **Sprint:** 12
**Status:** v0.1 Draft
**Blocked by:** #179 (Character definition schema)
**Blocks:** #182 (Divergent starting knowledge), #183 (Divergent relationships)
**Decisions referenced:** D-005, D-016, D-027, D-028, D-032, D-034, D-035, D-037, D-041, D-053, D-065
---
## identity
```yaml
identity:
canonical_id: "pc:smuggler"
display_name: "[Player-set name]"
archetype: "smuggler"
occupation: "Freight Logistics Operator, Sova Transit District"
posting_duration: "~3 years (settled resident of Sova)"
faction: null # No institutional affiliation. Informal: ring member.
lattice_tier: "civilian"
description: >
Logistics worker at Sova Terminal Freight Hub. Three years on the station.
This is home in the way a place becomes home when you've stopped thinking
about leaving — comfortable enough to feel permanent, not comfortable enough
to feel easy. The ring started as extra income. It stayed because the people
in it became the people worth protecting.
Manages cargo routing. Knows the shift schedules, the corridor access windows,
the containers that move in ways they shouldn't. Has lived inside the operation
long enough that the criminal mechanics and the legitimate mechanics feel
the same — two halves of a single job. The distinction between "dock worker"
and "ring operative" dissolved somewhere around the 18-month mark.
Stands to lose: freedom (Commission arrest), the people (Kael, Lera, the
whole network), and the only version of stability they've built.
```
---
## starting_knowledge
```yaml
starting_knowledge:
confidence_floor: "KnowsOf"
known_facts:
# Ring operational knowledge
- key: "contraband.ring_exists"
confidence: "KnowsEverything"
source: "direct"
notes: "Member. Not a surprise. Not abstract. The daily operational reality."
- key: "contraband.lattice_components"
confidence: "KnowsDetails"
source: "direct"
notes: >
Handles the containers. Knows the cargo is unlicensed lattice components
and medical-grade neural replacements. Doesn't look too closely at the
sealed containers — enough to know, not enough to be haunted by specifics.
- key: "contraband.supply_chain_partial"
confidence: "KnowsOf"
source: "secondhand"
notes: >
Knows cargo comes from off-station contact (Sabel). Does not know the
full upstream supply chain or Syndic connections. Deliberately incurious.
- key: "contraband.ring_members"
confidence: "KnowsDetails"
source: "direct"
notes: >
Nils (coordinator), Kael (closest colleague), Voss (shift supervisor, takes
a cut), Renn (courier), Devra (handler, mostly off-station). Resha and other
minor members known by face if not always by name.
- key: "contraband.operation_schedule"
confidence: "KnowsEverything"
source: "direct"
notes: >
Shift transitions create 20-minute oversight gaps. Runs know during these
windows. Freight bay → service corridor → temp storage → bar back room.
Has run this route dozens of times.
# Location knowledge
- key: "location.corridor_b7_access"
confidence: "KnowsEverything"
source: "direct"
notes: "Knows B-7 is restricted. Knows how to enter without triggering flags."
- key: "location.smuggling_route"
confidence: "KnowsEverything"
source: "direct"
notes: "Full route map. Knows which corridors have surveillance gaps."
- key: "location.dead_drop_location"
confidence: "KnowsDetails"
source: "direct"
notes: "Temp storage bay 4 is the handoff point. Container 4471 is a recurring flag."
- key: "location.sova_district_full"
confidence: "KnowsEverything"
source: "direct"
notes: "Three years here. Knows every social site, shortcut, and NPC face."
# Relationship knowledge
- key: "relationship.ring_membership_full"
confidence: "KnowsEverything"
source: "direct"
notes: "Knows who's in. Knows who knows. The map of the network is second nature."
- key: "relationship.voss_compromised"
confidence: "KnowsDetails"
source: "direct"
notes: "Voss takes a cut. The scheduling cover is explicit. Not a theory."
- key: "relationship.drin_compromised"
confidence: "KnowsDetails"
source: "direct"
notes: "Dock inspector who looks the other way. Known through operational familiarity."
- key: "relationship.naia_connection"
confidence: "KnowsOf"
source: "secondhand"
notes: >
Knows Naia as Kael's partner. Occasional bar encounters. Likes her.
Doesn't know Naia suspects something is wrong with Kael.
# World knowledge
- key: "world.shift_schedule"
confidence: "KnowsEverything"
source: "direct"
notes: "Knows the schedule the way you know your own heartbeat."
- key: "world.commission_presence_minimal"
confidence: "KnowsDetails"
source: "direct"
notes: >
Commission presence on Sova is Sera Venn (field tech) plus occasional
compliance visits. Not a heavy institutional presence. The ring has operated
in this environment comfortably.
- key: "world.torek_spending"
confidence: "KnowsOf"
source: "observation"
notes: >
Torek's visible spending has caught the smuggler's attention as a
potential security risk. Not confirmed dangerous. A worry.
unknown_facts:
- key: "investigation.detective_identity"
notes: >
Does not know the detective exists as an investigator. A Commission liaison
was mentioned (ring warning from Kael or Voss), but not yet identified
as a specific investigative threat.
- key: "person.kael_exit_attempt"
notes: >
CRITICAL unknown. Kael is trying to exit the ring via an unauthorized
external contact. The smuggler trusts Kael completely. This gap is the
structural basis for THE FRIEND arc.
- key: "person.sera_unreported_evidence"
notes: >
Sera Venn has run an unofficial compliance check on Kael's work patterns
and found manifest discrepancies. She has not reported them. The smuggler
knows Sera only as "Commission field tech — stay away."
- key: "person.naia_confided_in_sera"
notes: >
Naia has shared her worries about Kael with Sera. This connection — Naia
to Sera to suppressed evidence — is entirely invisible to the smuggler.
- key: "contraband.supply_chain_full"
notes: >
The full Syndic connection and upstream supply chain beyond Sabel.
Deliberately kept out of operational knowledge by Nils.
- key: "investigation.manifest_discrepancies_flagged"
notes: >
The detective's case file includes specific manifest discrepancy data.
The smuggler doesn't know the investigation is that far along.
```
---
## starting_relationships
```yaml
starting_relationships:
- target: "npc:kael-davan"
relationship_state: "Friendly"
access_tier: [insider, peer]
trust_level: "trusted"
relationship_type: "friend"
known_attributes:
name: "Kael Davan"
role: "dock worker, ring member"
faction: "ring"
notes: >
Closest colleague. Two years of shared risk. Works together on both
legitimate shifts and ring operations. Lunch every shift, drinks after.
Greet by first name. The smuggler knows Kael's personal tell
(looks left when lying) — this detail is critical for Phase 4.
- target: "npc:voss"
relationship_state: "Known"
access_tier: [insider, peer]
trust_level: "reliable"
relationship_type: "superior"
known_attributes:
name: "Voss"
role: "shift supervisor, ring-aware"
faction: "ring (passive)"
notes: >
Professional relationship. Low warmth. Voss takes a cut — the smuggler
knows this. Trusts Voss to provide scheduling cover. Would not trust
Voss with anything personal.
- target: "npc:nils-davan"
relationship_state: "Known"
access_tier: [insider]
trust_level: "reliable"
relationship_type: "superior"
known_attributes:
name: "Nils Davan"
role: "ring coordinator"
faction: "ring"
notes: >
Kael's brother. Ring coordinator. The smuggler receives instructions via
Devra, rarely directly. Respects Nils's authority, wary of his escalating
appetite for volume. Knows Nils is Kael's sibling — personal dimension.
- target: "npc:lera-sessik"
relationship_state: "Friendly"
access_tier: [insider, peer]
trust_level: "trusted"
relationship_type: "friend"
known_attributes:
name: "Lera Sessik"
role: "bar owner, ring-aware"
faction: "ring (peripheral)"
notes: >
Regular at The Last Shift for two years. Lera saves the smuggler's usual
seat. Warm, uncomplicated relationship within bar context. Knows Lera is
ring-aware but doesn't press her into operational details.
- target: "npc:renn"
relationship_state: "Known"
access_tier: [insider]
trust_level: "reliable"
relationship_type: "colleague"
known_attributes:
name: "Renn"
role: "courier, ring member"
faction: "ring"
notes: "Operational relationship. Knows Renn's routes. Professional, not warm."
- target: "npc:devra"
relationship_state: "Known"
access_tier: [insider]
trust_level: "reliable"
relationship_type: "colleague"
known_attributes:
name: "Devra"
role: "external contacts manager"
faction: "ring"
notes: "Channel for instructions from Nils. Operational distance maintained."
- target: "npc:maret-korr"
relationship_state: "Known"
access_tier: [peer, insider]
trust_level: "reliable"
relationship_type: "colleague"
known_attributes:
name: "Maret Korr"
role: "freight scheduler"
faction: "ring (aware)"
notes: >
Hub colleague. Handles scheduling. The smuggler knows Maret is aware of the
operation through her scheduling access — part of the cover structure.
- target: "npc:drin"
relationship_state: "Known"
access_tier: [insider]
trust_level: "reliable"
relationship_type: "colleague"
known_attributes:
name: "Drin Rosta"
role: "dock inspector, compromised"
faction: "ring (passive)"
notes: "Looks the other way during container inspections. Known asset."
- target: "npc:naia-tamm"
relationship_state: "Known"
access_tier: [peer]
trust_level: "reliable"
relationship_type: "colleague"
known_attributes:
name: "Naia Tamm"
role: "Kael's partner, teacher"
faction: null
notes: >
Known through Kael. Occasional bar encounters. Genuinely likes her. Would
not want Naia to know about the ring — protective instinct.
- target: "npc:torek-lintar"
relationship_state: "Known"
access_tier: [peer]
trust_level: "uncertain"
relationship_type: "colleague"
known_attributes:
name: "Torek Lintar"
role: "bar regular, ring-adjacent"
faction: "ring (peripheral)"
notes: >
Known from bar. Spending is conspicuous — new clothes, rounds for
strangers, better food than his salary explains. The smuggler keeps
distance. A security concern without being a confirmed problem.
- target: "npc:sera-venn"
relationship_state: "Known"
access_tier: [public]
trust_level: "uncertain"
relationship_type: "colleague"
known_attributes:
name: "Sera Venn"
role: "Commission field technician"
faction: "Commission"
notes: >
Recognized as Commission. Keeps distance reflexively. No personal
knowledge, no social interaction. Categorized as institutional risk,
not personal threat.
- target: "npc:pc-detective"
relationship_state: "Unknown"
access_tier: [public]
trust_level: "uncertain"
relationship_type: null
known_attributes:
name: null
role: "Commission liaison (warned by ring)"
faction: "Commission"
notes: >
May have been warned about a new Commission presence (via Kael or Voss).
Does not know name, face, or specific investigative mandate. Risk
assessment: moderate. Protocol: keep head down, act normal.
```
---
## access_permissions
```yaml
access_permissions:
can_unlock:
- tier: "public"
condition: "Always available"
max_npcs: "unlimited"
- tier: "peer"
condition: "RelationshipState reaches Known or Friendly"
max_npcs: "unlimited"
- tier: "insider"
condition: "Ring membership (pre-existing) or explicit trust invitation"
max_npcs: "ring network NPCs from session start; others via play"
structurally_locked:
- tier: "authority"
reason: >
The smuggler has no institutional credentials. Cannot flash a badge.
Cannot invoke institutional leverage. Even if the smuggler confronted
an NPC with physical proof, the confrontation operates in the peer/
insider register, not the authority register. This is the architectural
asymmetry with the detective.
- tier: "insider (non-ring)"
reason: >
Insider access is specific to the ring network. The smuggler cannot
unlock insider access with NPCs outside the ring's trust structure —
Lera's inner-circle, the Commission's internal channels, institutional
back-channels. The smuggler's world is bounded by the ring's social web.
```
---
## skills
```yaml
skills:
lattice_tier: "civilian"
movement_default: "walk" # D-053 MovementProfile
inventory_slots: 4 # D-065 (3 specific items + 1 spare)
evidence_display: "personal_notebook"
capability_flags:
- "ring_membership" # Treated as insider by ring NPCs from session start
- "ring_routes" # Knows smuggling corridors and timing windows
- "cargo_handling_cert" # Can interact with freight systems without security flags
- "shift_schedule_mastery" # Full knowledge of shift transition windows
starting_items:
- item_id: "smuggler_manifest_copy"
description: "Copy of a falsified manifest showing cargo discrepancy. Proves the ring's operational method."
leverage_type: "proof"
notes: >
Physical proof of cargo routing manipulation. Not useful as leverage
against ring members (they know). Potentially useful if the smuggler
needs to prove involvement to the detective — or use as leverage
against someone outside the ring.
- item_id: "smuggler_corridor_token"
description: "Maintenance corridor access token (Corridor B-7). Standard-issue for ring members."
leverage_type: "access"
notes: >
Physical proof of ring membership. If scanned by security, it reads as
an irregularly issued access credential — not automatically incriminating,
but suspicious. The detective finding this would be significant.
- item_id: "smuggler_comm_log"
description: "Personal comm log with coded operational messages (shift timing, container IDs)."
leverage_type: "leverage"
notes: >
Recorded coded messages between the smuggler and Kael. If decrypted, it
maps the operation. Carried because the smuggler hasn't thought to delete it.
A liability they don't know is a liability.
```
---
## presentation
```yaml
presentation:
monologue_pool: "smuggler"
evidence_display: "personal_notebook"
voice_register:
sentence_length: "fragments"
vocabulary: "concrete/sensory — trade jargon without explanation"
emotional_mode: "feeling-first, analysis second"
relationship_mode: "first-name/relational"
emotional_baseline:
state: "comfortable-tense"
description: >
Not happy, not miserable. The smuggler is managing a life with two faces
and has gotten good at it. The money helps. The team is solid. The risk is
real but felt manageable for two years. Comfort comes from knowing the
people, the routes, the rhythms. Tension lives in the background — ambient,
not acute. The day is better when everything goes as planned. The day turns
when something doesn't.
Protective by default. Thinks about people before systems. First instinct
when something looks wrong: who does this affect, and what can be done for
them? Institutions are backdrop. People are the world.
opening_monologue:
lines:
- "Morning shift. Recycled air and cargo lubricant. Home sweet home."
- "Kael's already at the dock. Good. The day's better when he's on shift."
- "Transition window opens in fifty minutes. Standard run — nothing complicated."
- "Just keep it clean. Keep it quiet. Another day."
emotional_payload: >
Comfort, routine, protectiveness. The smuggler is at home. The world is
manageable. Operational awareness runs in the background like breathing.
The player should feel settled before the first crack appears.
```
---
## Authoring Notes
**What makes the smuggler's experience distinct:**
- The ring is HOME. Every NPC in the ring network is a person they eat lunch with, drink with, cover for. The investigation, when it comes, doesn't feel like "a crime is being exposed" — it feels like "my people are in danger."
- The monologue is TERSE and relational. Two-word assessments. "Kael's good." "Voss is tense." No analysis of systems. Feeling first, named in physical terms.
- The emotional journey of the vertical slice moves from settled comfort to specific, personal betrayal. Not "the ring is threatened." "KAEL is lying to me."
**Voice anchors (mandatory reference for all smuggler monologue):**
1. *"Kael's already at the dock. Good. The day's better when he's on shift."*
2. *"Shift transition in thirty minutes. If Voss kept the schedule thin, the route's clear."*
3. *"Kael keeps checking his lattice. Waiting for a message? Not like him to be jumpy."*
4. *"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."*
5. *"Morning shift. Recycled air and cargo lubricant. Home sweet home."*
---
*Ticket #180 — Smuggler character build. v0.1 scope.*
+245
View File
@@ -0,0 +1,245 @@
# Character Definition Schema
**Ticket:** #179 | **Priority:** Critical | **Sprint:** 12
**Status:** v0.1 Draft
**Blocks:** #180 (Smuggler character build), #181 (Detective character build)
**Decisions referenced:** D-005 (single character, asymmetric information), D-016 (internal monologue), D-027 (vertical slice), D-028 (dialogue layers), D-032 (separate monologue pools), D-034 (THE FRIEND), D-035 (tag taxonomy), D-041 (knowledge graph), D-065 (smuggler inventory)
---
## 1. Purpose
This document defines the canonical schema for a playable character in v0.1. A playable character is **not just a perspective** — it is a complete starting state that determines what the player sees, who they know, what they understand, and what they can do. Every field in this schema has downstream content implications.
Two characters share the same world. Two different schemas produce two different games.
---
## 2. Schema Overview
A playable character definition has six top-level sections:
| Section | Purpose |
|---------|---------|
| `identity` | Who this character is in the world |
| `starting_knowledge` | What the character knows at session start (KG seed) |
| `starting_relationships` | Pre-existing relationships and their initial state |
| `access_permissions` | Which dialogue tiers are available at session start |
| `skills` | Capability flags affecting gameplay options |
| `presentation` | Voice, monologue register, and diegetic evidence display |
---
## 3. Identity
```yaml
identity:
canonical_id: "pc:{slug}" # e.g., "pc:smuggler", "pc:detective"
display_name: "{Name}" # Player-visible name (stub until #180/#181)
archetype: "{archetype}" # "smuggler" | "detective"
occupation: "{occupation}" # Diegetic job title
posting_duration: "{duration}" # How long they've been in Sova Transit District
faction: "{faction}" # Institutional affiliation if any
lattice_tier: "{tier}" # "civilian" | "professional" | "investigative"
description: >
1-2 paragraph description of who this character is in the world, from
a fictional universe perspective — not mechanical. Establishes emotional
baseline, social position, and what they stand to lose.
```
**Field notes:**
- `archetype` is used internally by the engine for monologue pool selection (D-032 hard partition). It is NOT exposed in the UI. Character feels like a person, not a game mechanic.
- `lattice_tier` determines which perception overlays are available (D-017). Civilian: no enhanced perception. Professional: diagnostic tools, analytical overlays. Investigative: full analytical suite.
- `posting_duration` affects which starting knowledge entries are plausible. A character who arrived 3 days ago cannot know the bar regulars by name.
---
## 4. Starting Knowledge
The starting knowledge section defines the Knowledge Graph seed (D-041) — the fact-set the character enters the session with. Facts not listed here are genuinely unknown at session start.
```yaml
starting_knowledge:
confidence_floor: "{tier}" # Minimum confidence level; see D-041
known_facts:
- key: "{fact.key}" # Namespaced fact key (e.g., "contraband.ring_exists")
confidence: "{tier}" # KnowsOf | KnowsDetails | KnowsEverything
source: "{source}" # "direct" | "secondhand" | "institutional" | "rumor"
notes: >
Optional. Authoring note on how the character holds this knowledge.
unknown_facts:
- key: "{fact.key}"
notes: >
Why this gap exists — what would they need to do to discover it?
```
**Fact key namespaces:**
| Namespace | Covers |
|-----------|--------|
| `contraband.*` | Ring existence, cargo types, supply chain, operational methods |
| `location.*` | Specific places, access routes, surveillance gaps |
| `investigation.*` | Manifest discrepancies, evidence, case file data |
| `relationship.*` | Who knows whom, trust levels, ring membership |
| `world.*` | Station geography, shift schedules, institutional structures |
| `person.*` | Specific facts about named individuals |
**Critical rule:** The unknown_facts section is as important as known_facts. It defines what the character CANNOT react to at session start and bounds what monologue triggers are legal.
---
## 5. Starting Relationships
Pre-existing relationships that determine initial RelationshipState (D-075) and AccessTier for every named NPC in the district. NPCs not listed here start at `Unknown` / `public` access.
```yaml
starting_relationships:
- target: "npc:{canonical_id}"
relationship_state: "{state}" # Unknown | Known | Friendly | PersonOfInterest | Hostile
access_tier: ["{tiers}"] # public | peer | insider | authority | hostile
trust_level: "{level}" # uncertain | reliable | trusted | hostile
relationship_type: "{type}" # colleague | friend | superior | contact | adversary
known_attributes:
name: "{name}" # Whether the character knows the NPC's name
role: "{role}" # What role the character assigns to this NPC
faction: "{faction}" # Whether the character knows the NPC's affiliation
notes: >
Short description of the nature of this relationship and how it was established.
```
**Relationship state → access tier mapping (default):**
| RelationshipState | Default Access Tier | Override possible? |
|-------------------|--------------------|--------------------|
| Unknown | public | Yes (authority overrides via badge/credentials) |
| Known | public, peer | Yes |
| Friendly | peer, insider | No insider without explicit history |
| PersonOfInterest | authority (detective) | Yes |
| Hostile | hostile | No |
**Character-specific overrides:** The smuggler has insider access to ring-member NPCs from session start (shared criminal history). The detective has authority access to institutional NPCs from session start (institutional credentials). These overrides are specified per-relationship, not as a global archetype flag (D-075).
---
## 6. Access Permissions
Global access tier capabilities at session start — what the character can unlock through gameplay versus what is structurally inaccessible.
```yaml
access_permissions:
can_unlock:
- tier: "{tier}"
condition: "{condition}" # When this tier becomes accessible
max_npcs: "{n | unlimited}" # How many NPCs can reach this tier
structurally_locked:
- tier: "{tier}"
reason: >
Why this tier is never accessible to this character across the
entire v0.1 vertical slice.
```
**v0.1 constraints:**
- **Smuggler** can unlock: public (all), peer (Known→Friendly), insider (ring-member NPCs from start; others locked). Cannot unlock: authority (no institutional credentials).
- **Detective** can unlock: public (all), authority (any Unknown/Known NPC via credentials), peer (Friendly NPCs only — currently Sera at session start). Cannot unlock: insider (ring dialogue never becomes available, even with investigation progress — the ring locks tighter, not looser, when the detective gets close).
This asymmetry is structural, not a content choice. It defines the two-keyhole architecture (D-027).
---
## 7. Skills
Capability flags that affect which game verbs are available and how gameplay systems respond.
```yaml
skills:
lattice_tier: "{tier}" # Copied from identity; determines perception modes (D-017)
movement_default: "{stance}" # Default stance from D-053 MovementProfile
inventory_slots: {n} # Physical inventory capacity (D-065)
evidence_display: "{format}" # "case_file" | "personal_notebook" (D-065)
capability_flags:
- "{flag}" # e.g., "manifest_access", "authority_credentials", "ring_routes"
starting_items:
- item_id: "{id}"
description: "{description}"
leverage_type: "{type}" # "proof" | "access" | "leverage"
notes: >
What this item proves, enables, or demonstrates.
```
**Capability flags vocabulary:**
| Flag | Effect |
|------|--------|
| `manifest_access` | Can request freight manifests from institutional NPCs |
| `authority_credentials` | Badge/identification; unlocks authority tier with Unknown/Known NPCs |
| `ring_routes` | Knows smuggling corridors and timing windows |
| `ring_membership` | Treated as insider by ring-member NPCs from session start |
| `analytical_lattice` | Pattern recognition overlay; flags behavioral anomalies at Careful stance |
| `cargo_handling_cert` | Can interact with freight systems without triggering security flags |
---
## 8. Presentation
How this character's internal voice, diegetic evidence display, and monologue pool are configured.
```yaml
presentation:
monologue_pool: "{archetype}" # Hard partition per D-032 ("smuggler" | "detective")
evidence_display: "{format}" # Inherited from skills; documented here for content authors
voice_register:
sentence_length: "{pattern}" # "fragments" | "complete" | "mixed"
vocabulary: "{style}" # "concrete/sensory" | "institutional/analytical"
emotional_mode: "{mode}" # "feeling-first" | "analysis-first"
relationship_mode: "{mode}" # "first-name/relational" | "surname-first/categorical"
emotional_baseline:
state: "{state}" # One-word summary of starting emotional state
description: >
3-5 sentences on the character's psychological starting position. This
is the foundation that monologue lines build from. Lines that contradict
this baseline are wrong.
opening_monologue:
lines:
- "{line}" # 3-5 authored lines; see D-039 wow moment #1
emotional_payload: >
What the player should feel in the first 60 seconds.
```
**Voice register is non-negotiable.** The smuggler thinks in fragments. The detective thinks in complete sentences. The moment an author writes a complete-sentence analysis for the smuggler or a terse fragment for the detective, they've broken the dual-lens contract. The voice register fields in this schema exist so downstream authors never have to re-read the authoring guide — the character build IS the authoring guide.
---
## 9. Schema Validation Rules
Before finalizing a character build, validate against these rules:
| Rule | Check |
|------|-------|
| **Knowledge symmetry** | For every `known_fact` about an NPC, that NPC file must have a corresponding `known_attributes` entry in the character's relationship |
| **Access tier consistency** | No `insider` access without `ring_membership` or equivalent history |
| **Monologue pool** | All authored monologue lines tagged with this character's `archetype` (hard partition) |
| **Unknown facts are real** | Every fact in `unknown_facts` must NOT appear in any authored monologue line at session start |
| **Opening monologue** | Opening lines must NOT reference facts the character doesn't yet know |
| **Inventory items** | All starting items must have corresponding world entities in content files |
| **Emotional baseline** | Opening monologue lines must match the character's `emotional_baseline.state` |
---
## 10. Cross-Reference: Downstream Documents
| Document | How This Schema Feeds It |
|----------|------------------------|
| `character-build-smuggler.md` (#180) | Full instantiation of this schema for the smuggler |
| `character-build-detective.md` (#181) | Full instantiation of this schema for the detective |
| `divergent-starting-knowledge.md` (#182) | Comparison table derived from both builds' `starting_knowledge` sections |
| `divergent-relationships.md` (#183) | Comparison table derived from both builds' `starting_relationships` sections |
| `npcs/pc-smuggler.yaml` | NPC mode representation when detective is the active player |
| `npcs/pc-detective.yaml` | NPC mode representation when smuggler is the active player |
| Content packs (#190, #191, #192) | Access tier decisions in this schema determine which content each character sees |
| Opening hooks (#299, #300) | Opening monologue section provides the first lines |
---
*Ticket #179 — Character definition schema. v0.1 scope. Blocks #180, #181.*
+247
View File
@@ -0,0 +1,247 @@
# Contraband Specification — Lattice Components & Supply Chain
**Ticket:** #321
**Author:** Miri
**Status:** canonical
**Last updated:** 2026-02-19
Reference: D-037 (contraband decision), D-036 (Sova setting), D-017 (perception modes)
---
## Purpose
This document defines the specific contraband being smuggled in the Sova Transit District scenario. It provides the supply chain, street terminology, moral dimensions, and detection/evasion methods that NPC dialogue and investigation mechanics depend on.
**Why this exists:** Mellanie cannot write ring NPC dialogue without knowing what the ring is smuggling and why people want it. Paula cannot design NPC motivations without knowing the moral stakes. The detective cannot investigate without knowing what they're looking for. This document is the shared reference.
---
## Part 1: Primary Contraband — Unlicensed Lattice Components
### What They Are
**Neural lattice components** are the hardware and firmware elements that make up the neural lattice implant — the Reach's universal human-machine interface. Every citizen with a lattice (i.e., almost everyone) has a configuration of components that determines their lattice tier and capability.
**Unlicensed lattice components** are lattice hardware and firmware obtained outside of Lattice Commission certification channels. This includes:
- **Enhancement modules:** Hardware add-ons that expand a Baseline lattice toward Augmented-tier capability. Improved sensory processing, faster Meridian query response, expanded information overlay capacity.
- **Aftermarket firmware:** Software modifications that unlock capability built into the hardware but blocked by Commission certification tiers. The hardware is the same; the firmware determines what it can do.
- **Capability-specific mods:** Targeted modifications for specific use cases — enhanced audio discrimination, improved spatial mapping, pattern recognition support. Often developed for occupational purposes in systems where Commission access is expensive.
- **Medical-grade neural replacements:** Certified neural lattice replacement components diverted from legitimate Syndic supply chains. Priced far below standard Syndic channels because they're off-manifest.
### Commission Regulatory Framework
The Lattice Commission regulates neural lattice technology under Reach-wide mandate. The certification system works as follows:
- **Baseline tier:** Standard-issue. Commission-certified, universally available, low-cost. Factory default capabilities only.
- **Augmented tier:** Institutional or premium. Requires Commission recertification, documented justification (occupational need, institutional role), and ongoing compliance monitoring. Significantly more expensive.
- **Enhanced tier:** Specialist. Requires extensive justification, Commission review board approval, and continuous monitoring. Rare.
- **Transcendent tier:** Theoretical or classified. Not available through any public channel.
The regulation exists for stated reasons: safety (unregulated hardware poses health risks), security (enhanced lattice capability creates surveillance and data-access concerns), and economic stability (Commission certification fees are a significant revenue stream).
The regulation also exists for unstated reasons: the Commission's tier pricing structure benefits Syndic manufacturers who hold Commission approval. Aftermarket components break the Commission's market control.
### Why People Want Unlicensed Components
The demand is not criminal in motivation. Most buyers want lattice components for ordinary reasons:
**Occupational capability:** A freight worker whose job involves spatial reasoning and pattern detection can do that job better with Augmented-tier pattern recognition support. Commission-certified upgrade: expensive, requires institutional sponsorship. Aftermarket equivalent: costs a month's wages, no sponsorship required, available through the ring.
**Medical necessity:** Neural lattices degrade over time, especially in older models and in people who've done intensive lattice work for years. Replacement components are essential healthcare. Syndic pricing for lattice replacements is calibrated to institutional buyers (hospitals, Commission offices). For individuals — particularly working-class individuals without employer-sponsored medical plans — the legitimate price is prohibitive. Aftermarket replacements work the same way for a fraction of the cost.
**Personal autonomy:** Some people simply believe they have the right to determine their own lattice configuration without Commission approval. The Guardians of Severance take this position philosophically. The ring's customers mostly take it practically.
**Access inequality:** The Commission's tier system means that institutional workers (investigators, administrators, Syndic employees) get enhanced lattice capability through their employer. Workers outside institutional employment don't. Aftermarket components are one way the non-institutional population accesses capabilities the institutional population considers routine.
### The Moral Dimension
The ring is smuggling **access**, not weapons.
No one is importing military-grade targeting systems or hostile surveillance technology. The contraband is the same components that the detective uses, bought through a different channel, without Commission paperwork.
The moral question the player faces is not "is it wrong to trade in harmful things?" It is "is it wrong to provide people with things the Commission has decided they're not allowed to have, for reasons that primarily serve the Commission's business interests?"
This question does not have a clean answer. The ring knows it doesn't have a clean answer. The ring's members have made their peace with working in the gray zone. Some of them use their own products.
**Key moral detail:** Naia (Kael's partner) uses a diverted medical-grade lattice replacement. Her original lattice degraded faster than expected — occupational wear. The legitimate replacement was priced at 14 months of combined wages. The aftermarket replacement worked. This is one of the ring's own. This is, in part, why Kael is in the ring at all. The detective doesn't learn this until they have Kael's trust.
---
## Part 2: Secondary Contraband
### Severance Counter-Surveillance Technology
**What it is:** Hardware and software tools designed to evade, disrupt, or spoof Meridian monitoring. Produced and distributed by the Guardians of Severance (the counter-surveillance movement) but also trafficked commercially to buyers who have no ideological alignment — they just want privacy.
**Why it's in the ring:** The ring uses Severance tech for operational purposes. They also move product for Severance-adjacent customers. It's not their primary business, and they don't advertise it, but if someone in their network needs a comm scrambler or a lattice signature mask, the ring can provide.
**Moral dimension:** Severance tech protects privacy, which is a legitimate interest. It also enables the ring's operations, which is less sympathetic. And it undermines Commission surveillance, which is either a feature or a bug depending on your perspective.
**Commission classification:** Category 2 violation — possession of surveillance countermeasure technology. More serious than unlicensed lattice components. The ring is careful with this part of their inventory.
**Detection note:** Severance tech is harder to scan for than lattice components — it's designed not to be found. The detective's augmented lattice can detect the signature of active Severance scrambling, but passive Severance hardware (not currently running) doesn't trigger. The smuggler knows this.
### Off-Manifest Biologicals
**What they are:** Medical supplies, pharmaceutical compounds, biological cultures, and occasionally re-embodiment materials that are diverted from official Syndic supply chains and moved through informal channels.
**Why they're off-manifest:** Multiple reasons:
- Syndic pricing for medical supplies creates a gray market, same as lattice components
- Some biological materials are classified as controlled under Commission or Concord Assembly pharmaceutical regulations — legitimate reasons for control, but regulations calibrated for Syndic-scale distribution, not individual access
- Re-embodiment backup materials are tightly controlled — owning a personal backup of your own cognitive state is technically legal but creating and moving the physical medium outside certified facilities is not
**Why the ring handles them:** The same logistics infrastructure that moves unlicensed lattice components can move biologicals. The margin is smaller, the risk is comparable, and some of the same customers want both.
**Moral dimension:** This is the murkiest category. Moving medical supplies to people who can't afford Syndic pricing — sympathetic. Moving re-embodiment backup materials to people who want personal insurance against death without going through the certified system — complicated. Moving materials of unknown provenance through an unregulated supply chain without knowing what they actually are — this is where the ring's "we don't ask" culture becomes a liability.
**Volume in v0.1:** Minor fraction of total cargo. The ring focuses on lattice components. The biologicals are occasional, handled only through a specific trusted member, and never discussed in the same conversation as the lattice business.
---
## Part 3: Supply Chain
### Upstream — Manufacture and Acquisition
**Aftermarket lattice component manufacturers:**
- Primarily operate in systems with lighter Commission presence — frontier and deep-frontier manufacturing, occasionally mid-Reach systems with local regulatory variance
- Not criminal enterprises in their origin systems — they operate legally where they are; the illegality is at the destination end where Commission certification applies
- Some Syndic-adjacent: subsidiaries or licensing agreements that produce off-spec components under plausible deniability
- Products are shipped in legitimate freight that also carries legitimate cargo — the contraband fraction of any given container transit is small
**Acquisition chain:**
- Coordinator receives orders from the distribution network
- Acquisition contacts at origin-system freight handlers arrange for components to be included in outbound shipments with doctored manifests
- The doctored manifests show legitimate cargo codes; the actual components are repacked into containers that look right
**Detection risk at origin:** Low. Commission enforcement is concentrated at destination hubs (where the Commission has jurisdiction), not at frontier or mid-Reach origin points.
### Midstream — The Ring's Role
**The ring's function is transit, not manufacture or retail.** They are a distribution node in a larger network. Their specific value is:
- Access to Sova's span gate (a legitimate freight chokepoint)
- Relationships with dock workers who control scanner routing
- Knowledge of scanner blind spots and maintenance windows
- The ability to blend contraband into legitimate freight flows
**Operational mechanics:**
1. **Notification:** Coordinator receives arrival details through off-Meridian channels. This information is passed to ring members through code language on Meridian or through physical dead drops.
2. **Preparation:** Ring member verifies scanner bay availability and status. Flags maintenance windows. Identifies lowest-scrutiny route through the hub.
3. **Receipt:** Contraband arrives in a standard freight container. On the manifest, it's listed as a legitimate cargo category (machine parts, agricultural supplies, industrial components — whatever is plausible for the transit route).
4. **Routing:** Ring member routes the container to a scanner bay with known compliance. Either a bay with a calibration blind spot (specific scanner types can't detect lattice components in certain packing configurations) or a bay operated by a ring-aware worker who logs a clean scan.
5. **Handoff:** Container moves to staging. Ring member marks it for the receiving contact's pickup. The receiving contact has legitimate access to staging (Syndic freight broker, commercial customer, station resident with freight delivery).
6. **Payment:** Through the coordinator, via off-manifest routes. The ring doesn't handle large cash-equivalent transactions on Meridian. Payment is managed in chunks, routed through legitimate purchases.
**What can go wrong:** The ring's operational security depends on nothing standing out. Problems occur when:
- A worker outside the ring notices something — wrong container in wrong bay, worker behavior that's off-routine
- A scanner bay does a spot-check instead of the expected throughput scan
- A ring member's behavior attracts attention (Torek's recent carelessness is an example)
- The detective's manifest analysis flags anomalies in the shipping record
### Downstream — Distribution
**The ring's customers are not known to most ring members.** The coordinator manages the downstream. Ring members know they're moving components; they don't know who ends up with them.
From what's inferrable:
- Local distribution: station residents, some district workers, a few customers from other districts on Sova
- Off-station: the span gate delivers cargo to the planetary freight depot on Velen. Some product continues via Velen's distribution network to system population centers.
- Regional: occasional higher-value shipments routed through the system to other ring nodes beyond Krenn System. The ring doesn't manage this leg.
**The v0.1 scope:** The detective's investigation is concerned with the Sova Transit District operation. The broader network is context, not target. Discovering that the ring is part of a larger network is an investigation milestone, not the investigation conclusion.
---
## Part 4: Terminology Reference
### Formal/Institutional Terms (Detective + Commission)
| Term | Meaning |
|---|---|
| **Unlicensed lattice components** | Hardware/firmware outside Commission certification |
| **Category 3 violation** | Commission classification for lattice tampering |
| **Manifest discrepancy** | Difference between declared and actual cargo |
| **Cargo routing anomaly** | Container reaching destination via non-standard path |
| **Off-manifest transfer** | Goods moving without Meridian-logged documentation |
| **Surveillance countermeasure equipment** | Severance tech (formal term) |
| **Category 2 violation** | Commission classification for Severance possession |
### Street/Ring Terms (Smuggler + NPC Insider)
| Term | Meaning | Context |
|---|---|---|
| **components** | Unlicensed lattice mods | "Got a components run tonight." |
| **inserts** | Same as above, slightly more specific | "The inserts are in Bay 7." |
| **upgrade kits** | Packaged lattice enhancement sets | "They ordered two upgrade kits this week." |
| **medical** | Medical-grade lattice replacements specifically | "This one's medical — handle it carefully." |
| **scramblers** | Severance tech | "He wanted a scrambler, I told him to ask the coordinator." |
| **the product** | Generic for whatever the ring is moving | "The product moves at 0200." |
| **a clean manifest** | Successfully falsified cargo documentation | "Manifest is clean." |
| **a bad can** | A container that's been flagged or is otherwise problematic | "Don't touch that one — it's a bad can." |
| **the coordinator** | The ring's off-site manager | Referred to obliquely. Never by name in non-secure channels. |
---
## Part 5: Detection and Evasion
### How the Commission Detects Contraband
**Manifest analysis:** Comparing declared cargo against shipping codes, routing patterns, and commercial plausibility. Anomalies flag for review. The detective's augmented lattice can run this analysis on a per-container basis; the Commission system runs it across all incoming freight as a pattern recognition exercise.
**Physical scanning:** Standard freight scanners check for mass discrepancies, specific material signatures, and packing anomalies. Unlicensed lattice components are difficult to detect via standard scan — they read as generic electronics without specific interrogation protocols. Enhanced scan (requiring cause and authorization) can identify lattice component signatures specifically.
**Lattice signature analysis:** An enhanced lattice running the right query can detect the electromagnetic signature of recently modified lattice hardware in proximity. This requires close physical proximity and active scanning — not passive detection. The detective could deploy this if they have a specific target and authorization.
**Behavioral surveillance:** Watching who goes where, who talks to whom, who handles certain containers. The Meridian logs access patterns in institutional spaces (Commission-authorized areas). Manual surveillance in non-Meridian spaces is the detective's work.
### How the Ring Evades Detection
**Manifest doctoring:** The primary defense. Contraband is listed as plausible legitimate cargo. Batch sizes are kept below the threshold that triggers automatic enhanced review. Shipping routes use established legitimate freight paths.
**Scanner routing:** Contraband containers are routed to scanner bays known to have calibration limitations or operated by ring-aware workers. The ring maintains awareness of which bays are running which protocols on any given day.
**Timing:** Operations occur during shift transitions (when attention is divided), during maintenance windows (when some scanner systems are offline), and during high-volume pushes (when individual container scrutiny is lower).
**Operational security:** No discussion of specifics on Meridian-logged channels. Limited need-to-know within the ring. The coordinator knows the full picture; individual ring members know only their operational role.
**Normalcy as cover:** The ring's greatest protection is that everything looks normal. No late-night break-ins. No dramatic meetings in dark corridors. Everything happens within the ordinary flow of freight operations. The detective is looking for anomalies in a sea of normal; the ring works to minimize the anomalies.
---
## Part 6: Gameplay Implications
### For the Smuggler
The smuggler's daily work IS the ring's operation. They don't separate their legitimate job from their criminal one — they do both simultaneously. The investigative element for a smuggler playthrough is not "will I get caught" but "how do I keep everything in balance as the detective's investigation narrows."
The three physical inventory items (D-065) connect directly to this:
1. **Manifest copy** — proves cargo discrepancy, but having it means carrying evidence of the operation
2. **Corridor access token** — proves ring membership, physical risk if discovered
3. **Personal comm log** — recorded conversations that prove network connections, the most dangerous of the three
### For the Detective
The detective's investigation begins with manifest analysis and behavioral observation. The progress trajectory is:
- Level 1: Notice statistical anomalies in freight manifests (requires augmented lattice + access)
- Level 2: Connect specific containers to specific workers (requires surveillance + NPC knowledge)
- Level 3: Establish the supply chain (requires either informant cooperation or direct evidence)
- Level 4: Identify the coordinator and the broader network (requires trust-gated disclosure or confrontation)
The detective does not need to know everything in this document. They discover it through investigation. This document is the ground truth that their knowledge state moves toward.
### Moral Ambiguity in Practice
The intent is that a player who investigates Kael seriously — who learns about Naia's medical situation, who understands why Kael is in the ring — should not feel straightforwardly good about bringing Kael down. The smuggler player, similarly, should not feel entirely comfortable with their complicity once they understand the ring's full scope.
The contraband is designed to be sympathetic at its core (medical necessity, access inequality) while creating complications the deeper you look (the biologicals of unknown provenance, the Severance tech enabling things the player may not endorse). Neither character has clean hands. Neither character has indefensible ones. That's the design intent.
---
*This document is the canonical contraband reference for v0.1 content. NPC dialogue involving ring operations, cargo handling, or investigation should cross-reference here for terminology consistency. If a content gap is identified — a term needed that isn't here, a scenario not covered — flag to Miri for addition.*
*Miri — Sprint 12*
@@ -0,0 +1,272 @@
# Contradiction Arc — THE FRIEND Pattern
**Ticket:** #332
**Source:** D-034 (THE FRIEND ECS spec), D-039 (wow moments — #3), D-041 (contradiction detection), D-028 (dialogue architecture)
**Derived from:** Kael Davan (smuggler's FRIEND, #297) + Sera Venn (detective's FRIEND, #298)
**Date:** 2026-02-19
---
## Overview
THE FRIEND is a reusable NPC pattern. One per playable character. The FRIEND is the emotional centerpiece of v0.1 — the NPC the player names when asked "who did you feel conflicted about?" (D-027 success criterion #3).
The pattern has one invariant: **the contradiction is observable through physical space or behavioral pattern, never through dialogue.**
The player discovers the truth by watching, not asking. Dialogue confirms or deflects — it never reveals first.
---
## The Five Phases
| Phase | Duration | NPC State | Player Experience |
|-------|----------|-----------|-------------------|
| 1. **Warmth** | 0-10 min | Baseline positive. No deviations. | This NPC is your best relationship. The day is better when they're here. |
| 2. **Background** | 10-15 min | Subtle behavioral flag begins. Not enough to read yet. | Social context accumulates. You learn who this person is to the world. |
| 3. **First Contradiction** | 15-20 min | Observable anomaly fires. Color shifts from green to amber. | Something doesn't fit. The belief you formed in Phase 1 is contradicted. |
| 4. **Confrontation Opportunity** | 20-25 min | Deflection. The TELL fires during the deflection. | You ask. They don't answer. But you notice something. |
| 5. **Contaminated Trust** | 25-30 min | Surface behavior returns to baseline. | The FRIEND acts normal. But you're watching now. Every Phase 1 moment is retroactively suspect. |
### Phase 1 requirements
- FRIEND must exhibit genuine warmth — greet the player first, share information willingly, reference personal connection
- At least one Phase 1 scene establishes an emotional anchor (shared meal, personal reference, easy banter)
- Routine is fully normal — punctual, sociable, consistent
- No behavioral flags — anything in Phase 1 must be readable as normal in isolation
### Phase 2 requirements
- FRIEND continues warm baseline but begins accumulating subtle signals
- Subtle signals must be individually readable as normal: leaving early has a plausible reason, checking lattice is common behavior
- Phase 2 establishes context that will retroactively charge Phase 3: introduce the relationship that's being protected, establish the FRIEND's expertise/role, build emotional depth
- At least one Phase 2 scene introduces the third party the FRIEND is protecting (Naia for Kael, Naia-via-Sera for the detective's arc)
### Phase 3 requirements — the contradiction
**Type A: Spatial Anomaly**
Character is in a location they have no legitimate reason to be in, at an illegitimate time, doing something outside expected protocol. Single observation. Sudden alarm.
*Kael example:* In Corridor B-7 (restricted, not his zone) after his shift, meeting an unrecognized person outside ring protocol. The smuggler sees it and the prior belief ("Kael operates within ring protocol") is immediately contradicted.
**Type B: Behavioral Pattern**
Character exhibits a consistent behavioral response across multiple observations. No single observation is conclusive — the third occurrence completes the pattern. Gradual recognition.
*Sera example:* Leaves the bar within 2-3 minutes every time Torek arrives. Three occurrences, three different stated reasons. Detective training fires on the third: this is not coincidence.
**Pattern B requires three observations** before the contradiction fires — one is nothing, two is coincidence, three is data.
**Both types:**
- The contradiction must be spatially or behaviorally discoverable — something the player's character would notice from observation
- Dialogue does not reveal the contradiction — the FRIEND either doesn't know the player observed, or has a plausible explanation that doesn't collapse the contradiction
- Color shifts from green (Friendly) to amber (PersonOfInterest) when contradiction is observed
- Knowledge graph: contradiction fact set to `suspects` level, triggering contradiction-detection monologue
### Phase 4 requirements — the confrontation opportunity
Player initiates dialogue about the Phase 3 observation.
**Required structure:**
1. Player asks about the anomaly (spatial presence, behavioral pattern, etc.)
2. FRIEND deflects — gives a plausible answer that doesn't confirm or deny
3. **The TELL fires during the deflection.** Not before, not after — during the deflection. The tell is how you know they're hiding something.
4. The tell must be:
- Established or at minimum detectable before Phase 4 (player can already know this tell)
- Observable through the character's behavior, not through dialogue content
- Something that becomes clearer in retrospect once the player knows to watch for it
**Key constraint:** The FRIEND does not confess in Phase 4. The confrontation reveals the lie, not the truth. The truth comes later — either through investigation, a separate disclosure event, or the player's own inference.
### Phase 5 requirements — contaminated trust
Surface behavior returns to Phase 1 baseline. The FRIEND continues as before.
**What changes:**
- Every prior Phase 1 moment is retroactively ambiguous. Was the warmth genuine, or instrumental?
- New monologue lines fire that filter current observations through doubt: "Same old Kael. Same jokes, same routine. ...What are you not telling me?"
- Relationship state: PersonOfInterest persists. Color stays amber.
- Three resolution paths open (see below)
**The answer to "was the warmth genuine?"** is always both — yes AND they were hiding something. This ambiguity is the emotional payload. The FRIEND pattern does not deliver a villain. It delivers a person making a hard choice.
---
## Resolution Paths
Three paths available post-Phase 4. None is correct. All carry costs.
| Path | Action | What It Tests |
|------|--------|---------------|
| **Understanding** | Gentle confrontation — player asks again, creates space for disclosure | Willingness to extend trust despite evidence |
| **Betrayal** | Report to authority (ring leadership / Commission) | Institutional loyalty over personal loyalty |
| **Observation** | Watch and wait — build knowledge without confronting | Information-gathering over emotional resolution |
**Design constraint:** No path delivers a clean outcome within v0.1. Understanding may get partial confession but not full truth. Betrayal removes the FRIEND from play but doesn't close the case. Observation extends the ambiguity indefinitely.
---
## Tell Progression
Every FRIEND NPC requires at least three tells, each with different observability:
| Tell Level | Observability | Timing | Purpose |
|-----------|---------------|--------|---------|
| **Peripheral** | Visible to any observer with some attention | Phase 2 onset | Subconscious accumulation — player may notice retroactively |
| **Forward** | Visible to focused observers / in direct interaction | Phase 3-4 | Confirmation tell — fires when the player is paying attention |
| **Direct** | Confirmed in post-conversation monologue | Phase 4 | The TELL — fires during the deflection, seals the case |
### Kael's tells
1. Increased lattice checking (peripheral, Phase 2)
2. Shortening social interactions / leaving early (any, Phase 2-3)
3. **Looks left when lying** (forward, Phase 4 — the personal tell the smuggler knows from years of friendship)
4. Unauthorized spatial presence in B-7 (any, Phase 3 — the contradiction itself)
### Sera's tells
1. Extended Commission kiosk time (peripheral, Phase 2)
2. Overcompensation with information (forward, Phase 2-3)
3. **Topic deflection — redirects rather than goes silent** (forward, Phase 4 — controlled delivery reveals the redirect IS the tell)
4. Leaves bar when Torek arrives (any, Phase 3 — the contradiction itself, cumulative)
---
## Knowledge Graph Mechanics
What the FRIEND arc requires from the knowledge system (D-041):
### Phase 1-2: baseline
```
player.KnowledgeGraph[friend_npc]:
confidence: KnowsOf # Knows who they are and their role
relationship: Friendly # Warm relationship established
state: Active
known_attributes.trust_level: trusted
```
### Phase 3: contradiction fires
```
# The contradiction event is observed
player.KnowledgeGraph.facts[investigation.{friend}_contradiction_event]:
confidence: Suspects # "Something's off"
state: Active
# If prior knowledge entry conflicts with observation:
player.KnowledgeGraph[friend_npc]:
state: Contradicted # Both entries get this — D-041 contradiction detection
# Triggers: ContradictionEvent → monologue system → relationship review
relationship: PersonOfInterest # Color shifts amber
```
### Phase 4: tell confirmed
```
# Tell confirmed in post-conversation monologue
player.KnowledgeGraph.facts[behavioral.{friend}_evasion]:
confidence: Direct # Set during conversation, Direct = "I just saw this"
state: Active
# Contradiction fact upgraded
player.KnowledgeGraph.facts[investigation.{friend}_contradiction_event]:
confidence: KnowsOf # Now more than suspicion — confirmed deliberate
```
### Phase 5: contaminated trust
```
player.KnowledgeGraph[friend_npc]:
confidence: KnowsDetails # You know they're hiding something specific
relationship: PersonOfInterest # Stays amber — cannot return to Friendly without deep trust event
state: Contradicted # Persists until resolution path completes
```
---
## Content Requirements Per FRIEND NPC
### Authored lines (no generation expansion)
| Content Type | Count | Notes |
|-------------|-------|-------|
| Dialogue — surface tier | 8-10 | Phase 1-5 casual lines, available at any trust |
| Dialogue — real tier | 6-8 | Phase 1-3 insider/operational content |
| Dialogue — secret tier | 3-5 | Phase 5 disclosure (if Understanding path taken) |
| Dialogue — confrontation options | 4-6 | Phase 4 initiation + FRIEND response variants |
| Monologue — about FRIEND (player char) | 12-15 | Full arc coverage; character partitioned (D-032) |
| Tell observation lines | 5-8 | One per tell, per observability tier |
| Contradiction discovery monologue | 3-4 | Fires when Phase 3 contradiction is observed |
| Post-contradiction monologue | 4-6 | Phase 5 filtered-through-doubt lines |
| Dual-lens notes (not in engine) | 1-2 per character | For authoring consistency |
**Total per FRIEND NPC: ~70-100 authored lines.** No generation expansion — every line hand-authored.
### Dual-lens authoring
The same FRIEND NPC is experienced differently by each playable character. Authors must write:
- Primary arc (bonded character): full emotional weight, personal tell knowledge, relationship history
- Secondary observation (other character): distant, filtered through their lens — the FRIEND is "just" a Commission worker / dock worker until patterns accumulate
**The dual-lens makes the FRIEND arc pay off on replay.** Playing the other character, you now understand Sera's avoidance or Kael's tension from a completely different angle.
---
## Checklist for New FRIEND NPCs
Use this checklist when creating a new FRIEND for a future playable character.
### Design requirements
- [ ] FRIEND motivation chosen: what drives the concealment? (Kael: protect partner, Sera: protect friend)
- [ ] Contradiction type chosen: spatial anomaly (Type A) or behavioral pattern (Type B)
- [ ] Third party identified: who the FRIEND is protecting, and why this creates the contradiction
- [ ] Sympathetic motivation: FRIEND has a reason the player can understand, even if they can't endorse it
- [ ] No clean resolution: the FRIEND arc ends in ambiguity, not revelation or catharsis
- [ ] Triangle integration: FRIEND participates in at least one active triangle (D-024)
- [ ] Dual-lens: secondary character observation notes written
### Tell requirements
- [ ] Three tells defined: peripheral + forward + direct
- [ ] Peripheral tell established in Phase 2 monologue (retroactive recognition)
- [ ] Direct tell fires during Phase 4 deflection (post-conversation monologue)
- [ ] Tell known by player character from prior context (friendship history or professional training)
### Phase requirements
- [ ] Phase 1 includes genuine emotional anchor (shared activity, personal reference, easy rapport)
- [ ] Phase 2 introduces the third party the FRIEND is protecting
- [ ] Phase 3 contradiction is physically/behaviorally observable (not dialogue-revealed)
- [ ] Phase 3 has required observation count: Type A = 1, Type B = 3
- [ ] Phase 4 deflection gives plausible answer that doesn't collapse the contradiction
- [ ] Phase 5 has three distinct resolution paths, none delivering clean closure
### Content requirements
- [ ] ~70-100 authored lines total (no generation expansion)
- [ ] Monologue pools hard-partitioned by character (D-032)
- [ ] Contradiction discovery monologue written (3-4 lines, fires on Phase 3 observation)
- [ ] Post-contradiction filtered-through-doubt monologue written (4-6 lines)
- [ ] All prerequisite fact IDs from knowledge-vocabulary-v01.md — no invented strings
### Systems integration
- [ ] FRIEND is in the knowledge graph at `KnowsOf + Friendly` from Phase 1
- [ ] Contradiction fact ID defined in knowledge-vocabulary-v01.md (e.g., `investigation.{friend}_contradiction_event`)
- [ ] Tell fact ID defined (e.g., `behavioral.{friend}_evasion`)
- [ ] Phase transitions map to relationship state changes (Friendly → PersonOfInterest)
- [ ] Resolution paths have distinct knowledge state outcomes
---
## Why This Pattern Works
**Single mechanical insight:** Asymmetric information creates emotional weight. The player has information (the observation) that the FRIEND doesn't know the player has. The FRIEND acts normally. The player watches through a new lens. That gap IS the dramatic irony.
**Systems framing:** The FRIEND arc is asymmetric information mechanics applied to trust. The player's knowledge graph and the FRIEND's knowledge graph are out of sync. The moment they sync — when the FRIEND realizes the player knows — is where the confrontation lives.
**Emergent replayability:** The dual-lens design means the FRIEND arc reads completely differently on a second playthrough as the other character. Sera's tells mean something different when you know what she's hiding. Kael's warmth is more painful when you know the exit attempt will fail. This is emergent story from system interaction, not scripted revelation.
---
*Design document. Reusable pattern template for all future FRIEND NPCs.*
*Derived from: Kael Davan (#297), Sera Venn (#298).*
*Full spec: D-034, D-039, D-041*
+296
View File
@@ -0,0 +1,296 @@
# Cultural Generation Guide — The Settled Reach
**Ticket:** #189
**Author:** Miri
**Status:** canonical
**Last updated:** 2026-02-19
---
## Purpose
This guide provides the framework for generating culturally consistent content in The Settled Reach. Every named district, NPC, and piece of authored dialogue reflects a specific cultural context. This guide codifies how to derive that context from first principles so that any content author — writing a dockworker or a station administrator, a bar greeting or a confrontation line — can produce work that feels like it belongs.
The Sova Transit District in the Krenn System is the first concrete instance of this framework. All examples in this guide are drawn from Sova. Subsequent settings use the same framework applied to different parameters.
---
## Part 1: The Framework — Five Dimensions of Cultural Context
Every location in The Settled Reach has cultural context along five dimensions. These are not rigid categories — they blend and interact. But knowing them for a setting before writing a single line of dialogue prevents inconsistency.
### Dimension 1: Reach Position
**Range:** Core → Mid-Reach → Frontier → Deep Frontier
This determines the age of institutions, the density of infrastructure, and the relationship between residents and authority.
| Position | Infrastructure | Institutional Presence | Resident Attitude to Authority | Setting Feel |
|---|---|---|---|---|
| **Core** | Mature, well-maintained | Heavy. Commission offices, Concord representatives, Syndic headquarters | Deferential-to-compliant. Authority is a daily reality. | Corporate. Managed. Politics visible. |
| **Mid-Reach** | Functional, mixed-era | Moderate. Commission terminals, periodic visits, field investigators | Pragmatic-to-skeptical. Authority exists but isn't everywhere. | Working. Lived-in. Rules enforced selectively. |
| **Frontier** | Sparse, improvised | Light. Syndic representatives, occasional Commission audit | Independent-to-resistant. Authority is a distant abstraction. | Improvised. Tight-knit. Self-reliant. |
| **Deep Frontier** | Minimal | Nominal or none | Sovereign. Authority is whatever the community enforces. | Harsh. Communal. Rules of necessity. |
**Sova Transit District:** Mid-Reach. Commission presence is intermittent — enough to maintain order, not enough to catch everything. The district's residents are pragmatic about authority: they comply when observed, continue as normal when not.
---
### Dimension 2: Economic Base
**Range:** Freight → Resource Extraction → Manufacturing → Service → Research → Mixed
Economic base determines daily rhythms, vocabulary, and the social pressures that create narrative stakes.
| Economic Base | Daily Rhythm | NPC Concerns | Social Pressure | Narrative Hook |
|---|---|---|---|---|
| **Freight** | Span gate schedules. Shift rotations. Cargo waves. | Manifests, container counts, shift assignments, dock schedules. | Wages stable but tight. Extra income tempting. | Manifest discrepancy. Smuggling access. |
| **Resource Extraction** | Mining shifts. Equipment maintenance cycles. | Equipment reliability, extraction quotas, safety protocols. | Dangerous work, irregular income, company-town dynamics. | Workplace accident cover-up. Union pressure. |
| **Manufacturing** | Production cycles. Quality control. | Production quotas, supply chains, quality flags. | Line work monotony, automation anxiety, middle-management tension. | Sabotage. Corporate espionage. |
| **Service** | Tourist/commercial traffic. | Tips, regulars, reputation management. | Income dependent on traffic volume. Reputation fragile. | Blackmail. Theft. Information sale. |
| **Research** | Project deadlines. Funding cycles. | Research progress, institutional politics, publication credit. | Competitive, status-driven, secrecy around results. | Data theft. Fabrication. Factions. |
**Sova Transit District:** Freight primary. The span gate transit schedule structures everything. NPC routines, the ring's operational calendar, and social patterns (when the bar fills, when corridors are quiet) all derive from freight rhythm.
---
### Dimension 3: Settlement Age and Character
**Range:** New Colony (< 20 years) → Established (20-80 years) → Mature (80-200 years) → Old (200+ years)
Settlement age determines architectural character, generational memory, and whether residents think of themselves as pioneers or locals.
| Age | Architecture | Cultural Identity | Generational Memory | Relationship to Origin |
|---|---|---|---|---|
| **New Colony** | Prefab, temporary-feeling. Little personalization. | Identity still forming. People compare everything to where they came from. | Recent. First-generation. Stories of the journey. | Strong nostalgia for origin system. |
| **Established** | Prefab base showing modification. Layers visible. Some personalization, some institutional standardization. | Local identity forming but not solid. Mix of origin-cultures. | Mixed. Parents remember old places; children only know here. | Ambivalent. Here is home, but parents' memories linger. |
| **Mature** | Mixed construction eras clearly visible. Original structure heavily modified. Strong local character in older sections. | Distinct local identity. People think of themselves as "from here." | Distant. The origin is history, not memory. | Local pride. Skepticism of core-system assumptions. |
| **Old** | Accretive. Original construction may be invisible under layers. Idiosyncratic. | Deeply rooted identity. History is ambient. | Institutional. Stories are embedded in place-names and customs. | The origin system is foreign. "We are our own thing." |
**Sova Transit District:** Established-to-Mature. The district is ~40 years old on a station settled ~180 years ago. The district itself shows layered construction — prefab base, retrofitted sections, personalized details. Local identity is present but not deep. Workers think of themselves as Sova people, not Krenn people; but they don't have the rootedness of a settlement in its third or fourth generation.
---
### Dimension 4: Cultural Reference Point
The cultural reference point is the system of origin for the majority population, combined with how many generations of local drift have occurred.
This determines:
- **Naming conventions** — how names sound, what they reference, whether family names matter
- **Social norms** — eye contact, titles, how trust is established
- **Relationship to technology** — whether lattice use is stigmatized, normalized, or a source of pride
- **Attitudes toward the Commission** — reverence, compliance, resentment, or active evasion
**Krenn System Cultural Profile (established, working-class mid-Reach):**
- **Names:** Compact, consonant-heavy, first-name-primary in social contexts. Family names exist but are used formally (official documents, institutional contexts, introductions to strangers). Nicknames common among colleagues. Off-system names (longer vowels, different consonant clusters) stand out subtly but aren't cause for suspicion — just quietly noted.
- **Social norms:**
- Eye contact is normal during conversation, brief during passing. Sustained eye contact from a stranger reads as challenge or authority.
- Physical contact minimal — a hand on the shoulder is significant. A handshake is formal.
- Complaining is fine. Complaining to the person you're complaining about is not.
- Mind your business is the unspoken rule. People don't ask intrusive questions; they don't volunteer information they haven't been asked for. This makes the detective's investigation harder and the smuggler's operation safer.
- Trust is established through repeated small interactions over time, not through declarations.
- **Technology attitudes:** Lattice use is unreflective — ubiquitous, taken for granted, the way breathing is taken for granted. Checking your lattice overlay in conversation is rude but common. The Unbound (those without lattices) are a known category — treated with mild curiosity and zero hostility. Nobody makes a fuss about it. Augmented lattice (institutional or premium) carries subtle status markers — people notice, don't comment.
- **Commission attitude:** Pragmatic compliance. The Commission regulates neural lattice technology; that regulation affects everyone, constantly. Workers find it mildly onerous (recertification requirements, software update mandates, firmware flags on aftermarket accessories). The Commission isn't hated — it's just the reality of living in the Reach. Like weather.
---
### Dimension 5: Institutional Ecosystem
Every location has a set of institutions that structure daily life. Understanding which institutions are present, which are visible, and which are resented tells you how NPCs talk about authority, rules, and power.
**Core institutions across the Reach:**
| Institution | Role | Local Manifestation |
|---|---|---|
| **Concord Assembly** | Reach-wide legislature | Background. Distant. Appears in news tickers. Not visible on the ground. |
| **Lattice Commission** | Neural tech regulation | Kiosks, certification terminals, field investigators. The most visible institutional presence in most districts. |
| **Syndics** | Commercial entities, freight and resource management | The actual employers of most working-class NPCs. Contracts, shipping manifests, wage structures. |
| **Meridian** | Information network | Ambient. The infrastructure everyone uses without thinking about. Dead spots are notable. |
| **Station Administration** | Local governance | Immediate authority. Maintenance schedules, district regulations, housing allocation. |
| **Medical Services** | Re-embodiment and healthcare | Presence varies by wealth. Clinics in working districts, full services in affluent ones. |
**Sova Transit District institutional profile:**
- Syndic logistics consortium: the main employer, highly visible in daily operations
- Station Administration: present through maintenance schedules, posted notices, the occasional administrator in the corridors
- Lattice Commission: intermittent. A kiosk in the logistics hub lobby, field investigators who visit quarterly (usually)
- Medical clinic: small, basic. Re-embodiment services require transfer to the main station medical center
- Meridian: strong coverage in institutional spaces (hub, bar), degraded in older sections, minimal in maintenance corridors
---
## Part 2: Derived Guidelines — From Framework to Content
### 2.1 Naming Conventions
Naming conventions follow from Dimension 4 (Cultural Reference Point). The table below captures the Krenn System standard, which serves as the v0.1 model.
**Krenn System naming rules:**
- **First names:** 1-2 syllables, consonant-forward, no double vowels in typical form. Examples: Kael, Voss, Drin, Lera, Torek, Maret, Naia, Sera, Nils, Kosse, Pael, Tev, Ren, Resha, Harek, Sess, Devra.
- **Family names:** 1-2 syllables, similar phoneme pattern. Examples: Davan, Lintar, Sessik, Korr.
- **Social usage:** First name in most contexts. "Supervisor Voss" for institutional address. Full name only in formal or hostile contexts.
- **Nicknaming:** Common among colleagues. A name like "Renn" might be short for something nobody uses.
- **Off-system names:** Slightly softer vowels, longer syllable count, different consonant clusters. Example: Olin, Sabel. These stand out quietly — the detective's lattice may flag them as non-Krenn-origin before the detective consciously registers why.
**IP check:** Krenn names are constructed to feel plausibly human-future without mapping to any existing franchise. They are NOT Hamilton Commonwealth names (conspicuously contemporary English, which is Hamilton's signature device). They are NOT Banks Culture names (often whimsical or elaborate). They are NOT Firefly or Star Wars (no Earth-ethnic-name echoes). They suggest cultural drift across 180 years of mid-Reach settlement.
---
### 2.2 Economic Vocabulary
Economic base (Dimension 2) determines the vocabulary of daily life. Workers in a freight hub speak differently from workers in a mining operation or a research station. This vocabulary should permeate every NPC's dialogue — not as jargon dumps, but as the ambient language of their daily reality.
**Sova Transit District — Freight Economic Vocabulary:**
See companion document: **Sova Texture Appendix (#302)** for the full slang glossary.
Core vocabulary patterns for freight:
- Work = "shift" (not "rotation," not "duty," not "tour")
- The gate = "the gate" casually, "the span gate terminal" institutionally — never "the span gate" in casual speech
- Cargo containers = "containers" or "cans" (informal) — never "pods"
- Cargo manifests = "manifests" (precise), "paperwork" (dismissive), "the file" (when being evasive)
- Credits = "marks" in casual conversation (a Krenn System local usage)
- Off-shift time = "off-book" among the ring; just "after" for civilians ("see you after")
- Lattice communication = "pinging" (sending), "a ping" (notification)
---
### 2.3 Social Rhythms and Norms
The freight rhythm (span gate schedules, shift rotations) structures social life. Content authors should use these rhythms as temporal anchors for dialogue and monologue.
**Sova Transit social rhythm:**
| Time | Activity | Social Atmosphere |
|---|---|---|
| 0600-0800 | Morning shift change | High traffic, business-focused, brief greetings |
| 0800-1200 | Morning freight cycle | Focused work, minimal social interaction |
| 1200-1400 | Midday break | Break room social time, bar has light lunch trade |
| 1400-1600 | Afternoon freight cycle | Quieter, post-lunch pace |
| 1600-1800 | Evening shift change | Second major transition, people heading to or from |
| 1800-2200 | Peak bar hours | The Last Shift fills up. Main social time of the day. |
| 2200-0200 | Late night | Bar quiet (late regulars only). Corridors quieter. Ring operations typical timing. |
| 0200-0600 | Deep night | Minimal staffing. Maintenance windows. Skeleton shift. |
---
### 2.4 Relationship to Authority
Dimension 3 (Reach Position) determines how NPCs speak about, around, and to authority figures. This affects dialogue at every trust tier.
**Sova Transit — Authority Relationship Patterns:**
| NPC Type | To Commission directly | About Commission (behind back) | To institutional-tagged stranger |
|---|---|---|---|
| Flat NPC (civilian) | Cooperative, neutral. Answers questions. Doesn't volunteer. | "They do their job. Just don't want them doing *my* job." | Polite. Slightly more careful with word choice. Watches for cues. |
| Triangle NPC (mundane) | Cooperative. Slightly more nervous if personally concerned. | "They'll always find something to flag if they look hard enough." | Normal, with an undercurrent of evaluation: are they relevant to me? |
| Entangled NPC (ring) | Carefully cooperative. Prepared answers. Nothing extra. | "Commission pokes around twice a year and finds nothing because they don't know where to look." | Alert. Measures words. Changes topics smoothly. |
---
### 2.5 Technology Integration
Lattice use is unreflective for most NPCs — it's background, like breathing. Content should treat lattice references the way people treat phone references: incidentally, in passing, without ceremony.
**Correct treatment:**
> "I pinged you twice. Did you sleep through your lattice?"
**Wrong treatment (over-technological):**
> "I sent a message through the neural lattice communication network to your personal lattice implant."
**Wrong treatment (under-technological):**
> "I tried to call you." *(This isn't the Reach — people don't "call." They "ping.")*
The exception: NPCs under stress or in unusual circumstances may reference their lattice more explicitly — "I checked three times, it's not in the manifest" — where the precision itself signals anxiety.
---
## Part 3: Scaling Beyond v0.1
The Krenn System instance demonstrates the framework. Future settings apply the same five dimensions with different parameters. The cultural output — naming conventions, vocabulary, social norms, authority attitudes — follows from the parameters, not from free-form invention.
**Quick-generation checklist for a new district:**
1. **Reach Position:** Core / Mid-Reach / Frontier / Deep Frontier
→ Determines institutional density and authority attitude
2. **Economic Base:** Freight / Extraction / Manufacturing / Service / Research / Mixed
→ Determines daily rhythm and vocabulary domain
3. **Settlement Age:** New / Established / Mature / Old
→ Determines architectural character and cultural rootedness
4. **Cultural Reference Point:** Which system of origin? How many generations of local drift?
→ Determines naming conventions and social norms
5. **Institutional Ecosystem:** Which institutions are present, how visibly?
→ Determines how NPCs relate to authority
From these five inputs, the following outputs are derivable for any new setting:
- Naming conventions (with example names)
- Economic vocabulary (10-15 terms)
- Social rhythm (daily time anchors)
- Authority relationship patterns (flat/triangle/entangled × authority figure)
- Technology integration register (how casually lattice is referenced)
**When a new setting is created:** Write a cultural profile following this guide and add it to `docs/wiki/cultural-groups/`. Tag it with the relevant star system. Link from the district entry in the wiki.
---
## Part 4: The Dual Lens Principle — Culture Serves Asymmetry
Every cultural detail in The Settled Reach exists to serve asymmetric information. Culture is not decoration — it's the medium through which two characters experience the same world differently.
**What this means in practice:**
The same cultural detail should mean something different to the smuggler and the detective:
| Cultural Detail | Smuggler Reads It As | Detective Reads It As |
|---|---|---|
| "Mind your business" norm | Operational cover. Nobody asks, nobody knows. | Investigative friction. People won't volunteer. |
| First-name social culture | Social camouflage. Insider by default. | Access challenge. Have to earn first names. |
| Lattice pings as communication | Ring coordination. Off-Meridian pings are clean. | Evidence trail. Pings log to Meridian (mostly). |
| Bar as social heart | Safe social space. Home territory. | Intelligence gathering venue. |
| Commission presence intermittent | The gap in oversight is the operational window. | The jurisdictional gap is also my investigative constraint. |
| Span gate schedule as daily rhythm | The operational calendar. Timing is everything. | Investigation timeline. Window when oversight is thinnest. |
**Authoring instruction:** When writing any cultural detail — a sign, a greeting, a social norm — ask: what does the smuggler see, and what does the detective see? If the answer is identical, the detail is decorative. If the answers differ, the detail is load-bearing.
---
## Appendix: v0.1 Named Characters — Krenn System Consistency Check
All named characters in the Sova Transit District, with naming convention validation:
| Name | Convention Check | Role | Notes |
|---|---|---|---|
| Kael Davan | Krenn standard | Dock worker, ring member, smuggler's FRIEND | Full name used in formal contexts |
| Sera Venn | Krenn standard | Commission field tech, detective's FRIEND | "Venn" slightly softer than typical Krenn surname — deliberate: she's institutional, slightly apart |
| Lera Sessik | Krenn standard | Bar owner, The Last Shift | "Lera's" informal name. Never Lera Sessik to regulars. |
| Torek Lintar | Krenn standard | Dock inspector, compromised | Full name used when the detective runs his file |
| Voss | Krenn standard | Logistics supervisor | Single name in social use. Probably Voss-something officially. |
| Drin | Krenn standard | Maintenance tech, flat NPC | Single name. Been here long enough nobody uses a surname. |
| Maret Korr | Krenn standard | Freight scheduler, ring-adjacent | "Korr" is direct Krenn standard |
| Naia | Krenn standard | Kael's partner (mentioned, not primary) | Brief, soft — contrast to Kael |
| Nils | Krenn standard | Bar regular (mentioned) | |
| Kosse | Krenn standard | Station name / character reference | |
| Pael | Krenn standard | Maintenance tech, flat NPC (Sprint 12: #307) | |
| Tev | Krenn standard | Lookout/runner, flat NPC (Sprint 12: #307) | |
| Ren | Krenn standard | Drifter, flat NPC (Sprint 12: #307) | "Ren" is earlier-established; "Renn" is the courier variant from Round 2 mapping |
**Off-system characters (deliberate naming deviation):**
| Name | Deviation | Role | Purpose of Deviation |
|---|---|---|---|
| Resha | Softer vowel pattern | New hire | Implies off-system origin — explains why they ask too many questions |
| Sabel | Different phoneme cluster | Ring fence/buyer, external contact | External contact should sound external |
**IP check:** All Krenn names validated against Hamilton (Contemporary English — cleared), Banks (whimsical/elaborate — cleared), Reynolds (Dutch/Germanic — cleared), Simmons (Greek/Latin register — cleared), Morgan (Anglo-American register — cleared). No franchise overlap found.
---
*This guide is a living document. As new settings are established, update the scaling examples. As cultural decisions are refined through gameplay testing, update the social norm descriptions. The framework is the constant; the instances are the evidence.*
*Miri — Sprint 12*
+162
View File
@@ -0,0 +1,162 @@
# Detective: Institutional Chain of Command
**Ticket:** #322 | **Priority:** High | **Sprint:** 12
**Status:** v0.1 Draft
**Owners:** Paula (narrative arc), Miri (setting/institutional context)
**Decisions referenced:** D-005, D-027, D-037, D-041
---
## 1. Purpose
The detective is not a freelance investigator. They are a Commission officer, which means someone assigned them here, someone defined their mandate, someone is waiting for reports, and someone may have an interest in how this investigation concludes — not all of them aligned.
The institutional chain of command is not just background. It determines:
- **What the detective is officially permitted to do** (arrest authority, warrant scope, search powers)
- **What they're NOT supposed to do** (exceed mandate, go outside channels, use extra-institutional leverage)
- **Who benefits if the investigation succeeds** — and who doesn't
- **The political weight of the previous liaison's rotation** — which may not have been routine
This document establishes the detective's institutional world. It does not need to be fully visible to the player in v0.1 — it lives in the detective's case file, in their monologue, in what institutional NPCs say when authority-tier access opens. But it must be consistent.
---
## 2. The Commission — Brief Institutional Context
The Commission (Intersolar Commission) is the primary regulatory body governing freight transit, customs enforcement, and neural lattice certification across the Settled Reach. It is not a police force. It operates through:
- **Compliance review** — routine audits of freight hubs, manifest reconciliation
- **Liaison officers** — semi-permanent postings to transit hubs; advisory and investigative authority
- **Field technicians** — technical support, equipment maintenance, passive monitoring (Sera Venn's role)
- **Enforcement officers** — arrest authority; called in from sector headquarters for formal investigations
The detective's posting as "Commission Liaison Officer" sits between compliance and enforcement. They have investigative authority — enough to request manifests, interview persons of interest, and flag suspects — but formal arrest and search warrant authority requires escalation to sector headquarters.
**This matters for the detective's v0.1 situation:** They can investigate. They cannot arrest without authorization from above. If they find evidence of a smuggling ring, they need to report it up the chain before taking action — and that creates a window of political exposure.
---
## 3. The Formal Chain
### Level 1: The Detective (On-Station)
**Title:** Commission Liaison Officer, Sova Transit District
**Mandate (official):** Routine compliance review — manifest reconciliation, scheduling compliance, standard freight hub audit.
**Mandate (actual):** Investigate manifest irregularities suggesting organized smuggling. Identify structure, membership, and supply chain. Report to Sector Oversight with arrest recommendations.
**Reporting to:** Sector Liaison Coordinator, Commission Sector 7 Headquarters (off-station, a span-gate transit away)
### Level 2: Sector Liaison Coordinator — Renna Veth
**Title:** Liaison Coordinator, Commission Sector 7
**Who she is:** Career Commission officer. Mid-level administrator with 20 years of routine compliance work and two serious investigations to her name. Professional, risk-averse. Not corrupt — but also not courageous. Wants the investigation to produce results without embarrassing the Commission or producing a political fight she didn't expect.
**Her interest in this investigation:** Wants it resolved cleanly. A successful arrest closes a compliance gap in her sector metrics. A failed investigation — especially one that implicates a Commission insider — would be a problem.
**Relationship with detective:** Assigned the detective to Sova because the detective has a clean record and a reputation for methodical work. Does not know the detective personally.
### Level 3: Sector Deputy Commissioner — Halden Pres
**Title:** Deputy Commissioner, Sector 7
**Who he is:** Political appointee. Has been in position for 6 years. Sector 7 covers the Krenn System transit hubs, which are economically significant — freight logistics for Velen's surface operations. Not personally corrupt. But his political relationships include figures who have interests in how freight regulation is managed in the sector.
**His knowledge of this investigation:** Was briefed at level of "routine compliance review at Sova." Does not know the actual investigative mandate. If the investigation escalates — if arrests are made, if Commission involvement in a smuggling ring is implied — he will be briefed, and his political relationships will activate.
**The political risk:** Deputy Commissioner Pres is not an enemy. But if the investigation implies that a previous Commission liaison was arranged to protect a smuggling ring, Pres may have indirect connections to whoever arranged it. He is not the villain. But he is a constraint.
---
## 4. The Previous Liaison: Korrean Tsev
**Title (former):** Commission Liaison Officer, Sova Transit District
**Rotation:** Posted to Sova for 18 months; rotated out 3 months before the detective arrived.
**Official reason for rotation:** Standard posting cycle. Liaison officers rotate every 18-24 months to prevent over-familiarity with local operations.
**The detective's case file note:** The previous liaison, Korrean Tsev, was in position during the period when manifest discrepancies were already detectable in the freight data. No irregularity reports were filed. Three compliance audits during Tsev's tenure all came back clean. The anomalies that the detective's pattern analysis flagged from the data should have been visible to Tsev.
**Was the rotation arranged?**
The detective's case file flags this as unconfirmed but suspicious. The canonical answer (not available to the player in v0.1): **yes, it was arranged.** Tsev was not corrupt in the sense of taking ring money — but Tsev was compromised by personal loyalty. A relationship with someone at sector administration meant Tsev was quietly moved when the manifest anomalies became too persistent to ignore, rather than being required to file a report that would implicate people above Tsev's pay grade.
**What the detective can learn in v0.1:**
- That Tsev filed no reports (case file, accessible from session start)
- That the anomalies were detectable during Tsev's tenure (manifest data analysis, takes investigation time)
- That Tsev was rotated on a compressed timeline (standard was 24 months; actual was 18 months) — this is discoverable through institutional records if the detective requests them
**What the detective cannot learn in v0.1:**
- WHY the rotation was arranged
- Who in sector administration was involved
- Whether Deputy Commissioner Pres had knowledge
This is a thread that extends beyond the v0.1 vertical slice. The detective can pull the thread enough to see it exists, but not enough to unravel what's at the other end.
---
## 5. Torek Lintar: Commission Compliance Officer
Torek Lintar is Commission compliance on Sova — not an investigator, not a liaison officer, but a permanent-fixture compliance monitoring role. Commission compliance officers are lower in the formal hierarchy than liaison officers, but they have one thing liaison officers don't: **local knowledge accumulated over years.**
Torek has been on Sova for 4 years. He knows everyone. He knows the bar. He knows the shifts. He knows the social fabric in a way a three-day-old liaison officer cannot.
**The detective's starting relationship with Torek:** Professional known name from institutional channels. Not a friend. Encountered at the bar (Torek is a bar regular).
**What Torek knows that the detective doesn't:**
- The ring's social footprint, at least at the edges — conspicuous spending, familiar faces
- Sera Venn's unofficial compliance check (Torek suspects; he doesn't know for certain)
- The shape of the social web connecting Kael's group
**Why Torek hasn't reported what he knows:**
Torek's tolerance threshold for confrontation is low. He is ring-adjacent (D-024 triangle 3: Bar Tensions), which means he is socially embedded with people who are embedded with the ring. His conspicuous spending suggests he may have accepted something from ring-peripheral sources — not enough to be a member, but enough to compromise his ability to report cleanly. He is not a villain. He is a compromised observer who has talked himself into believing the right moment to report will come later.
**Torek as asset for the detective:**
If the detective builds enough trust (social + authority), Torek can become an information source — but carefully. The ring is aware of Torek's connection to Commission compliance. Torek going to the detective too visibly risks blowback on both of them.
**Torek as the signal in Sera's FRIEND arc:**
Sera avoids Torek. Three times. Three different excuses. The detective's monologue reads this as a behavioral pattern. The reason: Sera ran an unofficial check on Kael's work patterns (using Commission technical access she wasn't supposed to use) and found the same manifest discrepancies the detective found. Torek, as Commission compliance, would be the person Sera should have reported this to. Sera's avoidance of Torek is the tell that she found something and didn't report it. The detective doesn't know this yet — but the behavioral signal points there.
---
## 6. Political Pressures on the Investigation
The detective's investigation operates under several simultaneous pressures:
| Pressure | Source | Direction | Effect |
|----------|--------|-----------|--------|
| **Succeed cleanly** | Veth (Sector Coordinator) | Upward | Produce results without creating institutional embarrassment |
| **Don't exceed mandate** | Institutional | Inward | Formal arrests require escalation; the detective can't act unilaterally |
| **Previous liaison question** | Case file | Lateral | Pulling this thread risks finding Commission involvement; politically dangerous |
| **Sera's position** | Personal | Personal | Using what Sera knows against her would destroy the detective's only personal relationship in the district |
| **Community non-cooperation** | Social | Environmental | The district's default resistance to institutional authority; this is not malice, it's culture |
The detective is not just investigating a smuggling ring. They are investigating within an institutional structure that has already failed to catch this ring once. The question of whether that failure was incompetence or arrangement is the shadow behind every manifest discrepancy.
---
## 7. How This Appears in Monologue
The detective's institutional context should surface in monologue lines as internal processing — the analytical framing through which they filter every observation. Not institutional jargon, but institutional habit.
**Examples:**
At session start, observing the hub:
*"Third day. Tsev was here for eighteen months and filed nothing. Either I'm better at this than Tsev, or there's something in the water. Let's find out."*
When Voss deflects a question:
*"That answer was technically responsive. It gave me nothing. Voss knows exactly what I'm asking. Note it."*
When considering what Torek knows:
*"Lintar's been on Sova for four years. He's seen whatever there is to see. Commission compliance doesn't file reports in a vacuum — something told him to stay quiet."*
When the Sera contradiction emerges:
*"I could escalate this. Report what I suspect to Veth. But if I'm wrong about Sera, I've just burned my only ally on a hunch. And if I'm right — I need to know how far this goes before I put it in writing."*
---
## 8. v0.1 Content Scope
What needs to be authored for this to work:
| Element | Content Requirement |
|---------|---------------------|
| Detective opening monologue (lines about Tsev's missing reports) | 1-2 lines from session start knowledge |
| Case file items (accessible via evidence display) | 3-5 readable entries: Tsev rotation note, manifest cluster data, POI summaries |
| Torek dialogue (authority tier) | 3-5 lines for institutional context questions |
| Veth (off-station, not interactive) | Referenced in case file and detective monologue only |
| Detective monologue about political constraints | 2-3 lines scattered across session |
The chain of command does not require NPCs to appear — Veth and Pres are off-station and non-interactive in v0.1. Their presence is felt through the detective's case file entries and internal monologue about institutional constraints. Torek is the only chain-of-command-adjacent NPC who is interactive.
---
*Ticket #322 — Detective institutional chain of command. v0.1 scope.*
+231
View File
@@ -0,0 +1,231 @@
# Divergent Relationships
**Ticket:** #183 | **Priority:** Critical | **Sprint:** 12
**Status:** v0.1 Draft
**Blocked by:** #180 (Smuggler build), #181 (Detective build)
**Decisions referenced:** D-005, D-027, D-028 (four relational layers), D-029 (30/50/20), D-033 (entity color), D-075 (knowledge confidence gate on trust)
---
## 1. Design Principle
Same NPCs. Two different starting relationship webs. The smuggler is inside the social fabric of Sova Transit District. The detective is outside it, with institutional authority as their primary access tool.
This divergence is not symmetric. The smuggler has deep relationships with fewer NPCs but holds insider access from session start. The detective has shallow relationships with many NPCs but can use authority credentials to access people the smuggler cannot pressure. Neither coverage model is superior — they produce different investigations.
**The honest truth is:** the smuggler's social depth is their strength AND their vulnerability. When the ring starts to crack, their close relationships become sources of betrayal, not safety. The detective's institutional distance is protection AND limitation — they never get the warmth, but they also never get the blindspot.
---
## 2. NPC Relationship Comparison
### Key: RelationshipState | Access Tier | Trust | Entity Color (D-033)
| NPC | Smuggler | Detective |
|-----|---------|-----------|
| **Kael Davan** | Friendly / insider+peer / trusted / **green** | Unknown / public / N/A / **grey** |
| **Sera Venn** | Known / public / uncertain / **teal** | Friendly / peer / trusted / **green** |
| **Voss** | Known / insider+peer / reliable / **teal** | Known / authority / uncertain / **teal** |
| **Maret Korr** | Known / insider+peer / reliable / **teal** | Known / authority / uncertain / **teal** |
| **Drin Rosta** | Known / insider / reliable / **teal** | Known / authority / uncertain / **teal** |
| **Lera Sessik** | Friendly / insider+peer / trusted / **green** | Unknown/Known / public / uncertain / **grey→teal** |
| **Naia Tamm** | Known / peer / reliable / **teal** | Unknown / public / N/A / **grey** |
| **Torek Lintar** | Known / peer / uncertain / **amber** | Known / public / uncertain / **teal→amber** |
| **Nils Davan** | Known / insider / reliable / **teal** | Unknown / N/A / N/A / **grey** |
| **Renn** | Known / insider / reliable / **teal** | Unknown / N/A / N/A / **grey** |
| **Devra** | Known / insider / reliable / **teal** | Unknown / N/A / N/A / **grey** |
| **PC-Detective** | Unknown/Known / public / uncertain / **amber** (warned) | N/A — is the detective | N/A |
| **PC-Smuggler** | N/A — is the smuggler | Unknown / public / N/A / **grey** |
**Color key (D-033):**
- **Green** = Friendly relationship — warm, accessible, insider content possible
- **Teal** = Known/neutral — recognized, baseline access
- **Amber** = Person of Interest — heightened observation, potential threat
- **Grey** = Unknown — no existing relationship
---
## 3. The Inversion Architecture
The same NPCs appear in both relationship maps. But the maps are mirrors, not duplicates.
### Kael Davan (THE FRIEND): Maximum Divergence
| | Smuggler | Detective |
|-|---------|-----------|
| RelationshipState | Friendly | Unknown |
| Access Tier | insider + peer | public |
| Trust | 8/10 — "the day is better when he's on shift" | N/A — one of many dock workers |
| Entity color | Green | Grey |
| Dialogue available | Phase-gated ring talk, personal warmth, confession (if trust maxed) | Greeting, basic questions, deflection |
| Emotional weight | Maximum — 2 years of shared risk | Zero — no relationship to lose |
| FRIEND arc | Betrayal, protection, contaminated trust | Behavioral tells, manifest connection, informant potential |
This is the central architectural divergence. The smuggler's most important NPC is the detective's least known. When the smuggler feels Kael's betrayal, the detective is trying to figure out who Kael is.
### Sera Venn (THE FRIEND): Mirror of Kael
| | Smuggler | Detective |
|-|---------|-----------|
| RelationshipState | Known | Friendly |
| Access Tier | public | peer |
| Trust | 0/10 — "Commission, keep distance" | 9/10 — "she always was the reliable one" |
| Entity color | Teal | Green |
| Dialogue available | Basic public-tier; no personal content | District orientation, gossip, institutional sharing |
| Emotional weight | Near zero — categorized threat | Maximum — only ally in unfamiliar territory |
| FRIEND arc | N/A | Avoidance pattern, suppressed evidence, contaminated trust |
Same NPC. The smuggler maintains institutional distance. The detective relies on personal friendship. The contradiction (Sera sitting on evidence that could advance the investigation) hits differently: for the detective, it's a betrayal of trust. For the smuggler, if they were ever to learn it, it would be irrelevant — Sera was never trusted in the first place.
### Voss: Inverted Access, Same NPC
| | Smuggler | Detective |
|-|---------|-----------|
| RelationshipState | Known | Known |
| Access Tier | insider + peer | authority |
| Trust | "reliable but not warm" — operational respect | "uncertain — answers but doesn't volunteer" |
| Dialogue available | Ring operational coordination, schedule management | Institutional questions, manifest data, evasive answers |
| What Voss says to them | Operational specifics: "Transition window is clean." | Institutional surface: "Everything is in compliance." |
Both characters talk to Voss. The conversations are nothing alike. The smuggler gets operational truth. The detective gets institutional deflection.
---
## 4. Access Tier Divergence Table
Per-NPC table of which dialogue tiers each character can access at session start vs. through play:
| NPC | Smuggler Starting | Smuggler Max | Detective Starting | Detective Max |
|-----|-----------------|-------------|-------------------|--------------|
| Kael | insider + peer | insider + peer | public | authority (if POI) |
| Sera | public | public (no growth path) | peer | authority (post-contradiction) |
| Voss | insider + peer | hostile (if cover blown) | authority | peer (if trust earned) |
| Maret | insider + peer | hostile (if pressured wrong) | authority | peer (if cooperation) |
| Drin | insider | hostile (if flipped) | authority | — see note |
| Lera | insider + peer | hostile (social contamination) | public | peer (4+ visits) |
| Naia | peer | hostile (if truth revealed) | public | peer (through Sera) |
| Torek | peer | hostile (if confronted about spending) | public | authority (financial tells) |
**Note on Drin:** The detective can pressure Drin via gambling debt (institutional leverage). This is the detective's entry point to actual ring structure — Drin as turned informant. The smuggler knows Drin is compromised; they have no leverage angle because they're on the same side.
---
## 5. Social Site Coverage
How each character covers the three social sites of Sova Transit District:
### The Terminal (Logistics Hub)
**Smuggler:**
- Full insider access to ring-aware NPCs (Kael, Voss, Maret, Drin, Renn)
- Cargo floor access (shifts, freight systems, break room)
- Social belonging — people wave, greet, assume presence is normal
- Blind spots: doesn't see the detective's investigation angle; doesn't know Maret is worried about the manifest requests
**Detective:**
- Authority access to institutional NPCs (Voss, Maret, Drin)
- Data access (manifests, scheduling records, inspection logs)
- Visible outsider — NPCs stiffen, answers get shorter, body language shifts
- Blind spots: ring-operational dialogue is completely inaccessible; NPCs perform compliance without candor
**Coverage overlap:** Both characters are in the Terminal. Same physical space. The overlap zone is the ~20% of dialogue that's truly `public` — shift schedule announcements, generic greetings, environmental observations. Everything else is character-specific.
### The Last Shift (Bar)
**Smuggler:**
- Insider status — Lera saves their seat, ring-aware regulars speak freely
- Social hub for ring: Kael, Renn, Torek (if present), Lera all accessible
- Emotional territory: this is where the smuggler goes to decompress, not investigate
- Blind spots: doesn't see Sera's avoidance of Torek as anything other than "Commission, normal behavior"
**Detective:**
- Outsider becoming regular — needs 3-4 visits to unlock peer access with Lera
- Social anchor through Sera — Sera's presence grants partial access to bar social fabric
- Observation territory: this is where the detective watches, not relaxes
- Blind spots: ring-adjacent social dynamics (Torek's rounds, Lera's protectiveness) read as social texture, not evidence
**Coverage overlap:** Sera is the bridge. Both characters interact with Sera at the bar. Sera's dialogue with the smuggler (brief, guarded, Commission-wary) and with the detective (warm, orienting, personal) are completely different. An NPC the detective trusts most is the one the smuggler avoids most.
### The Maintenance Corridors
**Smuggler:**
- Full access (corridor token)
- Routes, surveillance gaps, timing windows all known
- The corridors are operationally familiar — movement here is intentional and professional
- THE FRIEND contradiction happens here: Corridor B-7, Kael's unauthorized meeting
**Detective:**
- No natural access — maintenance corridors are not institutional territory
- Presence here is suspicious (both to NPCs and mechanically)
- May access if security authority is invoked, but it triggers NPC awareness
- Can observe corridor access from entry points; cannot follow undetected
**Coverage gap:** The corridors are entirely the smuggler's territory. The detective's only route in involves institutional authority that announces itself. This makes the smuggling operation's most vulnerable moments physically inaccessible to the detective's normal investigative approach.
---
## 6. NPC Knowledge of Each Character
NPCs hold knowledge about both characters simultaneously (D-041 bidirectional). The same NPC has:
- EntityKnowledge about the smuggler (from the smuggler's perspective)
- EntityKnowledge about the detective (from the detective's perspective)
Key examples:
**Kael's knowledge of both characters:**
- Of the smuggler: Friendly / trusted / ring member / close colleague
- Of the detective: Known / suspicious / Commission / risk: moderate
**Lera's knowledge of both characters:**
- Of the smuggler: Friendly / trusted / regular customer / ring-aware
- Of the detective: Unknown-becoming-Known / uncertain / Commission presence
**Sera's knowledge of both characters:**
- Of the smuggler: Known / uncertain / "dock worker, keep minimal contact"
- Of the detective: Friendly / trusted / friend, institutional colleague
**Voss's knowledge of both characters:**
- Of the smuggler: Known / reliable / ring member
- Of the detective: Known / suspicious / risk: moderate / managing exposure
---
## 7. Relationship Shift Paths (First 30 Minutes)
How relationships shift during the vertical slice for each character:
### Smuggler Relationship Shifts
| NPC | Starting State | Likely Shift | Trigger |
|-----|---------------|-------------|---------|
| Kael | Friendly | → PersonOfInterest | Corridor B-7 observation (Phase 3) |
| All ring NPCs | Known/Friendly | → Hostile (conditional) | If smuggler cooperates with detective or cover blown |
| Naia | Known | → (no shift in v0.1) | No player action triggers this in 30 min |
| Sera | Known | → (no shift in v0.1) | Smuggler never gets close enough |
### Detective Relationship Shifts
| NPC | Starting State | Likely Shift | Trigger |
|-----|---------------|-------------|---------|
| Sera | Friendly | → PersonOfInterest | Torek avoidance pattern (Phase 3) |
| Kael | Unknown | → Known → PersonOfInterest | Behavioral tells + manifest discrepancy connection |
| Drin | Known | → Cooperative (conditional) | Gambling debt leverage applied |
| Lera | Unknown | → Known → peer (conditional) | 3-4 bar visits |
| Maret | Known | → peer (conditional) | Cooperative interview unlocks |
| Smuggler-NPC | Unknown | → Known → PersonOfInterest | Tell accumulation + cargo attention on 4471 |
---
## 8. Design Implications
**The most emotionally resonant NPCs are the ones each character DOESN'T know.**
The smuggler will encounter the detective investigating the people they love most. The detective will encounter the smuggler operating inside the community they're trying to understand. The tension comes from the player knowing what the NPC they're watching doesn't know.
**Relationship symmetry check:** For every NPC in one character's web, verify the other character's starting state. If the smuggler has a `Friendly` / insider relationship, the detective must start at `Unknown` or `Known` / public for there to be discovery room. If both characters start at `Friendly`, the NPC is a shared ally and loses narrative potential.
**The bar is the convergence zone.** Both characters are present at The Last Shift. The physical overlap creates the mirror moment opportunities (ticket #329). Same room, same NPCs, different access. The bar scenes are the most structurally dense content space in v0.1.
---
*Ticket #183 — Divergent relationships. v0.1 scope.*
+171
View File
@@ -0,0 +1,171 @@
# Divergent Starting Knowledge
**Ticket:** #182 | **Priority:** Critical | **Sprint:** 12
**Status:** v0.1 Draft
**Blocked by:** #180 (Smuggler build), #181 (Detective build)
**Decisions referenced:** D-005 (asymmetric information), D-027 (vertical slice), D-041 (knowledge graph), D-065 (evidence display)
---
## 1. Design Principle
Asymmetric information is the master mechanic (D-005). The two characters know different things about the same world. Neither has the full picture. The game's central tension comes from each player discovering what the other already knew — but only on their second playthrough.
The knowledge sets are not merely different. They are **inverted at the points that matter most.** The smuggler knows the conspiracy from inside it. The detective knows the investigation from outside it. The middle — the social web that connects them — is what neither fully understands at session start.
---
## 2. Master Knowledge Comparison Table
### 2a. Contraband and Ring
| Fact | Smuggler | Detective |
|------|---------|-----------|
| Ring exists (organized smuggling operation) | **KnowsEverything** — member | Unknown — suspects "systematic manipulation" but no structural knowledge |
| Ring members (Nils, Kael, Voss, Renn, Devra, Resha) | **KnowsDetails** — works with them daily | Unknown — names absent from case file; only Voss, Maret, Drin as institutional POIs |
| Cargo type (unlicensed lattice components, medical replacements) | **KnowsDetails** — handles the containers | Unknown — case file has weight/size anomalies but not cargo type |
| Smuggling routes (corridor access, timing windows) | **KnowsEverything** — runs the routes | Unknown — knows maintenance corridors exist from district layout documents |
| Operation schedule (shift transition windows) | **KnowsEverything** — the 20-minute gaps are the job | **KnowsDetails** — case file shows anomaly clustering around shift transitions |
| Supply chain upstream (Sabel, Syndic connection) | **KnowsOf** (partial: knows Sabel, not the full chain) | Unknown |
| Voss's complicity (takes a cut, provides scheduling cover) | **KnowsEverything** — operational fact | Unknown — Voss is a POI because of scheduling authority, not proven complicity |
| Drin's complicity (looks the other way on inspections) | **KnowsDetails** — operational asset | Unknown — Drin is a POI because of inspection failures |
| Maret's awareness (scheduling access to cover for ring) | **KnowsDetails** — hub colleague | **KnowsOf** — person of interest from case file |
### 2b. Investigation
| Fact | Smuggler | Detective |
|------|---------|-----------|
| Detective's identity as investigator | Unknown — may know "new Commission presence" from ring warning | **KnowsEverything** — is the investigator |
| Detective's actual mandate (investigate freight irregularities) | Unknown — "routine compliance review" is the cover | **KnowsEverything** |
| Manifest discrepancies exist and are flagged | **KnowsOf** — operational awareness that paperwork is being managed | **KnowsDetails** — has the discrepancy data, cluster analysis, container series |
| Previous Commission liaison rotation (possibly arranged) | Unknown | **KnowsOf** — noted in case file as suspicious; unconfirmed |
| Commission's investigation timeline | Unknown | **KnowsDetails** — their own timeline |
### 2c. Key People
| Fact | Smuggler | Detective |
|------|---------|-----------|
| Kael's exit attempt (unauthorized external contact) | **Unknown** (CRITICAL gap — basis for FRIEND arc) | Unknown |
| Sera's unreported evidence (suppressed compliance check on Kael) | Unknown | **Unknown** (CRITICAL gap — basis for FRIEND arc) |
| Naia confided in Sera about Kael | Unknown | Unknown |
| Naia's worry about Kael's behavior | **KnowsOf** — has noticed Naia seems stressed at bar; doesn't know why | Unknown — Naia is not yet a known individual |
| Torek's spending (conspicuous, potential security risk) | **KnowsOf** — has noticed; worries about exposure | **KnowsOf** — "slightly off in a way I can't yet articulate" |
| Sera's background (pre-existing contact for detective) | Unknown — knows Sera only as "Commission, keep distance" | **KnowsDetails** — trusted pre-existing friend |
| Smuggler's role in the ring | **KnowsEverything** — is a member | Unknown — one of many dock workers |
| Kael's relationship with Nils (brother; ring coordinator) | **KnowsDetails** | Unknown |
### 2d. Location and World
| Fact | Smuggler | Detective |
|------|---------|-----------|
| Sova Transit District geography (full) | **KnowsEverything** — 3 years here | **KnowsOf** — 3 days; still orienting |
| Corridor B-7 (restricted, ring access) | **KnowsEverything** — knows the access token, uses the corridor | Unknown — maintenance corridors noted in district layout |
| Dead drop location (temp storage bay 4, container 4471) | **KnowsEverything** | Unknown — container 4471 appears in manifest discrepancy data but not mapped to a location |
| Shift schedule rhythms (social and operational) | **KnowsEverything** | **KnowsOf** — knows shifts exist, pattern from case file |
| Bar social structure (who sits where, who's a regular) | **KnowsDetails** — 2 years of evenings at Lera's | **KnowsOf** — has visited twice |
---
## 3. Knowledge Asymmetry Map
The gaps in each character's knowledge are not random — they cluster around the same events and people, approached from opposite directions:
```
THE SHARED WORLD
┌─────────────────────────────────────┐
│ Triangle 4 │
│ Naia ←→ Kael ←→ Sera │
│ ↑ ↑ │
│ (Kael's (suppressed │
│ partner, evidence) │
│ knows ↕ │
│ something) Detective's │
│ ↕ FRIEND │
│ Smuggler's │
│ FRIEND │
└─────────────────────────────────────┘
Smuggler knows: Kael (trusted friend), ring details, routes, members
Smuggler doesn't know: Kael is exiting, Sera's evidence, Naia→Sera connection
Detective knows: Investigation data, Sera (trusted friend), manifest anomalies
Detective doesn't know: Ring structure, Kael's role, Sera's evidence, Naia→Sera connection
Neither knows: Triangle 4's full shape — the chain from Naia's worry to Sera's suppression to the investigation closing in
```
The dramatic irony of the second playthrough is that the player who's played as the smuggler watches the detective investigate from outside what the smuggler knows intimately. The player who's played as the detective watches the smuggler trust Kael — not knowing the thing the detective now knows (Kael is the link between the ring and Sera's evidence).
---
## 4. Knowledge Acquisition Curves
### Smuggler: What they can learn, and how
| Unknown → Known | Method | Timing |
|----------------|--------|--------|
| Detective's identity | Ring warning (Kael/Voss mentions Commission presence) OR direct observation | Minutes 0-5 (if warned); later if not |
| Kael's exit attempt | THE FRIEND arc — spatial observation (Corridor B-7), then confrontation | Minutes 15-25 |
| Sera has unreported evidence | Cannot discover this through normal play. Would require Sera to confess or Naia to reveal — extreme late-game territory beyond v0.1 scope | Post-v0.1 |
| Torek's specific risk level | Observation of Torek's behavior over time | Minutes 10-20 |
### Detective: What they can learn, and how
| Unknown → Known | Method | Timing |
|----------------|--------|--------|
| Ring exists (structured) | Evidence accumulation — NPC behavioral tells, manifest patterns, corridor observation | Minutes 15-25 |
| Kael is ring member | Connecting manifest discrepancies to Kael's shift data; cargo attention tell | Minutes 20-30 |
| Sera's unreported evidence | THE FRIEND arc — behavioral pattern observation (Torek avoidance), then confrontation | Minutes 20-30 |
| Smuggler is in the ring | Tell accumulation (corridor check, social clustering with Kael/Renn), manifest connection | Minutes 15-30 |
| Kael's exit attempt | Would require Kael to confess or Naia to reveal — beyond normal investigation path | Post-confrontation branch |
---
## 5. Evidence Display Difference
Same underlying Knowledge Graph, different presentation layer (D-065):
**Detective — Case File Format:**
```
Entity: Kael Davan
Role: Dock worker, Sova Terminal
Confidence: KnowsOf
Source: observation
Attributes:
- routine_pattern: "morning shift 06:00-14:00"
- behavior_flags: [lattice_checking, shortened_interactions]
- risk_assessment: "moderate — behavioral anomalies suggest concealment"
- link: [manifest_discrepancy_cluster_A]
Insert suggests: "Check shift records against anomaly window"
```
**Smuggler — Personal Notebook Format:**
```
Kael — closes colleague, ring. Two years. Trust: solid.
Lately: distracted. Lunch cut short twice this week.
Something's off. Not the operation — personal.
Watch for: the look-left tell.
```
Same underlying entity. Different framing. The detective files evidence. The smuggler worries about people.
---
## 6. Design Implications for Content Authors
**Rule 1: Don't explain what the character already knows.**
The smuggler doesn't need exposition about how the ring works. They know. Monologue lines about the ring should be operational, not explanatory. The detective doesn't need exposition about institutional procedures. They know. Monologue lines about the investigation should be observational, not procedural.
**Rule 2: The unknown facts are load-bearing.**
When writing monologue lines, verify against the unknown_facts list. A smuggler monologue line that references Kael's exit attempt before Phase 3 is a continuity error. A detective monologue line that identifies Kael as a ring member before connecting manifest data to shift records is a continuity error.
**Rule 3: Knowledge gaps produce monologue, not silence.**
What a character doesn't know generates JUST AS MUCH monologue content as what they know. The smuggler's worry about "is Torek a risk?" is monologue material. The detective's uncertainty about "previous liaison rotation — arranged or routine?" is monologue material. The unknown is active, not empty.
**Rule 4: The same event produces inverted monologue.**
When the same NPC does the same thing in both playthroughs, the monologue responses should be detectably from different starting states. Kael checking his lattice → smuggler thinks "not like him to be jumpy" (personal familiarity) → detective thinks "lattice activity spiked, behavioral flag" (observational analysis).
---
*Ticket #182 — Divergent starting knowledge. v0.1 scope.*
+296
View File
@@ -0,0 +1,296 @@
# First 5 Minutes Experience Design
**Ticket:** #259
**Owner:** Gestalt (systems framework)
**Unblocks:** #260 (opening hook content per character)
**Source:** D-027 (vertical slice success criteria), D-032 (monologue pools), D-041 (background knowledge)
**Date:** 2026-02-19
---
## Design Constraints
### What this IS
A systemic opening sequence using only existing game systems. No cutscene. No special tutorial mode. No force-scripted path. The game runs at tick 1 — fog active, NPCs on schedule, world simulating — and the player experiences a designed version of that.
"Designed" means: the storyteller has selected a seed-specific opening state that makes the first 5 minutes feel motivated, not random. The player has clear first actions without being told to do them.
### What this is NOT
- A cutscene explaining the premise
- An objective marker pointing the player somewhere
- A tutorial UI teaching them how to move
- A scripted conversation they must complete
- A guaranteed identical sequence on every run
### D-027 success criteria — first 5 minutes contribution
The first 5 minutes must lay the foundation for all four success criteria:
1. **30 minutes of breathing room** — opening must feel like daily life, not like a conspiracy. The hook should be subtle enough that the player isn't in investigation mode yet.
2. **Different games** — smuggler and detective must feel like different people in different situations from tick 1.
3. **Named NPC** — the FRIEND relationship must be established in the first 5 minutes, before any contamination.
4. **Emerge from systems** — the first "something is wrong" must be observable, not spoken.
---
## Systems That Fire in the First 5 Minutes
### Tick 0: World state initialized
At game start, before the player's first input:
- Knowledge graph loaded with Background facts (per Appendix A, `docs/design/knowledge-flags-v01.md`)
- NPCs on their Phase 1 schedules (D-034 friend arc: baseline, no deviations)
- Fog fully opaque — the player's vision cone starts revealing the world from their spawn position
- Ambient audio playing (span gate hum, station background)
### First input: Movement
First LOS reveal is the first conscious experience. The world expands around the player's movement. This is simultaneously:
- Tutorial for the fog system (learned by doing, not told)
- Spatial introduction to the opening location
- First monologue opportunity: `enter_location` trigger fires on first tile in a new zone
**Design requirement:** The spawn location must have something interesting in LOS range without moving. The player should be able to see at least one NPC or meaningful location element from their starting position.
### First monologue: enter_location trigger
The insert chime plays (UI sounds bus, D-068). First internal monologue line fires from the `enter_location` trigger pool for this location.
**This line does three things:**
1. Establishes character voice (who are you?)
2. Establishes relationship to the space (do you belong here?)
3. Plants the first seed of the day's texture (operational awareness or investigative alertness)
The player doesn't need to understand the monologue system — they just hear a thought that sounds like them.
### First NPC: observe_npc trigger
Within the first minute of play, a significant NPC should enter the player's LOS. This is designed into the opening state: the FRIEND is present or arrives during this window.
The observe_npc trigger fires an appropriate opening-register monologue line. For the FRIEND specifically:
- Smuggler → sees Kael: warmth, ease, "the day is better when he's here"
- Detective → sees Sera (if bar opening) or a stranger (if Terminal opening): recognition vs. observation
**The FRIEND must be established in the first 5 minutes.** This is the anchor for success criterion #3.
### First operational beat
Within 2-3 minutes, the player encounters something to DO — not a quest, but a situation that pulls them toward action.
- **Smuggler:** Kael is at dock. Lunch window approaching. Shift board has today's assignments. Checking in = social interaction + operational context.
- **Detective:** Sera's usual booth. Or a manifest terminal with flagged entry. Or Voss giving the new arrival a territorial once-over.
The first action should be something the character would naturally do. Not "find the smuggling ring" — "say hello to Kael" or "get a read on the district."
---
## Character Opening Designs
### Smuggler Opening
**Core emotional register:** Operational confidence. This is your place. You know these people, this space, this rhythm. Something small is slightly off — but you won't notice it for a few minutes.
**Starting location:** The Terminal (Logistics Hub), dock floor or break room area.
**Background knowledge active at game start:**
- Full ring routing knowledge (`knows_details: knowledge.ring_routing_knowledge`)
- Kael is a ring member and close friend (`knows_details + friendly`)
- Today's operational context (whether a drop is scheduled or it's a routine day)
**First 5 minutes beat sequence:**
| Beat | Trigger | System | Content Purpose |
|------|---------|--------|-----------------|
| 1 | Enter Terminal | `enter_location` | Voice + tone established. "Home sweet home" register. |
| 2 | See Kael (if on shift) | `observe_npc` | FRIEND Phase 1 warmth. "The day's better when he's on shift." |
| 3 | See shift board / loading arms | `enter_location` / `observe_anomaly` | Operational awareness. World is working. |
| 4 | First Phase 2 signal (optional, seed-dependent) | `observe_anomaly` or time | Kael checks lattice. Brief distracted pause. Too subtle to read. |
| 5 | First interaction opportunity | WASD to Kael, dialogue opens | Natural: greet your friend. Establishes the relationship interactively. |
**Hook:** The day feels normal. That's the hook. You're inside something, comfortable. The player doesn't know yet that they're going to want to understand what Kael is hiding.
---
### Detective Opening
**Core emotional register:** Arriving into someone else's established rhythm. You are the outsider here. The district has its own momentum and you have to find your place in it.
**Starting location:** Variable by seed — see opening variant table below.
**Background knowledge active at game start:**
- Manifest discrepancy flagged (`suspects: investigation.manifest_discrepancy`)
- Commission jurisdiction understood (`knows_details: world.commission_sova_jurisdiction`)
- Corridor B-7 flagged as restricted zone (`knows_of: location.corridor_b7_restricted`)
- Sera Venn as pre-existing contact (if bar-first variant)
**First 5 minutes beat sequence:**
| Beat | Trigger | System | Content Purpose |
|------|---------|--------|-----------------|
| 1 | Arrive in district / enter first location | `enter_location` | Clinical register. "Sova Transit. Working-class pragmatic. Start reading the room." |
| 2 | See first NPC (Voss or dock worker) | `observe_npc` | Institutional observation. Territorial reception or neutral professionalism. |
| 3 | See Sera (if bar variant) or manifest terminal (if Terminal variant) | `observe_npc` or `observe_anomaly` | Either: warmth of familiar contact OR analytic flagging of first anomaly |
| 4 | First lattice query opportunity | `observe_anomaly` near manifest data | Detective's analytical lattice flags a discrepancy. Knows this behavior. |
| 5 | First adversarial NPC | Voss or institutional resistance | First social friction. The district doesn't want you here. |
**Hook:** The detective is watching. Every detail potentially matters. The detective never relaxes into the environment the way the smuggler does — but they notice things the smuggler misses. The player feels competent and slightly outside.
**First NPC is adversarial by default** (per sprint brief). Even in the warm-arrival Sera variant, the first dock-floor NPC (Voss) reads the detective as an institutional intrusion and responds territorially. The detective has authority; using it has social costs.
---
## Seed-Selected Opening Variants
The storyteller selects from 3-4 opening states per character at game start. This creates replay variation without scripted divergence — the systems remain the same, the initial conditions differ.
### Smuggler variants (3-4 opening states)
| Variant | Initial Condition | First Difference Felt |
|---------|------------------|----------------------|
| **A: Quiet morning** | Kael on shift, no deviations, routine operational day | Day feels completely normal. The crack doesn't appear until 10+ minutes. |
| **B: Kael is late** | Kael hasn't arrived at expected time — first behavioral anomaly is his absence, not his presence | Player starts without the FRIEND anchor. Absence itself is the first tell. |
| **C: Heat from Voss** | Shift supervisor is monitoring the dock more than usual — scheduling tension | Operational caution from the start. The ring feels more exposed. |
| **D: Container held** | An operational container is in temp storage longer than routing protocol specifies | First anomaly is logistical, not relational. Triggers operational investigation before social. |
**Storyteller selection logic:** The storyteller picks based on seed-configured narrative pressure. Quiet morning on low-pressure seeds; Kael late or container held on medium-pressure; all three tensions concurrent on high-pressure opens.
### Detective variants (3-4 opening states)
| Variant | Initial Condition | First Difference Felt |
|---------|------------------|----------------------|
| **A: Bar first (warm arrival)** | Sera has been contacted ahead of time — detective starts at The Last Shift | First contact is friendly. District access comes through Sera's social network. |
| **B: Terminal first (cold arrival)** | No pre-contact — detective enters through the dock floor | First NPC is territorial (Voss). Institutional authority is the only access mechanism. |
| **C: Evidence in hand** | Arrives with two manifest discrepancies already flagged from prior analysis | Opens in investigation mode. First observations are filtered through existing suspicion. |
| **D: Institutional escort** | Commission liaison accompanies detective briefly — creates visible institutional friction with locals | Authority is visible and creates social walls immediately. Bar regulars see the badge first. |
**Storyteller selection logic:** Bar-first on low-pressure seeds (softest introduction, most narrative space). Evidence-in-hand on high-pressure seeds (player is already in investigation mode, mystery accelerates).
---
## The "Not A Tutorial" Rule
The first 5 minutes teach the systems through play, not instruction.
| System | How It's Taught |
|--------|----------------|
| Fog / LOS | Player moves; fog reveals. No explanation needed. |
| Monologue | Chime plays + character speaks. Player hears their internal voice immediately. No UI callout. |
| NPC observation | Observe an NPC; monologue responds. Causal connection is felt, not explained. |
| Knowledge graph | Relationship color appears on FRIEND (green). Player learns to read colors before understanding what they mean. |
| Dialogue | First dialogue is low-stakes — greeting a colleague. Player learns the system exists before they need to use it strategically. |
| Insert / map | Player opens insert naturally (looking for objectives). Insert contains only what their character would have on it. |
**No tutorial pop-ups.** Monologue handles all context. If the player does something and a monologue line fires about it, that IS the tutorial.
---
## Opening Hook Content Brief (for #299/#300)
This section briefs Mellanie and Paula on what the opening hook monologue lines need to accomplish.
### Smuggler opening hook (feeds #299)
**Lines needed:**
- 3-5 `enter_location` (Terminal) lines — operational warmth, different registers
- 2-3 `observe_npc` (Kael, Phase 1) lines — genuine affection, colleague register
- 2-3 `observe_npc` (Voss) lines — professional caution, working relationship
- 1-2 `observe_npc` (dock floor general) lines — belonging, routine
- 2-3 variant lines for non-standard opens (Kael absent, container held)
**Voice requirements:** Short sentences. Operational. Comfortable. The smuggler is home.
**Knowledge prereqs available:** All Background facts active from tick 0. No investigation prereqs needed for opening lines. `ring_routing_knowledge` at `knows_details` — lines can reference operational reality.
### Detective opening hook (feeds #300)
**Lines needed:**
- 3-5 `enter_location` (Terminal or bar) lines — observational register, arriving perspective
- 2-3 `observe_npc` (Sera, if bar variant) lines — recognition + warmth + slight wistfulness
- 2-3 `observe_npc` (Voss or unknown dock worker) lines — clinical assessment, outsider reading the room
- 2-3 `observe_anomaly` lines — lattice firing on first anomalies
- 2-3 variant lines for cold-arrival opens (terminal first, no Sera contact)
**Voice requirements:** Complete sentences. Precise. Observational. The detective is reading, not inhabiting.
**Knowledge prereqs available:** `suspects: investigation.manifest_discrepancy` active from tick 0. Lines can reference the anomaly as existing suspicion. `knows_details: world.commission_sova_jurisdiction` — institutional confidence is in voice from the start.
---
## Success Metric
The first 5 minutes are successful if:
1. The player has not been told what to do, but knows what to do
2. The player has met the FRIEND before anything suspicious has happened
3. The player has experienced at least one monologue line that felt like it came from inside them
4. The player is curious about something — the district, a person, the operational texture — without being told to be curious
5. The two characters feel like different people in different situations, not the same player in two costumes
---
## Narrative Texture: Character Voice in the First 5 Minutes
*Added by Paula — character voice and emotional arc guidance for opening line authoring.*
### The Smuggler: What the First 5 Minutes Must Establish
The smuggler's first 5 minutes are not about tension. They are about **belonging.** The player needs to feel, before anything goes wrong, that this place and these people are worth protecting. The crack cannot land unless there is something to crack.
**What "belonging" looks like in monologue:**
The first monologue lines should make the player feel they have been in this place for years. Not "here I am at the cargo terminal" but "home sweet home." Not "Kael Davan, my colleague" but "Kael's already at the dock. Good." The compression is the character. The first-name intimacy is the belonging. The operational awareness is comfort, not anxiety.
The smuggler's opening voice should feel like muscle memory. The characters observes their world the way you observe a room you've lived in for three years — not looking, knowing. Kael's position on the floor. The temperature of the recycled air. The ambient frequency that means the span gate cycled recently. All of this is ordinary. None of it needs to be explained. The character doesn't explain their home to themselves.
**The emotional trajectory of the first 5 minutes:**
Minutes 0-2: Comfort settling in. *"This is mine. I know where everything is."*
Minutes 2-4: Operational layer activates. *"The transition window opens in fifty minutes. Nothing complicated today."*
Minutes 4-5: The first hairline crack (seed-dependent). Not alarming. *"Kael seems distracted. Probably nothing."*
The crack should feel like a whisper, not an alarm. The player may not even notice it. That's correct. They're supposed to look back at it later and think: *"It was right there from the beginning."*
---
### The Detective: What the First 5 Minutes Must Establish
The detective's first 5 minutes are not about confidence. They are about **adjustment.** The player needs to feel, from the first second, that they are slightly outside the rhythm of this place — and that they are professional enough to work with that, but human enough to feel it.
**What "outside" looks like in monologue:**
The first monologue lines should carry a controlled observational distance. Not hostility toward the district — the detective isn't hostile, they're analytical. But the distance is real. *"Third day on station."* That's the key phrase. Not "day three of the investigation." That's too procedural. *"Third day on station"* is how a person counts the time in an unfamiliar place. It's still counting.
The detective's opening voice should feel like reading a room for the first time and doing it professionally. They are good at this. They notice things. But they're noticing them as data points, not as feelings. The feelings come later — when Sera appears and the register softens. That contrast is the emotional architecture of the detective's whole arc: professional distance punctured by personal warmth punctured by professional suspicion. The first 5 minutes should establish the distance so the warmth lands when Sera arrives.
**The emotional trajectory of the first 5 minutes:**
Minutes 0-2: Professional orientation. *"This is the scene. Let me read it."*
Minutes 2-4: Slight isolation crystallizes. *"The briefing said routine. The briefing is always wrong about something."*
Minutes 4-5: Warmth (if Sera variant) or first friction (if Terminal variant). Either: *"Good to know Sera's here."* Or: *"That supervisor has decided I'm a problem. Filing it."*
The detective's emotional starting point is competent isolation. They are good at their job and slightly alone. The player should feel both.
---
### Voice Rules for Opening Hook Lines
**Smuggler — rules for the author:**
1. No sentences longer than 10 words unless it's an operational calculation
2. Fragment sentences are not a mistake — they are the voice
3. First-name all NPCs the character knows. Never "dock worker Davan" — "Kael"
4. Emotional states are physical: "my stomach settled," "something loosened," not "I felt relieved"
5. The world is sensory: recycled air, cargo lubricant, span gate frequency. Not visual abstractions.
6. No self-analysis: the smuggler doesn't think about why they feel things, they think about what to do next
**Detective — rules for the author:**
1. Complete sentences, but not long. One clause per thought, mostly.
2. Analytical habit is always on: "file it," "note it," "that's twice now"
3. Surname-first for NPCs without personal relationships: "Voss," "Davan," "Korr" (not yet first names)
4. Emotional states come out sideways: not "I felt alone" but "At least someone in this district speaks my language"
5. Self-monitoring: the detective observes their own analysis for bias — "I'm reading too much into this" is a detective-voice line; the smuggler never thinks this
6. Institutional humor: wry, observational, appreciates irony at a distance — "The briefing said routine"
---
*Ticket #259 — First 5 minutes experience design. Systems: Gestalt. Narrative texture: Paula. Line authoring: Mellanie (#299, #300).*
+170
View File
@@ -0,0 +1,170 @@
# Flat NPC Memorable Trait Pass
**Ticket:** #307 | **Priority:** High | **Sprint:** 12
**Status:** v0.1 Draft
**Owners:** Paula (traits + design), Mellanie (line authoring)
**Decisions referenced:** D-024 (NPC generation model), D-029 (30/50/20 population split)
---
## 1. Purpose
The 30/50/20 population split (D-029) requires that 30% of NPCs are genuinely flat: routine + greeting, nothing more. But flat does not mean invisible. The problem this document solves is: **how do you make a flat NPC memorable without making them entangled?**
Without this, the investigation noise floor collapses. If every NPC the player encounters has depth, the player learns to treat them all as potential leads. The flat NPCs must be indistinguishable from signal at first glance — and remain background when investigated.
The answer is: **one memorable observable trait, one plausible-but-meaningless line, one visible routine.** Nothing more. The trait makes the NPC feel real. The line makes them feel briefly important. The routine reveals they were never a lead.
---
## 2. The Three Flat NPCs (v0.1)
### Pael — Maintenance Tech
**Role in district:** Technical maintenance worker. Repairs station systems, patches conduit runs, handles the small constant failures of a 40-year-old prefab-modular station.
**Memorable observable trait:**
Pael always has a specific piece of mechanical debris in one hand — a loose coupling, a stripped calibration pin, a shim. Not working with it. Not throwing it away. Just carrying it, the way people carry things they've picked up without thinking. Changes every few days. Has been seen patting his pockets as if looking for it when it's not there.
**Seems-important-but-isn't line (for Mellanie to author):**
Something that sounds like it could be about the ring but isn't. Possible directions:
- A complaint about recurring conduit failures in a specific corridor (a real maintenance problem, not a smuggling tell)
- A reference to "the same cargo coming through twice" that turns out to be a legitimate scheduling quirk
- A cryptic comment about "what goes on in Corridor B-7 at night" that turns out to mean: the ventilation cycles loudly and keeps him awake when he's doing late maintenance
**Visible routine:**
- Morning: arrives at maintenance depot, checks the day's work orders
- Mid-morning: visible in one of three locations (terminal catwalks, maintenance corridor access points, or near the span gate mechanism housing) — rotates daily
- Afternoon: back at depot, logging completed work
- Evening: does not go to The Last Shift — his social life is off-district
**Design note:** The rotation across three maintenance locations is important. It means a player who is watching Pael will see him in different places on different days, which can read as suspicious to an overly attentive detective. But the reason is just: different maintenance jobs. The rotation is the noise, not the signal.
**Dual-lens flat NPC:**
*Smuggler:* Pael is background. Known by sight, not by name unless prompted. "That's the maintenance tech — he's been patching conduit runs near corridor B-7 for weeks. Annoying. Have to check if he's gone before any runs."
*Detective:* Pael is a potential lead (maintenance tech → corridor access) who turns out to be nothing. The detective's monologue might flag him early ("Maintenance tech. Knows the corridors.") and unproductively file him as investigated-nothing later ("Pael's just doing maintenance. The B-7 visits are conduit issues. Nothing there.").
---
### Ren — Drifter
**Role in district:** Ren has no formal employment at Sova Transit. They appear to be between postings — the kind of person who stays at a transit hub while waiting for a berth on a freighter, or because the accommodation is cheap and they have no particular place to be. Has been here for about two months. Nobody knows much about them.
**Memorable observable trait:**
Ren is always reading. Not a lattice — a physical reader, the kind that looks like it's been repaired three times and barely holds together. They read in the bar, in the terminal waiting area, in the corridor access lobby. But here's the notable thing: they never seem to be further along in the same book. Whatever they're reading, they're not making progress, or they keep starting over, or they're reading the same page. Hard to tell from a distance.
**Seems-important-but-isn't line (for Mellanie to author):**
Something that sounds conspiratorial but is just evasive privacy. Possible directions:
- "I don't ask what goes in containers. Safer that way." (Said to anyone who asks what they do here — it's just a social habit, not self-incrimination)
- "I've seen a lot of stations. This one's quieter than most." (Sounds like they're watching something, but they're just making conversation with anyone who'll have it)
- References to "a package I'm waiting for" that turns out to be a personal delivery, not a criminal handoff
**Visible routine:**
- Mornings: bar, before it opens fully. Lera tolerates early arrivals. Ren nurses a hot drink and reads.
- Afternoons: terminal waiting area, near the departure board. Not going anywhere. Just watching.
- Evenings: bar again, stays late. Quiet, doesn't bother people, drinks slowly.
**Design note:** Ren's presence in the terminal waiting area watching the departure board is the designed mislead. They look like they're surveilling. They're just waiting for something — possibly a ship that hasn't arrived, possibly just habit. The departure board is the most interesting thing in their day.
**Dual-lens flat NPC:**
*Smuggler:* "Ren's been here two months and nobody knows who they are. If I were paranoid, that'd worry me. But if they were commission, they'd have introduced themselves. Probably just between postings." [Assessment: filed, not relevant.]
*Detective:* Ren is the most suspicious person in the district who has nothing to do with anything. "Unknown. Unaffiliated. Watching the departure board daily. Background check shows nothing — no warrants, no commission flags, three previous transit hub stays in the last year. Probably a drifter. But I'm keeping an eye." The detective may waste 10-15 minutes of investigation time on Ren before filing them as noise.
---
### Tev — Lookout
**Role in district:** Tev works at the The Last Shift as part-time bar support — cleaning tables, stacking deliveries, doing the small physical work that Lera doesn't want to do herself. Not a formal bartender. Present most evenings, some afternoons.
**Memorable observable trait:**
Tev has a specific relationship with the bar entrance. Every time someone walks in, Tev notices — not obviously, not a full head-turn, but a brief flick of attention in that direction. It's trained behavior, or old habit. They check who's coming in. Every time. Including people they've seen every day for months. The tell is subtle but consistent, and the pattern becomes recognizable once noticed.
**Seems-important-but-isn't line (for Mellanie to author):**
Something that sounds like a lookout's report but is just social awareness. Possible directions:
- "Commission's in again" (said to Lera, routine observation — they note the detective's presence the same way they'd note a regular who comes in after a long absence: as social texture, not operational intelligence)
- "Kael's running late." (Said to nobody in particular. Not a ring update — just someone who keeps track of who shows up)
- Overheard to a regular: "You hear about the new cargo schedule?" — turns out to be about legitimate freight delays, nothing to do with the ring
**Visible routine:**
- Afternoons: setting up for the evening shift, cleaning, stocking
- Evenings: working the floor. Tables cleaned, deliveries stacked, entrance watched
- Late evenings: last to leave after the bar closes, stacking chairs
**Design note:** Tev's entrance-watching is the primary mislead. In a bar where a ring operates peripherally, having someone who tracks everyone who comes in reads as a lookout. And functionally, Tev probably IS a lookout for Lera — but Lera's lookout, not the ring's. Lera likes to know who's in her bar before they're at the bar. Tev's habit is loyalty to Lera, not criminal coordination.
**Dual-lens flat NPC:**
*Smuggler:* "Tev sees everything that comes through that door. Lera's eyes. Not our problem as long as Lera's solid." [Tev is background, not a concern.]
*Detective:* "The bar worker — Tev. Watches the entrance. Consistent pattern. In a criminal-adjacent environment, that's potentially a lookout. But their behavior doesn't cluster with any other ring indicators. Social texture, not operational role. File under 'observed, not flagged.'" The detective may ask Lera about Tev — Lera's answer ("been here three years, good worker, minds their own business") is uninformative and accurate.
---
## 3. Design Principles for Flat NPCs
**Principle 1: The trait must be observable without interaction.**
The player should be able to recognize Pael (carrying something), Ren (reading), and Tev (watching the door) from a distance, through fog-of-war partial visibility, without ever having a conversation. The trait is a visual behavior, not a dialogue reveal.
**Principle 2: The seems-important-but-isn't line is bait.**
Every flat NPC needs exactly one line that makes the player think "wait, is this person involved?" followed by a resolution that shows they're not. This is how the noise floor works: the player wastes attention on the noise and learns that not everything is a lead. This is both a design lesson and a realistic texture of investigation.
**Principle 3: The routine must explain the suspicious-looking behavior.**
Pael is in Corridor B-7 because of maintenance. Ren watches the departure board because they're waiting for something. Tev watches the door because Lera wants them to. In every case, the explanation is boring and true. The mundane explanation is the design.
**Principle 4: Dual-lens treatment for all three.**
Each flat NPC is perceived differently by the smuggler (background assessment, no anxiety) and the detective (potential lead, then filed as noise). The difference in perception is the same as for all other NPCs — but for flat NPCs, the detective's arc is: initial flag → investigation → nothing. That's the journey this population is designed to produce.
---
## 4. Content Requirements Summary
| NPC | Paula delivers | Mellanie delivers |
|-----|---------------|-----------------|
| Pael | Trait, dual-lens design, routine | 1 seems-important line + 2-3 variants, 2 dialogue lines (greeting-level) |
| Ren | Trait, dual-lens design, routine | 1 seems-important line + 2-3 variants, 2 dialogue lines |
| Tev | Trait, dual-lens design, routine | 1 seems-important line + 2-3 variants, 2 dialogue lines |
**Mellanie note:** The seems-important lines should feel like they could be something. They shouldn't feel like obviously random NPC chatter. The art is in the plausible misdirection — specific enough to be memorable, mundane enough to resolve as nothing. Voice for all three flat NPCs is brief, non-explanatory, station-working-class register.
---
## 5. YAML Stub for Content Authoring
Each flat NPC needs a minimal NPC profile. Example for Pael:
```yaml
canonical_id: "npc:pael"
display_name: "Pael"
tier: 3
pattern: "FLAT"
motivation: "WORKER"
description: >
Maintenance tech. Handles station system repairs across the district.
Observable: always carrying a piece of mechanical debris. Changes what
it is every few days.
routine:
summary: >
Maintenance depot (morning arrival), one of three maintenance locations
(mid-morning, rotates), back to depot (afternoon), off-district (evening).
trust_levels:
surface: >
Minimal. Complaints about current repair job, maintenance schedule.
Never personal, always task-focused.
real: "Not accessible — flat NPC."
secret: "Not accessible — flat NPC."
notes: >
FLAT NPC. No triangle membership. No ring connection. The B-7 corridor
visits are conduit maintenance. The investigation dead-end is the point.
```
---
*Ticket #307 — Flat NPC memorable trait pass. 3 flat NPCs. Paula: traits. Mellanie: lines.*
+273
View File
@@ -0,0 +1,273 @@
# Knowledge Flags — v0.1 Quick Reference
**For:** Mellanie (content authoring), Paula (narrative design)
**Source:** docs/design/knowledge-vocabulary-v01.md (full specification)
**Ticket:** #309 — unblocks #299, #300 (opening hook content)
**Date:** 2026-02-19
---
## How to Use This Document
When writing a monologue line that should only fire once the character knows something specific, add a `prerequisites:` block. Use the fact IDs and shorthand from this document — do not invent strings.
### The three prerequisite types
```yaml
# 1. Entity relationship check
prerequisites:
relationship:
target: npc:kael-davan # use the NPC's kebab-case file name
state: friendly # unknown | known | friendly | person_of_interest | hostile
# 2. World fact check
prerequisites:
facts:
- fact_id: investigation.kael_corridor_meeting
min_confidence: suspects # suspects | knows_of | knows_details | direct
# 3. Entity attribute check (rare — use for specific behavioral tells)
prerequisites:
entity_attributes:
- entity: npc:kael-davan
key: behavior_flags
value: avoidance
```
All conditions are AND. For OR logic, write two lines with different prerequisites targeting the same trigger.
---
## Confidence Level Cheat Sheet
| Level | Shorthand | Plain English | When to use it |
|-------|-----------|---------------|----------------|
| `suspects` | `suspects:` | "Something's off" | Early-game, vague lines — player has noticed but not confirmed |
| `knows_of` | `knows:` | "X is connected to Y" | Mid-game, specific lines — player has real information |
| `knows_details` | `knows_details:` | "X did Y because Z" — actionable | Late-game, confrontation-adjacent lines — player knows enough to act |
| `direct` | `direct:` | "I'm looking at it right now" | Set by perception system, not authored — use only for immediate-observation monologue |
**Design rule:** Most lines need no prerequisites (they fire freely on trigger). Gated lines should reveal something the ungated versions can't say. Don't over-gate — use prerequisites to create *surprise when a new line appears*, not to withhold most of the content.
---
## Valid Fact IDs
### `investigation.*` — case findings
Use for: specific events witnessed, evidence found, NPC behaviors confirmed through investigation.
```
investigation.kael_corridor_meeting Kael was seen in B-7 off-shift, meeting someone
investigation.kael_corridor_sighting Detective's initial B-7 observation (detective only)
investigation.kael_unknown_contact Kael's contact is not from the regular rotation
investigation.kael_ring_membership Kael is a ring member
investigation.kael_exit_attempt Kael is trying to leave the ring
investigation.manifest_discrepancy Cargo manifests show irregularities
investigation.cargo_anomaly Specific container/weight/route anomaly
investigation.shift_mismatch Shift records don't match observed personnel
investigation.voss_ring_involvement Voss manages schedule changes to cover drops
investigation.torek_spending_pattern Torek is spending beyond his means
investigation.drin_inspection_pattern Drin's inspections are conspicuously lax
investigation.drin_gambling_debt Drin has a gambling debt — leverage
investigation.oversight_gap_pattern Inspection gaps are systematic, not random
investigation.surveillance_gaps Specific corridor camera blind spots
investigation.smuggling_route The exact smuggling route through the station
investigation.ring_existence A smuggling ring is definitely operating here
investigation.renn_courier_role Renn is a courier for the ring
investigation.corridor_regular_use Maintenance corridors are routinely used for drops
investigation.container_delay A specific container has been held too long
investigation.sera_evidence_held Sera has unreported manifest evidence
investigation.naia_lattice_condition Naia's lattice is degrading — the medical components are for her
investigation.ring_leadership Who directs ring operations (late discovery)
```
### `awareness.*` — situational awareness
Use for: who's present right now, active threats, operational status.
```
awareness.detective_presence A Commission detective is on Sova
awareness.detective_investigating_ring Detective is specifically targeting the ring
awareness.commission_audit_scheduled An audit is coming — officially scheduled
awareness.ring_route_compromise The current drop route may be blown
awareness.surveillance_change Corridor surveillance patterns have changed
awareness.ring_tension_escalation Something is wrong inside the ring
awareness.inspection_sweep_active Active sweep in progress right now
```
### `behavioral.*` — recognized patterns
Use for: tells and habits confirmed through multiple observations.
```
behavioral.kael_evasion Kael looks left when lying — tell recognized
behavioral.kael_behavioral_change Kael is quieter, distracted, different from normal
behavioral.sera_avoidance_pattern Sera consistently avoids Torek Lintar
behavioral.sera_topic_deflection Sera deflects Commission/manifest topics
behavioral.sera_kiosk_pattern Sera stops at the same kiosk at the same time
behavioral.sera_knowledge_detail Sera's offhand comments reveal too much routing knowledge
behavioral.voss_awareness Voss is tracking people — beyond normal supervisor behavior
```
### `knowledge.*` — operational knowledge
Use for: procedural/institutional knowledge the character has accumulated or knows from background.
```
knowledge.ring_routing_knowledge Full drop routes and timing protocols (smuggler background)
knowledge.ring_bar_operations The bar has a role in ring communications
knowledge.bar_back_room_significance The back room at The Last Shift matters somehow
knowledge.bar_ring_awareness Bar regulars have varying awareness of the ring
knowledge.kael_exit_plan Kael has a specific exit plan — timing, contact, conditions
```
### `contraband.*` — smuggling ring world facts
Use for: what the ring is moving and how.
```
contraband.ring_exists A smuggling operation is running through Sova (smuggler starts at knows_details)
contraband.ring_lattice_components The ring moves unlicensed lattice mods
contraband.ring_medical_components The ring also moves diverted medical-grade lattice
contraband.supply_chain Understanding of where the components come from
contraband.severance_tech_presence Severance counter-surveillance tech is in play
contraband.drop_tonight A drop is scheduled for tonight
```
### `location.*` — spatial knowledge
Use for: what a location is, who has access, what it's used for.
```
location.corridor_b7_restricted Corridor B-7 requires credentials
location.corridor_b7_drop_site B-7 is an active drop site
location.maintenance_corridors_access Character can navigate the maintenance corridor layout
location.bay4_sealed Bay 4 is sealed — inspection or cover
location.span_gate_schedule_gap Span gate has a window that enables unseen drops
```
### `relationship.*` — inter-NPC connections
Use for: third-party connections between NPCs (not player-NPC relationship).
```
relationship.kael_naia_connection Kael and Naia are romantically partnered
relationship.ring_membership Player has identified specific ring members
relationship.trust_network Who trusts whom inside the ring (operational level)
```
### `social.*` — social positioning
Use for: player's social standing in a specific context.
```
social.bar_regular_status Player is a recognized regular at The Last Shift
```
---
## Valid NPC IDs (for `relationship:` and `entity_attributes:` blocks)
```
npc:kael-davan Dock worker, smuggler's FRIEND, ring member
npc:sera-venn Commission field tech, detective's FRIEND
npc:voss Shift supervisor, ring operative
npc:maret-korr Scheduler
npc:drin Inspector, ring-adjacent
npc:torek-lintar Dock worker, spending beyond means
npc:naia-tamm Ring adjacent, Kael's partner
npc:nils-davan Kael's brother, dock worker
npc:lera-sessik Bar owner at The Last Shift
npc:renn Drifter, ring courier
npc:pell Maintenance
npc:sess Dock worker
npc:tav Dock worker
npc:sabel Bar regular
npc:harek Bar regular / dock adjacent
npc:olin Ring operative
npc:resha Ring operative
npc:devra Ring operative
```
---
## Character Starting Knowledge
Lines in the opening sequence should fire on Background knowledge — these facts are in the graph from tick 0, no investigation needed.
### Smuggler — knows at game start
The smuggler IS in the ring. These prerequisites will always pass:
| Fact ID | Confidence |
|---------|-----------|
| `contraband.ring_exists` | `knows_details` |
| `knowledge.ring_routing_knowledge` | `knows_details` |
| `contraband.ring_lattice_components` | `knows_details` |
| `location.corridor_b7_drop_site` | `knows_details` |
| `location.maintenance_corridors_access` | `knows_details` |
| `world.lattice_regulation_active` | `knows_of` |
### Detective — knows at game start
The detective has anomalies flagged, but nothing confirmed:
| Fact ID | Confidence |
|---------|-----------|
| `investigation.manifest_discrepancy` | `suspects` |
| `contraband.ring_exists` | `suspects` |
| `world.lattice_regulation_active` | `knows_details` |
| `location.corridor_b7_restricted` | `knows_of` |
---
## Opening Hook Prereq Patterns (for #299/#300)
### Smuggler opening lines (first 10-15 monologue lines)
The smuggler arrives with operational awareness. Use these patterns:
```yaml
# No prereqs — fires for any player on arrival (atmospheric, sets tone)
trigger: enter_location
tags: [arrival, atmospheric]
# Fires when smuggler sees a specific known NPC
trigger: observe_npc
# No prereqs needed — smuggler knows everyone from background
tags: [npc, kael, warm]
# First sign something might be wrong
prerequisites:
facts:
- fact_id: contraband.ring_exists
min_confidence: knows_details # Always true for smuggler — line fires immediately
tags: [operational, setup]
```
### Detective opening lines (first 10-15 monologue lines)
The detective arrives with a case hypothesis. Use these patterns:
```yaml
# No prereqs — arrives fresh, first impressions
trigger: enter_location
tags: [arrival, institutional]
# First lattice-flag moment
prerequisites:
facts:
- fact_id: investigation.manifest_discrepancy
min_confidence: suspects # True from background — detective has the anomaly flagged
tags: [investigation, lattice]
# Sees a person of interest for the first time
trigger: observe_npc
# No prereqs for first sighting — use relationship gating on follow-up lines
tags: [npc, observation]
```
---
*Quick reference for Sprint 12 content authoring. Full specification: docs/design/knowledge-vocabulary-v01.md*
+531
View File
@@ -0,0 +1,531 @@
# Knowledge Vocabulary for v0.1 Content
**Source:** D-041 (Knowledge Graph Data Model), D-035 (Tag Taxonomy), D-028 (Dialogue Architecture), D-075 (Dialogue Filtering)
**Ticket:** #368
**Unblocks:** #309 (knowledge state vocabulary for v0.1) → #299, #300 (opening hook content)
**Date:** 2026-02-19
---
## Overview
Two knowledge stores, one prerequisite system.
**Entity knowledge** — what your character knows *about a specific NPC*. Stored per entity in `EntityKnowledge.known_attributes`. Six attribute categories. Accessed via `relationship:` and `entity_attributes:` blocks in `prerequisites:`.
**Fact knowledge** — what your character knows *about the world*. Stored in `KnowledgeGraph.facts`, keyed by `FactId` in `category.topic` format. Eight categories. Accessed via `facts:` blocks in `prerequisites:`.
Both stores gate monologue lines (D-035 `prerequisite` tag) and dialogue access tiers (D-028 / D-075).
---
## Part 1: Confidence Levels
Four levels. Ordered ascending. Knowledge decays downward over time (once per game-minute, per D-031/D-026).
| Engine Name | Author Shorthand | Plain English | What It Unlocks |
|-------------|-----------------|---------------|-----------------|
| `Suspects` | `suspects:` | "Something's off" / "I've heard the name" | Initial investigation, vague monologue, open-ended questions |
| `KnowsOf` | `knows:` | "X is involved in Y" / "X has a connection to Z" | Topic-specific dialogue, specific monologue, peer-tier access (D-075) |
| `KnowsDetails` | `knows_details:` | "X did Y at Z because W" — actionable | Confrontation unlock, secret-tier dialogue (D-075), detailed monologue |
| `Direct` | `direct:` | "I'm looking at X right now" | Live position, full rendering fidelity — **set by perception system, not authored** |
**Decay path:** `Direct``KnowsDetails``KnowsOf``Suspects` (floor — `Suspects` does not decay further).
**For content authors:** Lines gated at `suspects:` fire early and broadly. Lines gated at `knows_details:` fire late and rarely. Design monologue arcs with both ends — arrival lines (no prereqs) and deep-investigation lines (high confidence prereqs).
---
## Part 2: Entity Knowledge
Entity knowledge tracks what character A knows about a specific NPC B. Stored in `EntityKnowledge.known_attributes` as a BTreeMap of string keys. The `EntityKnowledge` struct also carries `confidence`, `relationship`, `state`, and `source` fields at the top level.
### 2.1 Entity Knowledge Categories
Six categories of entity-level knowledge for v0.1:
| Category | What It Covers | Key Attributes |
|----------|---------------|----------------|
| **identity** | Who they are — name, role, faction affiliation | `name`, `role`, `faction`, `species` |
| **location** | Where they go — last known position, routine zones | `routine_pattern` (observed), `last_known_position` (system-managed) |
| **behavior** | How they act — tells, flags, pattern anomalies | `routine_pattern`, `behavior_flags`, `tell_observed` |
| **relationship** | Their connections — to the player, to other NPCs | `relationship_type`, `trust_level`, `loyalty_assessment` |
| **secret** | What they're hiding — leverage, contradiction flags | `leverage_held`, `leverage_confidence`, `contradiction_flagged` |
| **contraband** | Ring involvement — membership, operational knowledge | `faction` (ring member), `behavior_flags` (contraband-tagged) |
Full attribute definitions: `content/global/knowledge/entity-attributes.yaml`
### 2.2 Prerequisite Format — Entity Checks
```yaml
# Check relationship state with a specific entity
prerequisites:
relationship:
target: npc:kael-davan
state: friendly # unknown | known | friendly | person_of_interest | hostile
# Check a specific entity attribute value
prerequisites:
entity_attributes:
- entity: npc:kael-davan
key: behavior_flags
value: avoidance
# Both together (all conditions AND)
prerequisites:
relationship:
target: npc:kael-davan
state: person_of_interest
entity_attributes:
- entity: npc:kael-davan
key: contradiction_flagged
value: meeting_unknown_contact
```
**Relationship states** (D-033): `unknown` · `known` · `friendly` · `person_of_interest` · `hostile`
### 2.3 Entity Shorthand Conventions
For planning and #309 shortlist authoring:
| Shorthand | Meaning | Prerequisite Equivalent |
|-----------|---------|------------------------|
| `met: npc:{id}` | NPC is in the knowledge graph at all | entity entry exists, any confidence |
| `has_seen: npc:{id}` | NPC was directly observed in LOS at least once | Direct confidence was set at least once |
| `knows_npc: npc:{id}` | Character knows who this NPC is (KnowsOf+) | entity confidence ≥ KnowsOf |
| `friend: npc:{id}` | Friendly relationship established | relationship state = friendly |
| `poi: npc:{id}` | NPC flagged as person of interest | relationship state = person_of_interest |
| `hostile: npc:{id}` | Character perceives NPC as hostile | relationship state = hostile |
---
## Part 3: Fact Knowledge
Fact knowledge tracks non-entity information — events, situations, operational states, world conditions. Stored in `KnowledgeGraph.facts` keyed by `FactId`.
### 3.1 Fact ID Naming Convention
**Format:** `{category}.{topic_in_snake_case}`
**Examples:**
- `investigation.kael_corridor_meeting`
- `awareness.detective_presence`
- `behavioral.sera_avoidance_pattern`
- `knowledge.ring_routing_knowledge`
**Naming rules:**
- Category is always one of the eight below
- Topic uses `snake_case`, no spaces, no hyphens
- Topic should name one discrete fact — specific enough to gate a specific line
- Avoid generic topics: `thing_happened` → no; `kael_seen_with_unknown_contact` → yes
- NPC facts use the NPC's canonical short name: `kael`, `sera`, `voss`, `torek`, `drin`, `renn`
- Shared facts across characters use identical IDs — same fact, different confidence per character
### 3.2 Fact Categories
Eight categories for v0.1:
| Category | What It Tracks | Namespace Usage |
|----------|---------------|-----------------|
| `investigation` | Case-specific findings — events witnessed, evidence discovered, NPC behaviors confirmed | Primary investigation facts, both characters |
| `awareness` | Situational awareness — who's present, active threats, operational status changes | Real-time or near-term situational facts |
| `behavioral` | Behavioral patterns recognized across multiple observations | Accumulated tell data, avoidance patterns, habit deviations |
| `knowledge` | Operational knowledge — ring procedures, bar dynamics, specific institutional mechanics | Character-background knowledge + learned procedures |
| `contraband` | Smuggling ring facts — existence, components, routes, supply chain | Ring-specific world facts |
| `location` | Location-specific facts — access rights, significance, usage patterns | Spatial knowledge that drives spatial decisions |
| `relationship` | Inter-NPC connections — who knows who, what connects them | Third-party relationship knowledge (not player-NPC) |
| `social` | Social positioning — standing in a social context, bar regular status | Context-specific social facts |
> **Note on `progress` category:** A `progress` namespace exists in the file structure but is not used directly in prerequisite checks. Narrative phase gating uses relationship states and specific investigation facts rather than explicit progress flags. Authors should NOT create `progress.*` facts for opening hook content — use investigation fact chains instead.
### 3.3 Prerequisite Format — Fact Checks
```yaml
# Single fact check
prerequisites:
facts:
- fact_id: investigation.kael_corridor_meeting
min_confidence: suspects
# Multiple facts (all must pass)
prerequisites:
facts:
- fact_id: knowledge.ring_routing_knowledge
min_confidence: knows_details
- fact_id: awareness.detective_presence
min_confidence: knows_of
# Combined entity + fact check
prerequisites:
relationship:
target: npc:kael-davan
state: friendly
facts:
- fact_id: investigation.kael_corridor_meeting
min_confidence: knows_of
```
**`min_confidence` values:** `suspects` · `knows_of` · `knows_details` · `direct`
### 3.4 Fact Shorthand Conventions
For planning, shortlists, and #309 authoring:
| Shorthand | Meaning | Prerequisite Equivalent |
|-----------|---------|------------------------|
| `suspects: {fact_id}` | Fact known at Suspects level | `min_confidence: suspects` |
| `knows: {fact_id}` | Fact known at KnowsOf level | `min_confidence: knows_of` |
| `knows_details: {fact_id}` | Fact known at KnowsDetails level | `min_confidence: knows_details` |
| `phase: {progress_fact}` | Narrative milestone reached | relationship-state check or specific investigation fact |
---
## Part 4: v0.1 Fact Catalog — Transit District
Canonical fact list for the Sova Transit District scenario. All fact IDs in content must appear here. This is the source of truth for #309 (knowledge state vocabulary for v0.1).
Facts marked **[S]** are relevant to the Smuggler, **[D]** to the Detective, **[B]** to both.
### 4.1 Investigation Facts
Case-specific findings accumulated through play.
| Fact ID | Plain Description | Typical Confidence When Learned | Chars |
|---------|------------------|--------------------------------|-------|
| `investigation.kael_corridor_meeting` | Kael was seen in corridor B-7 off-shift, apparently meeting someone | `suspects` (glimpsed) → `knows_of` (confirmed) | B |
| `investigation.kael_corridor_sighting` | Kael spotted in the maintenance corridors at an unusual hour (detective's initial observation ID) | `suspects` | D |
| `investigation.kael_unknown_contact` | The person Kael met in B-7 is unknown — not from the regular rotation | `suspects``knows_of` | B |
| `investigation.kael_ring_membership` | Kael is a member of the smuggling ring | `suspects``knows_details` | B |
| `investigation.manifest_discrepancy` | Cargo manifests show irregularities — containers, weights, routing anomalies | `suspects` (detective lattice scan) → `knows_of` | B |
| `investigation.manifest_discrepancy` | *Same ID, different source:* Smuggler notices an anomalous entry in shared manifest data | `suspects``knows_of` | S |
| `investigation.cargo_anomaly` | A specific cargo anomaly — wrong container, wrong weight, wrong route | `suspects` (observation) | D |
| `investigation.shift_mismatch` | Shift records don't match observed personnel — someone's covering for someone | `knows_of` | B |
| `investigation.voss_ring_involvement` | Voss is involved in ring operations — schedule changes cover for drops | `knows_of` (pattern) → `knows_details` | B |
| `investigation.torek_spending_pattern` | Torek is spending beyond his means — drinks, visible spending uptick | `knows_of` (observation) | B |
| `investigation.drin_inspection_pattern` | Drin's inspection style is conspicuously casual — clearing containers without real checking | `knows_of` | D |
| `investigation.drin_gambling_debt` | Drin has a gambling debt — provides leverage for whoever is running him | `knows_details` | D |
| `investigation.oversight_gap_pattern` | Inspection gaps are systematic, not accidental — someone designed the blind spots | `knows_of``knows_details` | D |
| `investigation.surveillance_gaps` | Specific surveillance gaps in the maintenance corridors — where the cameras don't reach | `knows_details` | B |
| `investigation.smuggling_route` | The specific smuggling route through the station | `knows_details` | B |
| `investigation.ring_existence` | A smuggling operation is definitely running through Sova Transit | `suspects` (accumulating anomalies) → `knows_of` | D |
| `investigation.renn_courier_role` | Renn operates as a courier in the ring — runs packages between drop sites | `knows_of` | D |
| `investigation.corridor_regular_use` | The maintenance corridors are regularly used for smuggling runs, not just incidentally | `knows_of` | D |
| `investigation.container_delay` | A specific container has been held in temporary storage longer than normal — something is wrong | `suspects` | S |
| `investigation.sera_evidence_held` | Sera has unreported evidence about manifest irregularities — sitting on it deliberately | `suspects` (avoidance pattern) → `knows_details` (Sera discloses) | D |
| `investigation.kael_exit_attempt` | Kael is actively trying to leave the ring | `knows_of``knows_details` | B |
| `investigation.naia_lattice_condition` | Naia's neural lattice is degrading — the medical components are for her | `knows_details` (Kael discloses or Naia encountered) | B |
> **Normalization note:** `investigation.kael_corridor_meeting` (smuggler perspective, meeting focus) and `investigation.kael_corridor_sighting` (detective perspective, initial observation) describe different observations of the same event. They should remain as separate IDs — the detective may reach `kael_corridor_meeting` after establishing `kael_corridor_sighting`. Content authors should prefer `kael_corridor_meeting` when writing lines that reference the meeting itself.
### 4.2 Awareness Facts
Situational awareness — who's here, what's active, what's changed.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `awareness.detective_presence` | A Commission detective is operating on Sova Station | `knows_of` (rumor, observation) | S |
| `awareness.detective_investigating_ring` | The detective is specifically investigating the ring, not a routine audit | `knows_details` | S |
| `awareness.commission_audit_scheduled` | A Commission audit is coming — schedule confirmed | `knows_of` | B |
| `awareness.ring_route_compromise` | The current smuggling route may be compromised or under observation | `suspects``knows_of` | S |
| `awareness.surveillance_change` | Surveillance patterns in the corridors have changed recently | `suspects` | S |
| `awareness.ring_tension_escalation` | Tension inside the ring is escalating — someone is scared or making moves | `suspects``knows_of` | S |
| `awareness.inspection_sweep_active` | Active inspection sweep in progress right now | `direct` | B |
### 4.3 Behavioral Facts
Patterns recognized through repeated observation across multiple scenes.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `behavioral.kael_evasion` | Kael uses a specific evasion tell (looks left when lying) — recognized post-conversation | `direct` (set in conversation) | S |
| `behavioral.kael_behavioral_change` | Kael's behavior has noticeably changed recently — quieter, distracted, different routine | `suspects``knows_of` | S |
| `behavioral.sera_avoidance_pattern` | Sera consistently avoids Torek Lintar — leaves when he arrives, repositions | `suspects` (one observation) → `knows_of` (pattern confirmed) | B |
| `behavioral.sera_topic_deflection` | Sera deflects specific topics — Commission work, manifest audits | `suspects``knows_of` | B |
| `behavioral.sera_kiosk_pattern` | Sera habitually stops at the same kiosk at the same time — a recognizable routine | `knows_of` | B |
| `behavioral.sera_knowledge_detail` | Sera's offhand comments reveal detailed knowledge of manifest routing she shouldn't have | `knows_of` | S |
| `behavioral.voss_awareness` | Voss appears to be tracking people — situational awareness beyond normal supervisor behavior | `suspects` | D |
### 4.4 Knowledge Facts
Operational knowledge — ring procedures, social dynamics, institutional mechanics.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `knowledge.ring_routing_knowledge` | Full knowledge of the ring's drop routes and timing protocols | `knows_details` | S (background) |
| `knowledge.ring_bar_operations` | The bar (The Last Shift) has a role in ring communications or social cover | `knows_of``knows_details` | B |
| `knowledge.bar_back_room_significance` | The bar's back room has significance — meeting space, handoff point, or storage | `suspects``knows_details` | D |
| `knowledge.bar_ring_awareness` | The bar's regulars have varying levels of ring awareness — it's an open secret at some level | `knows_of` | B |
| `knowledge.kael_exit_plan` | Kael has a specific plan for exiting the ring — timing, conditions, contact | `knows_details` | B |
### 4.5 Contraband Facts
Smuggling ring world facts — existence, composition, operations.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `contraband.ring_exists` | A smuggling operation is running through Sova Transit District | `suspects` (detective) / `knows_details` (smuggler, background) | B |
| `contraband.ring_lattice_components` | The ring moves unlicensed lattice components — aftermarket neural mods | `knows_of``knows_details` | B |
| `contraband.ring_medical_components` | The ring also diverts medical-grade lattice replacements from Syndic supply chains | `knows_details` | B |
| `contraband.supply_chain` | Understanding of the ring's supply chain — where components come from | `knows_details` | B |
| `contraband.severance_tech_presence` | Severance counter-surveillance technology is in play on the station | `suspects` | D |
| `contraband.drop_tonight` | A drop is scheduled for tonight specifically | `knows_details` | S |
### 4.6 Location Facts
Location-specific access, restrictions, and significance.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `location.corridor_b7_restricted` | Corridor B-7 is a restricted access zone — requires credentials | `knows_of` | B |
| `location.corridor_b7_drop_site` | B-7 is an active drop site for the ring | `knows_details` | B |
| `location.maintenance_corridors_access` | Character knows the maintenance corridor layout and can navigate it | `knows_of` (detective, investigation) / `knows_details` (smuggler, background) | B |
| `location.bay4_sealed` | Bay 4 is sealed — inspection or deliberate cover | `suspects` | B |
| `location.span_gate_schedule_gap` | The span gate schedule has a window that enables unseen drops | `knows_details` | S |
### 4.7 Relationship Facts
Third-party inter-NPC connections — what one character knows about relationships between others.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `relationship.kael_naia_connection` | Kael and Naia are romantically partnered — not just colleagues | `knows_of``knows_details` | B |
| `relationship.ring_membership` | Character has identified specific ring members | `knows_of` (list incomplete) → `knows_details` (full ring) | B |
| `relationship.trust_network` | Understanding of who trusts whom inside the ring | `knows_details` | S |
### 4.8 Social Facts
Social positioning in a specific context.
| Fact ID | Plain Description | Typical Confidence | Chars |
|---------|------------------|-------------------|-------|
| `social.bar_regular_status` | Character is established as a regular at The Last Shift — recognized, trusted in that context | `knows_of` | B |
---
## Part 5: Prerequisite Format Quick Reference
### Full YAML format (engine canonical)
```yaml
- id: transit_m_s_025
character: smuggler
text: "Kael keeps checking his lattice. Waiting for a message? Not like him to be jumpy."
trigger: observe_anomaly
prerequisites:
relationship: # Optional: entity relationship state check
target: npc:kael-davan
state: friendly
facts: # Optional: world fact checks — ALL must pass
- fact_id: investigation.kael_corridor_meeting
min_confidence: suspects
entity_attributes: # Optional: entity attribute value checks
- entity: npc:kael-davan
key: behavior_flags
value: lattice_checking
priority: 7
tags: [npc, kael, tell, friend-arc]
```
### Shorthand map (for #309 planning, not used directly in YAML)
| Shorthand | Full YAML | Notes |
|-----------|-----------|-------|
| `suspects: investigation.kael_corridor_meeting` | `facts: [{fact_id: investigation.kael_corridor_meeting, min_confidence: suspects}]` | |
| `knows: awareness.detective_presence` | `facts: [{fact_id: awareness.detective_presence, min_confidence: knows_of}]` | |
| `knows_details: contraband.ring_routing_known` | `facts: [{fact_id: contraband.ring_routing_known, min_confidence: knows_details}]` | |
| `met: npc:kael-davan` | entity in KnowledgeGraph at any confidence | |
| `has_seen: npc:kael-davan` | entity had Direct confidence at some point | |
| `friend: npc:kael-davan` | `relationship: {target: npc:kael-davan, state: friendly}` | |
| `poi: npc:kael-davan` | `relationship: {target: npc:kael-davan, state: person_of_interest}` | |
### Multi-condition logic
All conditions within a `prerequisites:` block are **AND** — all must pass for the line to be eligible.
To express **OR** logic: author multiple lines targeting the same trigger with different prerequisites. The engine selects the highest-priority eligible line.
---
## Part 6: Common Authoring Patterns
### Pattern 1: Progressive revelation — early
Line fires from the first suspicious observation. No prior facts required.
```yaml
trigger: observe_anomaly
# No prerequisites — fires on any anomaly observation
tags: [environmental, caution]
```
### Pattern 2: Progressive revelation — mid
Line fires after initial suspicion is established:
```yaml
prerequisites:
facts:
- fact_id: investigation.kael_corridor_meeting
min_confidence: suspects
```
### Pattern 3: Progressive revelation — late
Line fires with actionable evidence in hand:
```yaml
prerequisites:
facts:
- fact_id: investigation.kael_ring_membership
min_confidence: knows_details
```
### Pattern 4: THE FRIEND arc — contradiction trigger
Line fires when the contradiction is discovered (both relationship state and investigative knowledge required):
```yaml
prerequisites:
relationship:
target: npc:kael-davan
state: person_of_interest
facts:
- fact_id: investigation.kael_corridor_meeting
min_confidence: knows_of
tags: [npc, kael, contradiction, friend-arc]
```
### Pattern 5: D-075 trust tier — secret-tier dialogue gate
Secret-tier dialogue requires both `friendly` relationship AND `knows_details` on target (D-075):
```yaml
# This is handled by the dialogue engine — content authors do NOT need to
# add this as a prerequisite on dialogue lines. It applies automatically.
# For MONOLOGUE lines that mirror the emotional weight of secret disclosure:
prerequisites:
relationship:
target: npc:kael-davan
state: friendly
facts:
- fact_id: investigation.kael_exit_attempt
min_confidence: knows_details
```
### Pattern 6: Opening hook (for #299/#300)
Opening hook lines fire from character Background knowledge. The character arrives already knowing these things — do not gate them on investigation facts.
```yaml
# Smuggler — knows the ring, knows the route. No investigation prereqs needed.
# Gate on simple triggers (enter_location, time_idle) with no prerequisites.
trigger: enter_location
# No prerequisites — smuggler's operational awareness is Background knowledge
tags: [arrival, operational]
# Detective — knows Commission jurisdiction, has manifest anomaly suspicion.
prerequisites:
facts:
- fact_id: investigation.manifest_discrepancy
min_confidence: suspects
```
---
## Appendix A: Character Starting Knowledge
What each character knows at game start. Source: `Background` (set at initialization). These facts do NOT need to be discovered — they are in the knowledge graph from tick 0.
### Smuggler starting facts
| Fact ID | Starting Confidence | Notes |
|---------|--------------------:|-------|
| `contraband.ring_exists` | `knows_details` | Is an active member |
| `knowledge.ring_routing_knowledge` | `knows_details` | Knows the full routing protocol |
| `contraband.ring_lattice_components` | `knows_details` | Knows what they're moving |
| `location.corridor_b7_drop_site` | `knows_details` | Knows the drop sites |
| `location.maintenance_corridors_access` | `knows_details` | Knows the layout |
| `relationship.ring_membership` | `knows_of` | Knows some ring members; full membership is `knows_details` |
| `world.lattice_regulation_active` | `knows_of` | Knows why there's demand |
| `world.unlicensed_lattice_demand` | `knows_of` | Understands the moral context |
| `location.corridor_b7_restricted` | `knows_of` | Knows the official restriction |
### Detective starting facts
| Fact ID | Starting Confidence | Notes |
|---------|--------------------:|-------|
| `investigation.manifest_discrepancy` | `suspects` | Has flagged anomalies, no confirmation yet |
| `contraband.ring_exists` | `suspects` | Has a case, not yet confirmed |
| `world.lattice_regulation_active` | `knows_details` | Full institutional knowledge |
| `world.commission_sova_jurisdiction` | `knows_details` | Knows their authority |
| `location.corridor_b7_restricted` | `knows_of` | Knows the restricted zone map |
| `investigation.surveillance_gaps` | `suspects` | Has noticed something is off |
---
## Appendix B: Fact ID Index
Alphabetical index for quick lookup. Format: `category.fact_name` → section.
| Fact ID | Section |
|---------|---------|
| `awareness.commission_audit_scheduled` | 4.2 |
| `awareness.detective_investigating_ring` | 4.2 |
| `awareness.detective_presence` | 4.2 |
| `awareness.inspection_sweep_active` | 4.2 |
| `awareness.ring_route_compromise` | 4.2 |
| `awareness.ring_tension_escalation` | 4.2 |
| `awareness.surveillance_change` | 4.2 |
| `behavioral.kael_behavioral_change` | 4.3 |
| `behavioral.kael_evasion` | 4.3 |
| `behavioral.sera_avoidance_pattern` | 4.3 |
| `behavioral.sera_knowledge_detail` | 4.3 |
| `behavioral.sera_kiosk_pattern` | 4.3 |
| `behavioral.sera_topic_deflection` | 4.3 |
| `behavioral.voss_awareness` | 4.3 |
| `contraband.drop_tonight` | 4.5 |
| `contraband.ring_exists` | 4.5 |
| `contraband.ring_lattice_components` | 4.5 |
| `contraband.ring_medical_components` | 4.5 |
| `contraband.severance_tech_presence` | 4.5 |
| `contraband.supply_chain` | 4.5 |
| `investigation.cargo_anomaly` | 4.1 |
| `investigation.container_delay` | 4.1 |
| `investigation.corridor_regular_use` | 4.1 |
| `investigation.drin_gambling_debt` | 4.1 |
| `investigation.drin_inspection_pattern` | 4.1 |
| `investigation.kael_corridor_meeting` | 4.1 |
| `investigation.kael_corridor_sighting` | 4.1 |
| `investigation.kael_exit_attempt` | 4.1 |
| `investigation.kael_ring_membership` | 4.1 |
| `investigation.kael_unknown_contact` | 4.1 |
| `investigation.manifest_discrepancy` | 4.1 |
| `investigation.naia_lattice_condition` | 4.1 |
| `investigation.oversight_gap_pattern` | 4.1 |
| `investigation.ring_existence` | 4.1 |
| `investigation.renn_courier_role` | 4.1 |
| `investigation.ring_leadership` | 4.1 |
| `investigation.sera_evidence_held` | 4.1 |
| `investigation.shift_mismatch` | 4.1 |
| `investigation.smuggling_route` | 4.1 |
| `investigation.surveillance_gaps` | 4.1 |
| `investigation.torek_spending_pattern` | 4.1 |
| `investigation.voss_ring_involvement` | 4.1 |
| `knowledge.bar_back_room_significance` | 4.4 |
| `knowledge.bar_ring_awareness` | 4.4 |
| `knowledge.kael_exit_plan` | 4.4 |
| `knowledge.ring_bar_operations` | 4.4 |
| `knowledge.ring_routing_knowledge` | 4.4 |
| `location.bay4_sealed` | 4.6 |
| `location.corridor_b7_drop_site` | 4.6 |
| `location.corridor_b7_restricted` | 4.6 |
| `location.maintenance_corridors_access` | 4.6 |
| `location.span_gate_schedule_gap` | 4.6 |
| `relationship.kael_naia_connection` | 4.7 |
| `relationship.ring_membership` | 4.7 |
| `relationship.trust_network` | 4.7 |
| `social.bar_regular_status` | 4.8 |
---
*Fact catalog current as of 2026-02-19. All fact IDs in existing content are documented here. New IDs added during Sprint 12 content authoring should be added to the relevant section and Appendix B.*
@@ -0,0 +1,239 @@
# Meridian Coverage Map — Sova Transit District
**Ticket:** #335
**Author:** Miri
**Status:** canonical
**Last updated:** 2026-02-19
Depends on: #320 (Sova Station Profile), #302 (Sova Texture Appendix)
Reference: D-036 (setting), D-037 (contraband), D-017 (perception modes), D-018 (sound model)
---
## Purpose
The Meridian is the Reach's information network — the infrastructure through which lattice-equipped citizens communicate, access data, and are monitored. Coverage is not uniform. In Sova Transit District, coverage quality directly affects:
- **The detective's investigative capability** — where they can run queries, log observations, access Commission records
- **The smuggler's operational security** — where they can communicate off-record, coordinate ring operations, and avoid Meridian-logged evidence trails
- **NPC behavior** — what people say, where they say it, and how they behave in zones they know are monitored vs. unmonitored
This document maps coverage zones across Sova Transit District for use by content authors, NPC routine designers, and investigation sequence planners.
---
## Coverage Classification
| Classification | Signal Quality | Monitoring | Notes |
|---|---|---|---|
| **Comprehensive** | Full strength, no gaps | Active Commission monitoring in all public spaces; transaction logging, movement tracking | Institutional-grade. Commission can pull records without special access. |
| **Standard** | Full strength | Station-administration monitoring; Commission pull requires formal request | Normal for commercial and residential spaces. |
| **Degraded** | Partial strength; intermittent gaps | Passive logging only; Commission monitoring not active | Older infrastructure. Data exists but is patchy. |
| **Dead Air** | Minimal or no signal | No logging | Hardware gaps, infrastructure age, or active Severance interference. |
---
## Zone-by-Zone Coverage Map
### Zone 1: Gate Terminal Approach Corridor
**Coverage: Comprehensive**
The newest, most institutionally maintained infrastructure in the district. Commission-grade Meridian repeaters line the corridor from the span gate terminal entrance through the primary freight inspection checkpoint.
**What this means:**
- Every person who passes through this corridor is Meridian-logged. Movement time, identity (from lattice signature), any communications sent or received.
- The detective has full access to these logs with Commission credentials — arrival and departure times for every shift are reconstructable.
- The ring does not conduct any ring business in this corridor. Workers in the ring walk through here the same as everyone else, communicating nothing.
- **For content:** The gate terminal approach is where both characters arrive into the district. Their first moments are on maximum Meridian coverage. This is by design — the Commission uses the approach as a natural data-collection point.
**Investigative use:** The detective can establish where people were at specific times via gate terminal logs. Cross-referencing gate passage times against freight transit events is an early-investigation technique.
---
### Zone 2: Span Gate Terminal (Freight Processing)
**Coverage: Standard**
The freight processing floor attached to the gate terminal — where containers are received, manifests verified, and cargo routed into the hub system.
**What this means:**
- Station-administration monitoring. Transaction records (which containers moved, which workers handled them, which scanner bays they passed through) are standard-logged.
- Commission access to these records requires a formal request — not passive surveillance, but available with authorization.
- This is where the detective's augmented lattice manifest analysis is most useful. The logs exist; reading them is a matter of access tier.
- **The ring's vulnerability:** All scanner routing is logged. Anomalies (a container routed to an unexpected bay, a bay that logged no scan despite receiving a container) are detectable if someone runs the analysis. The detective's investigation begins here.
**Dead spots:** Scanner bay 7 (see below) is a known hardware gap in this zone.
---
### Zone 3: Logistics Hub — Main Floor ("The Floor")
**Coverage: Standard**
The primary workspace of the freight logistics operation. The manifest terminals, container staging, supervisor stations.
**What this means:**
- Standard station-administration monitoring. Movement and communications logged.
- Workers know they're on Meridian on the floor. Social conversation is guarded — people default to professional register and avoid sensitive topics.
- **The tell system:** An NPC checking their lattice frequently on the floor is either nervous about something or actively monitoring something. The normal pattern is occasional checks; frequent checks are a behavioral deviation.
- The supervisor's station has an enhanced local Meridian connection — administrative access level. Voss can see floor activity data in real time.
---
### Zone 4: Administrative Offices (Hub)
**Coverage: Standard, access-restricted**
The supervisory and administrative spaces within the logistics hub. Voss's office. Filing areas. The shift scheduling terminal.
**What this means:**
- Standard coverage, but access is restricted. The physical spaces require authorization. The Meridian records from these spaces are classified at administrative level — accessible to the detective with Commission credentials and formal request.
- The shift scheduling terminal is particularly relevant: it contains the maintenance window schedule, scanner bay assignments, and override logs. The detective who gets access to this terminal has the investigative thread that leads to how the ring knows the maintenance calendar.
- **The ring's information asset:** Whoever provides the ring with advance knowledge of maintenance windows is either accessing this terminal directly or has a relationship with someone who does. The shift scheduler (Maret Korr) has this access. This is not an accident.
---
### Zone 5: Break Room / Locker Area
**Coverage: Degraded**
The social space within the logistics hub — break room, locker area, coffee dispenser. Ostensibly institutional space. In practice, the oldest section of the hub's interior, with original-era Meridian infrastructure that has never been upgraded.
**What this means:**
- Degraded coverage. Communications are possible but signal is weaker. Logging is passive and intermittent — the system logs what it can, which is less than in Zone 3.
- Workers know this. The break room is where conversations that wouldn't happen on the floor do happen. Not sensitive enough for ring business, but where social conversations are more candid.
- **The ring's use:** Brief confirmations ("tonight's on") happen here, in the coverage gap. Nothing detailed — but the gap provides enough margin for a short verbal exchange that leaves no Meridian record.
- **For the detective:** Surveillance in the break room is less productive than on the floor. The detective's analytical lattice can still log conversations they're physically present for — but they can't pull passive surveillance of this space retroactively.
**Far corner dead spot:** The northwest corner of the break room (near the secondary locker bank) is effectively dead air — a hardware gap behind the original wall layer that no repeater reaches. Brief whispered conversations in this corner are not logged. The ring is aware of this specific location. Workers who've been here long enough know it informally as "the quiet corner" without necessarily knowing why it's quiet.
---
### Zone 6: The Last Shift (Bar Interior)
**Coverage: Standard**
The bar maintains standard Meridian coverage — Lera's business license requires it, and the station administration confirmed it when she renewed her commercial permit three years ago.
**What this means:**
- The bar is monitored at standard level. The fact that this is a social space doesn't change the monitoring status.
- **The detective's intelligence gathering:** Conversations the detective is physically present for are logged to their own lattice (if they choose to record). They cannot pull passive Meridian surveillance of bar conversations — the logs are there, but Commission access to a commercial establishment's social monitoring requires elevated authorization and clear investigative cause.
- **The ring's awareness:** The ring doesn't conduct ring business in the bar under standard coverage. Social contact, yes. Operational coordination, no — unless at the corner booth or at specific times when the bar's layout creates natural screening.
**Corner booth exception:** The corner booth sits in a partial coverage shadow — a consequence of the bar's converted architecture (originally a maintenance staging area, the back corner was never built with standard repeater placement). Coverage in the corner booth is degraded rather than standard. Not dead air, but weaker. Conversations are logged with gaps. This is why the corner booth became the booth people use when they want relative privacy — not by deliberate design, but by the bar's structural coincidence.
---
### Zone 7: Transit Corridor (Hub to Bar District)
**Coverage: Standard**
The main pedestrian corridor connecting the logistics hub to the bar district. The chokepoint that everyone passes through when going from work to drinks.
**What this means:**
- Standard monitoring. Movement is logged. Anyone who regularly makes the hub-to-bar commute is in the station logs.
- **The detective's passive intelligence:** Over time, the detective can build a pattern map of who moves where, when, and with whom, using corridor logs. Anomalies — someone who usually takes this route at 1800 who took it at 2300 instead — become investigative flags.
- **The ring's awareness:** Corridor movement is unavoidable and expected. The ring doesn't try to hide their normal commute. What they avoid is coordinated movement — multiple ring members traveling together at atypical hours.
---
### Zone 8: Older District Sections (Between Hub and Residential Boundary)
**Coverage: Degraded**
The peripheral sections of the Transit District — areas that predate the current hub configuration, with original-era Meridian infrastructure. Retail spaces that have changed hands multiple times. Storage areas. The district's edges.
**What this means:**
- Degraded coverage. Monitoring is passive and incomplete. This zone is where the district's informal economy operates — not the ring specifically, but the ordinary gray-market commerce that exists in any working district. Off-book repairs, informal trade, personal transactions that people prefer not to log.
- The detective can access these zone's logs but should expect gaps. Pattern analysis across this zone is useful for identifying movement anomalies but not reliable for specific event reconstruction.
- **The ring's secondary use:** Some ring-adjacent meetings (with receiving contacts, with the coordinator's intermediaries) happen in the older sections. Nothing incriminating enough that it would matter if it were logged — the meetings look like social encounters. But the degraded coverage provides a margin of comfort.
---
### Zone 9: Maintenance Corridors (Standard)
**Coverage: Minimal — approaching dead air in specific sections**
The maintenance infrastructure between and behind the district's occupied spaces. Original prefab-era construction, never upgraded for Meridian infrastructure.
**What this means for the general corridor network:**
- The main maintenance corridors — the ones Station maintenance actually uses — have minimal Meridian. Enough for basic identification ping (so maintenance workers can be located in emergencies), but no logging, no monitoring.
- Communications in the maintenance corridors are effectively unmonitored under standard conditions.
- **The ring's primary operational space:** Operations, coordination, and product staging in the maintenance corridors leave no Meridian footprint. This is by far the most secure communication environment available in the Transit District without Severance tech.
---
### Zone 10: Corridor 7B ("The Gap") — Ring Operations Space
**Coverage: Dead Air (Severance-enhanced)**
The specific maintenance corridor that the ring uses for operations. Located between the old prefab shell and the interior partition of the logistics hub's eastern expansion — a space between walls that exists because the 40-year-old retrofit never fully closed the gap between the original structure and the new section.
**What this means:**
- Natural dead air from structural interference. The signal gap exists because metal wall layers block repeater reach.
- **Severance enhancement:** The ring has installed two passive Severance hardware units in Corridor 7B — one at each entrance point. These devices don't actively jam (active jamming would be detectable); they passively absorb and scatter the Meridian signal that does manage to reach the corridor. The result is that even if Commission infrastructure were upgraded nearby, the corridor would remain effectively dead air.
- **Total communications security:** No Meridian logging of any kind. Ring members can speak freely. The ring conducts its planning, financial settlements, and sensitive coordination exclusively in Corridor 7B.
- **The detective's challenge:** The dead air in Corridor 7B is itself detectable — the detective's augmented lattice can identify a coverage void. A void this deep in what should be a covered station is anomalous. But knowing there's a dead spot doesn't tell you what happens in it. The detective would need to be physically present in Corridor 7B during a ring meeting to observe what's said. Getting into Corridor 7B without being noticed requires either legitimate maintenance access or a reason to be in the maintenance sections.
**Access:** Corridor 7B has two entrances. Primary: a service hatch in the logistics hub break room (Zone 5). Secondary: an external maintenance access panel in the older district section (Zone 8). Both access points are not on any public-facing map. The detective discovers them through either observation (noticing the hatch in the break room, following someone into Zone 8 and watching where they go) or through a trust-gated disclosure from an NPC who knows.
---
## Coverage Overlay — Gameplay Implications
### For the Detective
| Investigative Action | Best Zone | Worst Zone |
|---|---|---|
| Retrospective movement tracking | Zone 1 (Gate Terminal) | Zone 9-10 (Maintenance) |
| Real-time observation logging | Zone 3 (Hub Floor) | Zone 10 (Corridor 7B) |
| Communication intercept | Zone 2 (Freight Processing) | Zones 9-10 |
| NPC behavioral tells visible via lattice | Any zone with standard coverage | Dead air zones |
| Identifying coverage anomalies | Anywhere — the void in Zone 10 is detectable from adjacent zones | N/A |
**Investigative progression:** The detective's investigation naturally moves from high-coverage zones (Gate Terminal, Freight Processing — early manifest analysis) through medium zones (Hub Floor observation, Bar intelligence) toward the low-coverage discovery (the maintenance corridor system, Corridor 7B). This is also a progression from "reading records" to "physical presence required."
### For the Smuggler
| Operational Need | Preferred Zone | Risk Zone |
|---|---|---|
| Routine ring communication | Zone 5 far corner, Zone 9 | Zone 1, Zone 3 |
| Operational planning and coordination | Zone 10 (Corridor 7B) | Any standard zone |
| Product staging | Zone 9 / Zone 10 during maintenance window | Zone 2 during active transit |
| Meeting receiving contacts | Zone 8 (older sections) | Zones 1-3 |
| Social cover (looking normal) | Zone 6 (bar) | N/A — social cover works anywhere |
### For NPC Behavior
NPCs who are ring-aware modulate their behavior by coverage zone. Content authors should treat this as an NPC behavioral layer:
- **In Zone 1-3 (comprehensive/standard):** Ring members use professional register. No ring-related conversation. Normal work behavior.
- **In Zone 5 (degraded):** Ring members may exchange brief confirmations. Non-ring NPCs speak more candidly about personal matters.
- **In Zone 6 (bar, standard):** Ring members conduct social cover. Operational conversation only at the corner booth (Zone 6a, degraded) and only in coded language.
- **In Zones 8-9 (degraded/minimal):** Ring members can speak more openly. Not about operations in specific, but about the situation generally.
- **In Zone 10 (dead air):** Ring members can say anything.
---
## The Severance Tech Presence
The ring's two passive Severance units in Corridor 7B are their highest-value operational asset and their most significant legal risk. Possessing Severance counter-surveillance tech is a Category 2 violation (more serious than the lattice component trafficking itself in Commission classification).
**Who knows about them:** The coordinator, Kael, and one other ring member who helped install them. Torek knows they're there but not the technical details. Most ring members know Corridor 7B is "clean" without knowing why.
**What the detective discovers:** The augmented lattice can detect the Severance units if close enough (within 3 meters of Corridor 7B). The detection trigger is an anomalous pattern in the coverage void — a natural dead air has a specific signal falloff; an artificial one has a different signature. The detective needs to be in the adjacent maintenance corridors (Zone 9) to get close enough. The detection event is an investigative milestone.
**What happens if found:** Finding the Severance units transforms the investigation. The ring is no longer "suspected smugglers" — they're "suspected smugglers with active counter-surveillance infrastructure." Commission authorization for enhanced investigation methods (physical search, broader access to freight logs, official presence in the district) becomes much easier to obtain.
---
## Authoring Notes
**Coverage as character perception:** The Meridian coverage map is an in-world fact, but the characters don't experience it as a map. The detective's augmented lattice shows coverage strength as an overlay — they can see zone boundaries. The smuggler knows the coverage landscape through accumulated experience, not through a map.
**Monologue triggers:** Zone transitions are monologue opportunities.
| Transition | Smuggler | Detective |
|---|---|---|
| Entering Zone 5 (break room) from Zone 3 (floor) | "Quieter in here. Finally." | "Signal drops here. Noted." |
| Entering Zone 9 (maintenance) | "This is where you can breathe." | "Coverage drops to near-nothing. Whoever uses these corridors knows that." |
| First detection of Zone 10 void | — | "That void is too sharp to be structural. Someone made it." |
| Passing through Zone 1 (gate corridor) | "Every time through here, they've got you on file." | "Approach logs are reliable. Who came through, when." |
**Dead air zones and the trust system:** Some trust-gated NPC disclosures only happen in dead air zones. An NPC who knows about Corridor 7B won't discuss it in Zone 3. They won't discuss it in Zone 5 unless they're at the far corner. They will discuss it in Zone 9 or Zone 10 — but you have to be there first, which requires either following them or knowing to look.
---
*The Meridian is the nervous system of the Settled Reach. Sova Transit District's nervous system has gaps — some structural, some created. Those gaps are where the game lives.*
*Miri — Sprint 12*
+273
View File
@@ -0,0 +1,273 @@
# Mirror Moment Design Document
**Ticket:** #329
**Owners:** Gestalt (framework + specs), Paula + Mellanie (line authoring)
**Source:** D-032 (separate monologue pools), D-034 (FRIEND pattern), D-039 (wow moments)
**Date:** 2026-02-19
---
## What Is a Mirror Moment
A mirror moment is a shared trigger that fires different monologue for each playable character.
Same event. Same tile position. Different observation. The smuggler and the detective see the same thing and think completely different thoughts. Neither perspective is wrong. Neither is complete.
**The dramatic irony is the point.** On first playthrough, you get one lens. On replay as the other character, you revisit the same moment and understand what the other character was thinking. The world you thought you understood had another interpretation the whole time.
---
## Mechanics
### How mirrors work (D-032)
Monologue pools are hard-partitioned by `character` tag. Same trigger fires two different pools — one for the smuggler, one for the detective. The engine selects from the appropriate pool based on which character is playing.
```yaml
# Same trigger, two pools — the mirror
- id: example_mirror_s_001
character: smuggler
trigger: observe_npc # Trigger condition
text: "Smuggler's interpretation of the event"
tags: [mirror, the-friends-routine]
- id: example_mirror_d_001
character: detective
trigger: observe_npc # Same trigger condition
text: "Detective's interpretation of the same event"
tags: [mirror, the-friends-routine]
```
### Mirror vs. standard monologue
Standard monologue: fires for any character, addresses the environment from their general perspective.
Mirror: intentionally paired — the contrast between the two lines IS the content.
**Not every environmental line needs a mirror.** Most monologue is character-specific without being a deliberate paired contrast. Mirrors are designed — each one has a specific dramatic irony that pays off on replay.
### Prerequisite considerations
Mirror moments can have prerequisites that differ by character. The smuggler sees Kael's routine as familiar. The detective sees the same routine as data. But both must be able to encounter the trigger — prerequisite design should not lock one character out of a mirror unless that's intentional.
---
## The Seven Core Mirrors
### Mirror 1: The Friend's Routine
**Trigger:** `observe_npc` — observe Kael Davan at the beginning of his shift (dock floor, Phase 1 timing)
**Dramatic irony:** The smuggler sees their most trusted colleague. The detective sees a dock worker with flagged manifests.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Warm recognition, ease, competence acknowledged | "The day's better when Kael's on shift" |
| Detective | Clinical observation, professional assessment | "Davan. He's the one on three of the flagged manifests" |
**Prereqs:**
- Smuggler: none — fires on game start
- Detective: `suspects: investigation.manifest_discrepancy` (has flagged the anomaly)
**Payload on replay:** The detective's cool assessment of "the one on three flagged manifests" lands differently when you've played the smuggler and know Kael is your most trusted friend.
---
### Mirror 2: The Empty Corridor
**Trigger:** `enter_location` — enter Corridor B-7 (maintenance corridor, restricted zone)
**Dramatic irony:** The smuggler knows this place. The detective is piecing it together.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Operational familiarity — this is a work space | "Quiet tonight. Route's clear. Move." |
| Detective | Investigative alertness — spatial reasoning about why this corridor | "This shouldn't connect to the main dock flow. Someone redesigned the access." |
**Prereqs:**
- Smuggler: `knows_details: knowledge.ring_routing_knowledge` (always true — background knowledge)
- Detective: none — first-time observation lines for arrival; `suspects: investigation.manifest_discrepancy` for more specific reads
**Payload on replay:** The detective's spatial analysis ("someone redesigned the access") is exactly what the smuggler takes for granted — someone DID redesign it, to create the drop route.
---
### Mirror 3: The Worried Partner
**Trigger:** `observe_npc` — observe Naia Tamm at The Last Shift (bar, first encounter)
**Dramatic irony:** The smuggler knows exactly why Naia looks worried — she's worried about Kael, and the smuggler knows why Kael is distracted. The detective sees a civilian who might be a useful connection.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Guilt-adjacent recognition — knows more than Naia does | "Naia. She's got that look — the one she gets when Kael's not telling her something." |
| Detective | Analytical warmth — potential social anchor, reads the worry | "Someone waiting for someone who's late. Or someone carrying something they can't put down." |
**Prereqs:**
- Smuggler: `knows: relationship.kael_naia_connection` (learns through background or Kael references)
- Detective: none — fires on first observation of Naia
**Payload on replay:** The detective's "someone carrying something they can't put down" is unknowingly accurate — Naia is carrying worry about Kael that she can't voice. The smuggler knew the source.
---
### Mirror 4: The Spending
**Trigger:** `observe_npc` — observe Torek Lintar at The Last Shift during a spending moment (rounds for the bar, visible display)
**Dramatic irony:** The smuggler sees an operational risk — someone advertising that they have more money than they should. The detective sees the investigation materializing.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Operational caution — this is how you get caught | "Torek's buying rounds again. Someone needs to say something. This is how you draw attention." |
| Detective | Investigative recognition — the pattern confirms | "There it is. Dock inspector, mid-pay-grade, buying rounds at a freight bar. Where does that come from?" |
**Prereqs:**
- Smuggler: none — fires on observation (smuggler intuits operational risk)
- Detective: `suspects: investigation.torek_spending_pattern` (has noticed the pattern beginning)
**Payload on replay:** The smuggler's exasperation ("this is how you get caught") is exactly what eventually happens — Torek's spending is how the detective builds the case.
---
### Mirror 5: Sera Leaving
**Trigger:** `witness_interaction` — observe Sera Venn abruptly leave the bar when Torek Lintar arrives (The Last Shift)
**Dramatic irony:** This is the core Sera contradiction trigger for the detective. For the smuggler, it's just a Commission woman who doesn't like Torek — plausible, not significant.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Mild observation, dismissive — not their circle | "Commission woman doesn't like Torek. Smart. He's loud." |
| Detective | Pattern recognition accumulating — third observation fires the arc | "Three times. Same exit, three different reasons. That's not personality — that's data." |
**Prereqs:**
- Smuggler: none — fires on any observation of the departure
- Detective (Phase 3): fires specifically on third observed departure — the contradiction completes
**Notes:** This mirror has asymmetric emotional weight by design. For the smuggler it's noise; for the detective it's the arc-completion moment. The contrast is the dramatic irony — on replay as the detective, you understand why the same moment that was nothing to the smuggler was everything to the detective.
---
### Mirror 6: The Hum
**Trigger:** `hear_sound` — hear the span gate harmonics (ambient sound in the Transit District, audible near the gate)
**Dramatic irony:** The smuggler hears the operational heartbeat — the gate timing is part of their work. The detective hears institutional infrastructure that someone is exploiting.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Operational familiarity — rhythmic, known, workable | "Gate cycle. Twenty-two minutes before the next transit window. Plenty of time." |
| Detective | Analytical listening — patterns reveal structure | "The gate schedule has a window. Someone's using it. You don't exploit something this consistent by accident." |
**Prereqs:**
- Smuggler: none — fires on any span gate sound observation
- Detective: `suspects: investigation.manifest_discrepancy` (has the anomaly, is building the picture)
**Payload on replay:** The detective's "you don't exploit something this consistent by accident" is confirmed by the smuggler's casual knowledge of the transit window timing.
---
### Mirror 7: The Commission Kiosk
**Trigger:** `observe_npc` — observe Sera Venn at the Commission calibration kiosk (Level 3, morning diagnostics routine)
**Dramatic irony:** The smuggler sees Commission presence and reads it as operational risk. The detective sees their closest ally doing their job — and then maybe something else.
| Character | Monologue Register | Key Emotional Note |
|-----------|-------------------|-------------------|
| Smuggler | Caution, professional distance — Commission means danger | "Commission tech running diagnostics. Stay normal. She's doing her job. Let her do it." |
| Detective | Fond recognition shifting to observation | "Sera, running morning rounds. Same time, same kiosk. She always is. ...Why does she stay so long?" |
**Prereqs:**
- Smuggler: none — fires on any Sera observation (context-adjusted by Phase)
- Detective: `knows: behavioral.sera_kiosk_pattern` fires the extended-time observation variant
**Payload on replay:** The smuggler's "let her do her job" — not knowing that Sera ISN'T just doing her job, she's compulsively rechecking compliance data to make sure she hasn't been caught — reframes the detective's late-game worry ("why does she stay so long?").
---
## Authoring Guidelines
### What makes a mirror effective
**1. Asymmetric depth.** One character has more context than the other. The better-informed read is more specific; the less-informed read is still true — just incomplete. Neither is wrong.
**2. The replay payoff is the point.** Write both lines imagining the player who knows both perspectives. The goal is the recognition moment: "Oh — that's what they were thinking."
**3. Same emotional register is wrong.** If both characters sound the same, it's not a mirror — it's two generic observations. The characters must feel differently about the same trigger.
**4. Don't editorialize.** Neither character says "little did I know..." or provides retrospective framing. They speak in the moment, from their knowledge state. The irony emerges from context, not from the line itself.
### Trigger selection
Mirror moments work best on triggers that are:
- **Routine** — something that recurs throughout the session, so both characters naturally encounter it
- **Spatially available to both** — the smuggler and detective both operate in the three social sites
- **Emotionally loaded differently** — the same object/person/sound has different resonance depending on your knowledge
Triggers to avoid for mirrors:
- One-time scripted events (both characters might not see them)
- Confrontation triggers (those are character-specific by design)
- Lines gated so heavily by prerequisites that one character rarely sees them
### Line count per mirror
Each core mirror: **2-4 lines per character** — variations on the same observation for different confidence states or visit counts (e.g., first visit, return visit).
Total for 7 core mirrors: approximately **28-56 lines** (14-28 per character), spread across trigger types.
### Paired notation for authors
When authoring mirror lines, use `dual_lens` authoring note to document the contrast:
```yaml
- id: mirror_7_s_001
character: smuggler
trigger: observe_npc
text: "Commission tech running diagnostics. Stay normal. She's doing her job. Let her do it."
tags: [mirror, the-commission-kiosk]
dual_lens:
smuggler: "Defensive distance — Commission means risk, avoid attention"
detective: "Same NPC as the detective's FRIEND — the contrast is that the smuggler dismisses exactly what the detective is learning to doubt"
notes: "Paired with mirror_7_d_001"
```
---
## Organic Mirrors (3-5 to discover during content review)
During Mellanie's content pack authoring (#190, #191, #192), additional mirror moments will emerge organically when the same environmental element naturally reads differently for each character. Authors should flag these with `[MIRROR CANDIDATE]` in comments.
**Likely candidates:**
- Voss giving shift assignments (the smuggler reads operational signaling; the detective reads management as cover)
- Kael at lunch (the smuggler reads normalcy; the detective reads behavioral data)
- Lera serving a regular order (the smuggler reads social comfort; the detective reads access patterns)
- The empty bay at shift transition (the smuggler reads operational window; the detective reads anomalous access pattern)
These become confirmed mirrors after both character packs are written — identify paired lines that emerged independently, then strengthen the contrast deliberately.
---
## Integration with Content Authoring
### For Mellanie (#190, #191, #192 content packs)
When authoring NPC-observation and environmental lines, flag any line that:
- Involves a character or location that is significant to both the smuggler and detective
- Would read differently depending on what the observing character knows
- Contains a detail that is loaded for one character but neutral for another
Flag format: add `[MIRROR CANDIDATE: {brief description of the contrast}]` as an authoring note.
### For Paula (#299, #300 opening hooks)
Opening hook content should include at least 2-3 mirror moments from the Core 7 above. The smuggler's arrival at the Terminal should establish The Friend's Routine (Mirror 1) and may include The Hum (Mirror 6). The detective's arrival should include Mirror 1 (from their clinical perspective) and Mirror 5 setup (Sera present in bar).
### Prerequisite cross-reference
All mirror moment prerequisite fact IDs are defined in: `docs/design/knowledge-flags-v01.md`
---
*Design document. Core 7 mirrors are confirmed and spec'd. Organic mirrors (3-5) to be identified during content authoring.*
*Line authoring: Mellanie + Paula. Framework: Gestalt.*
@@ -0,0 +1,251 @@
# THE FRIEND: Contradiction Arc Pattern
**Ticket:** #332 | **Priority:** Medium | **Sprint:** 12
**Status:** v0.1 Draft
**Owner:** Paula
**Blocked by:** #297 (Kael Davan profile — done), #298 (Sera Venn profile — done)
**Decisions referenced:** D-034 (THE FRIEND pattern), D-035 (tag taxonomy), D-039 (wow moment #3), D-041 (knowledge graph), D-062 (invisible locked options), D-063 (confrontation mechanics)
---
## 1. What This Document Is
A reusable design pattern document extracted from the two concrete THE FRIEND implementations: Kael Davan (smuggler's FRIEND) and Sera Venn (detective's FRIEND). Both are complete. Both worked. This document captures the structural logic they share so that future FRIEND NPCs can be authored consistently and quickly.
The pattern is called the **Contradiction Arc**. It is not a mystery. It is not a betrayal in the conventional sense. It is the discovery that someone the player trusts is carrying something they haven't shared — and that what they're carrying changes everything the player thought they understood about the relationship.
---
## 2. The Pattern: Definition
A Contradiction Arc is a 5-phase character trajectory built around a single observable contradiction that the player discovers through spatial observation, not through dialogue. The contradiction is:
- **Observable**: The player sees it happen. No NPC tells them. No dialogue flag. No notification.
- **Spatially staged**: The contradiction is a behavioral deviation from expected routine (wrong location, wrong time, wrong person, wrong pattern).
- **Morally complex**: THE FRIEND's concealment comes from care or fear, not malice. There is no clean resolution.
- **Dual-lens resonant**: The same contradiction means something different to each playable character. The second playthrough recontextualizes the first.
---
## 3. The 5 Phases
### Phase 1: Baseline Warmth (0-10 minutes)
**Purpose:** Establish the relationship BEFORE the contradiction. Create genuine attachment so the discovery has emotional weight.
**Mechanics:**
- THE FRIEND is warm, reliable, helpful. Their dialogue is at surface/real trust for their relationship tier.
- Monologue during this phase is fond, casual, present-tense. No hints of incoming trouble.
- THE FRIEND's routine is consistent with their stated role. No behavioral deviations.
- Their personal tell (the observable habit that will become load-bearing later) is established here — but reads as innocuous.
**Content authoring rule:** Write Phase 1 as if it is the whole story. The warmth must be real, not performed. If the player never discovers the contradiction, Phase 1 should feel like a complete, satisfying relationship.
**Tag notes:**
- Dialogue: `trust: surface` (greeting, small talk) + `trust: real` (personal warmth, shared history)
- Monologue: positive mood tags (content, fond, warm)
**Kael example:** "Kael's already at the dock. Good. The day's better when he's on shift."
**Sera example:** "Good to see Sera. She always was the reliable one. At least someone in this district speaks my language."
---
### Phase 2: First Behavioral Shift (10-15 minutes)
**Purpose:** Introduce behavioral tells that the observant player might notice — but that have plausible innocent explanations.
**Mechanics:**
- THE FRIEND's behavior changes in small ways. Shortened interactions. Increased action frequency (lattice checking, looking around). Redirected attention.
- These are observable but not alarming. A non-observant player may not register them at all.
- The monologue shifts to mild concern or puzzlement. "Not like them to be..." registers something without naming it.
- THE FRIEND's dialogue remains warm at the surface level. The behavioral tells exist beneath the dialogue.
**Content authoring rule:** Every tell in Phase 2 must have a non-suspicious explanation AND a suspicious one. The player who discovers the contradiction on a second playthrough should recognize these tells as load-bearing. The player experiencing them for the first time should accept the innocent explanation.
**Tag notes:**
- Dialogue: same trust tiers as Phase 1 — surface content unchanged
- Monologue: mild concern tone; `trigger: observe_anomaly` for behavioral tell moments
- Tell system: all Phase 2 tells fire with `visible_to: peripheral` or `visible_to: any`
**Kael example:** "Kael's distracted today. Checks his lattice more than usual. Probably nothing."
**Sera example:** "Sera left early. 'Headache,' she said. First time she's done that."
---
### Phase 3: The Contradiction (15-20 minutes)
**Purpose:** The observable contradiction. THE FRIEND is in the wrong place, doing the wrong thing, with the wrong person — and the player sees it.
**Mechanics:**
- Spatial staging: THE FRIEND deviates from their established routine in a way that is spatially visible to the player. This is NOT communicated through dialogue or UI. The player moves through the world and encounters the deviation.
- The contradiction must be observable without being forced. The player discovers it; the game does not direct them to it. However, the contradiction is staged in a location the player will naturally visit in the course of normal gameplay.
- The monologue fires on `trigger: observe_anomaly` or `trigger: witness_interaction`. It names what the player has seen without drawing a conclusion. The character asks the question; the player supplies the answer.
- **Entity color shifts** at this moment: `Friendly` (green) → `PersonOfInterest` (amber). The visual system registers what the player has just observed.
- **Dialogue tiers lock**: The discovery creates a relationship fracture. Insider/peer warmth content becomes unavailable. Only public/surface dialogue remains for this NPC going forward. The absence is the signal.
**Content authoring rule:** The contradiction must be:
1. Spatially clear (the player can see what is happening)
2. Contextually ambiguous (they understand WHAT but not WHY)
3. Emotionally weighted (the relationship context makes the "what" feel like a wound)
**Tag notes:**
- Monologue at discovery: `trigger: witness_interaction` or `trigger: observe_anomaly`, `mood: [shocked, analytical]`
- Monologue should name the observation, not the interpretation
- KG update: `contradiction_flagged: true`, relationship shifts to PersonOfInterest
**Kael example:** "That's Kael. But who's he talking to? I don't know that face. And that's not anyone from our rotation."
**Sera example:** "That's the third time. Sera leaves when Torek walks in. Three different excuses. Same result."
---
### Phase 4: Confrontation Opportunity (20-25 minutes)
**Purpose:** Give the player the option to press THE FRIEND. The confrontation dialogue reveals concealment without full confession.
**Mechanics:**
- A new dialogue option appears (D-062 — invisible until unlocked; players won't see it until the contradiction triggers it). The option is written in the character's internal voice (D-063).
- THE FRIEND deflects. They give an answer that is technically not a lie, but is not the truth. Their personal tell fires during the deflection — the tell established in Phase 1 becomes the proof that they're hiding something.
- The player can choose to accept the deflection, push harder, or walk away. All three paths have consequences.
- This is the emotional peak. Whatever the player does here determines the shape of the rest of the session.
**Content authoring rule:** THE FRIEND's deflection must be sympathetic. They are not lying to harm the player. They are concealing because the truth would hurt someone they love, or would place them in impossible danger. The player who understands the full arc (second playthrough) recognizes the deflection as fear, not treachery.
**Tag notes:**
- New dialogue option: confrontation style, `situation: [confrontation]`, `access: [peer, insider]` — only the character who has the relationship can ask this question
- THE FRIEND's response: `trust: real` (this is as honest as THE FRIEND can be without full confession), `situation: [confrontation]`, mood that indicates concealment
- Personal tell confirmation: observation monologue fired by THE FRIEND's specific tell behavior
**Kael example:** "Just a thing. Don't worry about it." [looks left before answering] / Smuggler monologue: "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."
**Sera example:** "He's just loud. I don't like loud drunks." / Detective monologue: "She's not lying. But she's not telling me everything. Sera, what do you know?"
---
### Phase 5: Contaminated Trust (25-30 minutes)
**Purpose:** THE FRIEND returns to surface-normal behavior. But the player is now watching differently. Every warm interaction is filtered through doubt.
**Mechanics:**
- THE FRIEND's surface behavior normalizes. They resume their routine, greet the player warmly, act as if Phase 3 and 4 didn't happen (or at least, as if they're trying to move past it).
- But dialogue has shifted: insider/peer warmth is gone, replaced by public/surface content only. The player hears the same NPC; the texture is different. The warmth is slightly performed.
- Monologue shifts into the contaminated register: every warm interaction generates a note of doubt. The player's observational frame has changed.
- Three player paths diverge from here:
- **Understanding** — gentle follow-up, possible confession, partial truth
- **Betrayal** — report/expose THE FRIEND; relationship damage is permanent
- **Observation** — watch and wait; the doubt lives but no confrontation happens
**Content authoring rule:** Phase 5 content must work for all three player paths. The NPC's surface behavior is path-neutral — they don't know what the player has decided. The player's monologue shifts based on what decision they've made (or chosen not to make yet).
**Tag notes:**
- Dialogue: `trust: surface` only; warm tone but no real-tier content; `mood: [composed]` for THE FRIEND
- Monologue: `mood: [conflicted]` or `mood: [analytical]`; prerequisite gates on relationship state PersonOfInterest
- If player chose Understanding path: additional monologue lines for partial-confession scenario
**Kael example:** "Same old Kael. Same jokes, same routine. ...What are you not telling me?"
**Sera example:** "Still the same Sera. Still reliable. ...But reliable about what?"
---
## 4. Required Content Per FRIEND NPC
| Content Type | Count | Notes |
|-------------|-------|-------|
| Phase 1 dialogue lines (warmth, establishment) | 8-12 | Across greeting, social, situation contexts |
| Phase 1-2 monologue (fond, mild concern) | 5-8 | `trigger: observe_npc`, `trigger: post_conversation` |
| Phase 2 behavioral tells (description + monologue) | 3+ | Each tell: visible_to, behavior, monologue line |
| Phase 3 contradiction discovery monologue | 3-5 | `trigger: witness_interaction`, observation language only |
| Phase 4 confrontation dialogue option (player choice) | 1 | In character's internal voice per D-063 |
| Phase 4 THE FRIEND deflection dialogue | 3-5 | With personal tell behavior flagged |
| Phase 4 tell confirmation monologue | 1-2 | "I know this tell. They're lying." |
| Phase 5 contaminated-trust monologue | 4-6 | Varies by player path taken |
| **Total authored** | **~70-100 items** | No generation expansion — all hand-authored |
---
## 5. The Tell System
Every FRIEND NPC has a **personal tell** — a specific behavioral marker established in Phase 1 (innocent, recognizable) and confirmed in Phase 4 (proof of concealment). The tell must:
1. Be **physical and observable** (not internal state — visible behavior the player can see)
2. Be **introduced before it becomes load-bearing** — Phase 1 or early Phase 2
3. Be **unique to this NPC** — not a generic "nervousness" tell, but a specific habit
4. Be **explainable differently before and after** — innocent early, incriminating late
**Kael's tell:** Looks left before answering. Established as a habit the smuggler has observed for two years. Becomes proof of lying at Phase 4 because the smuggler knows the tell AND its meaning.
**Sera's tell:** Gathers her things quickly when leaving. Not a lie tell — Sera isn't lying when she leaves. But the timing (always when Torek arrives) and the variety of excuses (genuine, not scripted) creates the pattern the detective reads as behavioral.
**Tell design rule:** The tell should produce a different emotional register for each playable character observing it:
- The character with the relationship reads the tell as **personal** ("I know that tell — I've seen it before")
- The character without the relationship reads the tell as **behavioral pattern** ("That's twice now — flag it")
---
## 6. Dual-Lens Design Requirements
Every FRIEND NPC must produce different monologue experiences for both characters observing the same behavior. The contradiction arc has two distinct "readings":
| Phase | Bonded Character Reading | Other Character Reading |
|-------|--------------------------|------------------------|
| Phase 1 | Warmth, familiarity, fond reliance | Clinical observation, behavioral baseline, possible suspicion |
| Phase 2 | Personal concern ("something's wrong with them") | Analytical flag ("deviation from baseline, note it") |
| Phase 3 | Betrayal, confusion, hurt | Evidence accumulation, pattern confirmation, institutional assessment |
| Phase 4 | Personal confrontation (uses the personal tell as confirmation) | Investigative confrontation (uses behavioral pattern as evidence) |
| Phase 5 | Contaminated intimacy (every warm moment now suspect) | Professional caution (reliable informant now uncertain quantity) |
**The second-playthrough resonance:** The player who has experienced both sides of Triangle 4 understands:
- Kael is trying to get OUT of the ring to protect Naia. His exit attempt is an act of love, not treachery.
- Sera is protecting Naia by NOT reporting what she found. Her concealment is loyalty, not corruption.
- The smuggler and detective are both experiencing friendship fractured by the same cause — Naia's worry about Kael, and the chain of protection that worry created.
This is the design. The contradiction is not "THE FRIEND is bad." The contradiction is "THE FRIEND is doing something complicated for reasons that make sense, and it's happening to YOU."
---
## 7. The No-Clean-Resolution Rule
Every Contradiction Arc must refuse to offer a clean resolution. This is non-negotiable.
**What "no clean resolution" means:**
- Confronting THE FRIEND does not produce a confession that absolves them. It produces a partial truth that raises more questions.
- Protecting THE FRIEND does not produce safety. It produces continued risk with the added weight of knowledge.
- Reporting THE FRIEND does not produce justice. It produces a particular kind of harm to people who didn't deserve to be caught in the consequence.
- Ignoring THE FRIEND does not produce peace. The doubt persists. The knowledge lives.
**Why this rule exists:** D-027 success criterion #3 is "after each playthrough, player names an NPC they felt conflicted about." Confliction requires that no choice was clearly right. The moment a clean resolution exists — confess = forgiven, report = solved, ignore = safe — the Contradiction Arc collapses into a problem with a correct answer. The player stops feeling conflicted and starts making optimal choices. Emotional weight requires moral weight. Moral weight requires cost on every path.
---
## 8. Content Authoring Checklist
Before shipping a FRIEND NPC with a Contradiction Arc, validate:
- [ ] Phase 1 warmth is genuine — the relationship is established as real before it's threatened
- [ ] Phase 2 tells are pre-established in Phase 1 (not appearing for first time at contradiction)
- [ ] Phase 3 contradiction is observable through spatial play — no dialogue/UI flag triggers it
- [ ] Phase 3 monologue names the observation without naming the explanation
- [ ] Phase 3 entity color shifts to amber (PersonOfInterest) at contradiction discovery
- [ ] Phase 3 dialogue tier lock: insider/peer content unavailable post-contradiction
- [ ] Phase 4 confrontation option appears only post-Phase 3 (D-062 invisible until unlocked)
- [ ] Phase 4 confrontation written in character's internal voice (D-063)
- [ ] Phase 4 THE FRIEND's deflection is sympathetic — they're concealing, not lying maliciously
- [ ] Phase 4 personal tell fires during deflection and is confirmed in monologue
- [ ] Phase 5 content exists for all three player paths (Understanding / Betrayal / Observation)
- [ ] Dual-lens notes written for both characters on every key monologue trigger
- [ ] No clean resolution on any player path
- [ ] Total authored content: 70-100 items
---
## 9. Application to Future FRIEND NPCs
This pattern is designed to be reused. When designing a future FRIEND NPC for a new template:
1. **Identify the concealment.** What is THE FRIEND hiding, and WHY? The why must be sympathetic. "Protecting someone they love" or "fear of consequences" — not "because they're secretly bad."
2. **Design the contradiction.** What does the player OBSERVE that breaks the baseline? It must be spatial and behavioral, not told.
3. **Design the tell.** What physical habit is introduced in Phase 1 that becomes load-bearing in Phase 4? It must be specific to this NPC.
4. **Map the dual-lens difference.** How does the bonded character read the contradiction differently from the observing character? The emotional register must diverge.
5. **Refuse the clean resolution.** What does each of the three player paths cost? If any path costs nothing, revise.
---
*Ticket #332 — Contradiction arc design document. Reusable FRIEND pattern. v0.1 scope.*
+280
View File
@@ -0,0 +1,280 @@
# Mirror Moment Design Document
**Ticket:** #329 | **Priority:** Medium | **Sprint:** 12
**Status:** v0.1 Draft
**Owners:** Paula (design), Mellanie (authoring)
**Blocked by:** #297 (Kael profile — done), #298 (Sera profile — done)
**Decisions referenced:** D-032 (separate monologue pools), D-034 (THE FRIEND), D-039 (wow moment #4 — Divergence Reveal)
---
## 1. What Is a Mirror Moment?
A mirror moment is a **shared trigger that produces completely different monologue for each playable character.** Same event. Same NPC. Same location. Two entirely different emotional experiences.
The player who has completed both playthroughs encounters the mirror moments as a recognition: *"I was in this exact situation, but I felt something completely different."* This is the mechanism of Wow Moment #4 (Divergence Reveal — D-039): the realization that the same world contains two different experiences, and that you've had both.
**What a mirror moment is NOT:**
- A branching dialogue option
- A triggered cutscene
- An informational difference ("the smuggler knows X and the detective doesn't")
A mirror moment is an **interpretive difference** — same information, same NPC behavior, same physical event. Different internal frame. Different emotional weight.
---
## 2. Design Principles
**Principle 1: Same trigger, different pool (D-032 hard partition)**
Mirror moments operate because monologue pools are hard-partitioned by character. The same trigger key fires into the smuggler pool OR the detective pool — never both. The authoring requirement is to write both sides of every shared trigger.
**Principle 2: The difference must be felt, not explained**
A mirror moment works when the player FEELS the divergence without having it explained. The two monologue lines should be immediately recognizable as coming from different places — different relationships, different frames, different fears.
**Principle 3: The mirror reveals what the character CARES about**
The smuggler's monologue reveals personal loyalty (people). The detective's monologue reveals investigative analysis (patterns). A mirror moment that doesn't surface this difference is a content gap, not a mirror.
**Principle 4: At least some mirrors should work on first playthrough**
Not every mirror moment requires a second playthrough to land. Some mirrors work as a moment of character voice — the player just hears what their character notices. Second playthrough makes the divergence explicit, but first playthrough should have its own emotional coherence.
---
## 3. The 10 Mirror Moments (v0.1)
### Mirror 1: The Friend's Routine
**Trigger:** `observe_npc` — Seeing THE FRIEND (Kael/Sera) going about their daily routine in their expected location.
**Shared observation:** THE FRIEND is doing exactly what they always do. Nothing unusual. The baseline before anything cracks.
**Smuggler monologue:**
*"Kael's at his station. Right where he always is. Something settles when the shift starts right."*
**Detective monologue:**
*"The dock worker — Davan — is on the floor again. Same position, same timing. Three days in a row. That's a routine. File it."*
**What the mirror reveals:** The smuggler sees a person. The detective sees a data point. Same NPC at the same station. The relationship determines the register.
---
### Mirror 2: The Empty Corridor
**Trigger:** `enter_location` — Player enters a maintenance corridor when no other NPCs are present.
**Shared observation:** The corridor is empty. The maintenance systems hum. No one is watching.
**Smuggler monologue:**
*"Clear. Good. Transition window's still open for eleven more minutes."*
**Detective monologue:**
*"Three separate entries to this corridor in my district walkthrough. Nothing documented in my file about why workers access this route. Off-books path? Add it to the log."*
**What the mirror reveals:** The smuggler reads the empty corridor as operational safety. The detective reads it as an investigative lead. The physical space is identical; the cognitive frame inverts it.
---
### Mirror 3: The Worried Partner (Naia Tamm)
**Trigger:** `observe_npc` — Seeing Naia Tamm sitting alone at the bar, visibly preoccupied.
**Shared observation:** Naia is at The Last Shift without Kael. She looks tired or worried. Not in distress — just carrying something heavy.
**Smuggler monologue:**
*"Naia's here without Kael again. She looks rough. Should check in. ...Not now. Later."*
**Detective monologue:**
*"That woman — Tamm, I think Sera mentioned her — is sitting alone. Wrong energy for a social evening. Something at home, or something she's heard?"*
**What the mirror reveals:** The smuggler has a protective impulse toward Naia but defers it. The detective categorizes Naia as a potential information source. Same worried woman, same empty chair across from her. One character plans to check on a person; the other plans to assess a witness.
---
### Mirror 4: The Spending
**Trigger:** `observe_npc` — Watching Torek Lintar buy rounds at the bar.
**Shared observation:** Torek is buying drinks for several people he doesn't know well. The drinks are nicer than they should be on a compliance officer's salary. He's clearly enjoying himself.
**Smuggler monologue:**
*"Torek's buying rounds again. That's the third time this week. Either he won something, or he's getting paid from somewhere he shouldn't be. Starting to worry about that."*
**Detective monologue:**
*"Commission compliance officer, buying grain spirit for half the bar. On his salary? That doesn't close. Financial tell. Noted."*
**What the mirror reveals:** Both characters notice the same behavioral anomaly. The smuggler frames it as a security risk to the ring. The detective frames it as an evidentiary lead. Both have the same concern (Torek's spending is unusual) — but the concern has completely different stakes.
---
### Mirror 5: Sera Leaving
**Trigger:** `observe_npc` — Watching Sera Venn gather her things and leave the bar when Torek arrives.
**Shared observation:** Sera gives a brief excuse (headache, kiosk check, early shift) and leaves. She was comfortable until the moment Torek walked in.
**Smuggler monologue:**
*"Sera left. Good timing — she was starting to make everyone nervous anyway. Commission presence at Lera's always shifts the temperature."*
**Detective monologue:**
*"Second time now. Sera leaves when Torek walks in. Same type of excuse. 'Headache' last time, 'kiosk' tonight. Same result. That's two data points. Third is a pattern."*
**What the mirror reveals:** The smuggler reads Sera's departure as relief (Commission leaving = safer conversation). The detective reads the same departure as a behavioral anomaly. The event is identical. The emotional response is inverted.
This mirror is structurally important: it shows the detective's FRIEND arc developing through observational patience, while simultaneously showing the smuggler's categorical relationship with Commission presence.
---
### Mirror 6: The Station Hum
**Trigger:** `time_idle` — Player is stationary in the terminal for more than a few seconds with no active interaction.
**Shared observation:** The station's ambient systems — span gate hum, ventilation cycle, cargo machinery in the background — fill the silence. Sova at rest.
**Smuggler monologue:**
*"Span gate's cycling early. One hour to the next transit. Voss'll be running the schedule check in twenty."*
**Detective monologue:**
*"They've been on this station their whole lives. That hum is just background to them. I'm still trying to tune it out."*
**What the mirror reveals:** The smuggler hears operational data in the ambient sound. The detective hears alienation. The station hum carries the same frequency to both characters' ears. It means completely different things to their experience of being in this place.
---
### Mirror 7: The Commission Kiosk
**Trigger:** `enter_location` or `observe_npc` — The player walks past or observes the Commission compliance kiosk near the terminal entrance.
**Shared observation:** The kiosk is a small Commission compliance desk near the terminal entrance — mostly unmanned, used for permit verification and compliance queries. Standard infrastructure.
**Smuggler monologue:**
*"Commission kiosk. Nobody uses it. Institutional furniture. Long as nobody decides to run a full cargo audit, that's exactly what I want it to stay."*
**Detective monologue:**
*"The compliance kiosk has public access to manifest verification records. Local. Less filtered than the request I made through channels. Check it when Maret's not watching."*
**What the mirror reveals:** The smuggler reads the kiosk as a passive threat to be ignored. The detective reads it as an investigative tool. Same object. Same physical location. The smuggler hopes it stays irrelevant; the detective plans to use it.
---
### Mirror 8: Kael and Naia Together (Bar)
**Trigger:** `observe_npc` — Seeing Kael and Naia sitting together at their usual table. Naia is talking; Kael is listening but seems distracted.
**Shared observation:** A couple having dinner. Naia is clearly talking seriously about something. Kael is present but not quite there. The gap between them is subtle but visible.
**Smuggler monologue:**
*"Kael and Naia look... off. Naia's talking a lot. Kael's nodding but he's somewhere else. Something's going on at home. He doesn't say."*
**Detective monologue:**
*"Couple — the dock worker, Davan, and the woman who sits with Sera sometimes. She's animated; he's distracted. Relationship tension. That level of checked-out usually means someone's keeping something."*
**What the mirror reveals:** The smuggler reads this as a friend's private trouble that deserves respect and distance. The detective reads it as behavioral evidence — someone keeping something from a partner. Same visible tension. One character looks away; the other leans in.
---
### Mirror 9: The Manifest Terminal
**Trigger:** `observe_npc` or `enter_location` — The freight manifest terminal in the terminal hub.
**Shared observation:** A freight terminal kiosk showing manifest data. Busy during shifts; the data moves constantly.
**Smuggler monologue:**
*"That's Maret's terminal. She's got access to all the schedules from here. If anything's getting flagged in the manifest data, she'd see it before anyone else. Maret's solid. She won't say anything."*
**Detective monologue:**
*"Freight manifests for the last six months, loaded from here. The cluster analysis shows three anomaly windows, all at shift transition. That's not clerical error. Someone's using this terminal to adjust records after the fact."*
**What the mirror reveals:** The smuggler sees a trusted colleague's workspace and trusts that colleague's loyalty. The detective sees the exact location where evidence of the ring's operations lives. The terminal is simultaneously proof of criminal activity and proof of a trusted relationship — depending on which character is reading it.
---
### Mirror 10: The End of Shift
**Trigger:** `observe_npc` — Watching the shift change happen across the terminal floor — workers clocking out, new crew arriving, a brief period of cross-traffic and handoffs.
**Shared observation:** Dozens of workers moving at once. The shift change is organized chaos. Cargo gets routed, schedules are handed off, the floor changes.
**Smuggler monologue:**
*"Shift transition. Twenty-minute window. The floor's all cross-traffic now — nobody's tracking individual movement. This is when it runs."*
**Detective monologue:**
*"Shift change. Every anomaly in my manifest data clusters around this. The window. The handoff moment. Someone is using the cross-traffic as cover. But for WHAT?"*
**What the mirror reveals:** The smuggler knows exactly what the shift change covers. The detective sees the pattern but not the specific. Both characters observe the same 20-minute window of organized chaos. One is running something through it; the other is trying to figure out what's being run.
---
## 4. Mirror Moment Authoring Rules
### Rule 1: Both sides are mandatory
A mirror moment is only complete when both the smuggler monologue and the detective monologue are authored. Omitting one side is a content gap.
### Rule 2: Same trigger key, different pool
Both lines use the same `trigger` type and similar `prerequisite` conditions. They appear in separate character monologue files (D-032 hard partition). The only difference is the `character` tag.
### Rule 3: No shared content
Neither monologue line should directly reference the other or acknowledge the other character's frame. Each line is internally coherent to its character. The divergence is created by the CHARACTER's relationship to the world, not by explicit contrast.
### Rule 4: The lines must stand alone
A player who only plays one character should not feel the mirror moment is incomplete. Both lines should work as independent character voice — good monologue that stands on its own.
### Rule 5: Tag all mirrors for tracking
Add `tags: [mirror_moment]` to both sides of every mirror pair so they can be identified and validated as a set.
---
## 5. YAML Example
```yaml
# Smuggler side — monologue-smuggler.yaml
- id: transit_m_s_044
character: smuggler
trigger: observe_npc
prerequisite:
location: the-terminal
target: npc:naia-tamm
relationship_state: Known
text: "Naia's here without Kael again. She looks rough. Should check in. ...Not now. Later."
mood: [concerned, fond]
topic: [colleague]
tags: [mirror_moment, mirror_worried_partner]
# Detective side — monologue-detective.yaml
- id: transit_m_d_044
character: detective
trigger: observe_npc
prerequisite:
location: the-last-shift
target: npc:naia-tamm
text: "That woman — Tamm, I think Sera mentioned her — is sitting alone. Wrong energy for a social evening. Something at home, or something she's heard?"
mood: [analytical, curious]
topic: [investigation]
tags: [mirror_moment, mirror_worried_partner]
```
---
## 6. Validation Checklist
For each mirror pair:
- [ ] Both sides authored
- [ ] Same trigger type used
- [ ] Both sides use `tags: [mirror_moment, {mirror_name}]`
- [ ] Lines stand independently as character voice
- [ ] Emotional register diverges (personal vs. analytical, OR equivalent meaningful contrast)
- [ ] No shared vocabulary that implies the characters are comparing notes
- [ ] Prerequisites match (same NPC, same location condition, equivalent state requirements)
---
## 7. Expansion Notes (Post-v0.1)
Additional mirror moments to author in future content passes:
- **The Missing Person Notice:** A posted notice near the bar about a missing dock worker (pre-existing, unrelated to main plot). Smuggler: "People disappear. Station life." Detective: "Filed under 'before my posting.' What was the investigative status?"
- **Voss Walking the Floor:** The shift supervisor doing a routine floor inspection. Smuggler sees cover being maintained. Detective sees a potential informant.
- **Drin's Inspection:** Dock inspector doing a container verification. Smuggler knows Drin will find nothing. Detective sees the inspection as either genuine compliance or evidence of a compromised process.
---
*Ticket #329 — Mirror moment design document. 10 paired observations. v0.1 scope.*
+490
View File
@@ -0,0 +1,490 @@
# Opening Hook Content Per Character
**Ticket:** #260 | **Priority:** High | **Sprint:** 12
**Status:** v0.1 Draft
**Owner:** Paula
**Blocked by:** #259 (First 5 minutes experience design — done)
**Source:** docs/design/first-5-minutes-experience.md (systems + narrative design)
**Decisions referenced:** D-032, D-035, D-039 (wow moment #1), D-053, D-064
---
## Overview
This document specifies the authored content for each character's opening hook — the first 3-4 minutes of play, before any investigation or conspiracy elements appear. Content is organized by seed variant (A-D per character) and includes authored monologue lines, NPC dialogue cues, and diegetic objective setup.
Authoring note: this document provides design spec and reference lines. Final authored YAML files go in the appropriate monologue pools per D-032 and D-035.
---
## 1. Smuggler Opening Variants
### Common Elements (All Variants)
All smuggler openings share:
- **Location:** The Terminal (Logistics Hub), dock floor or entrance
- **Ambient:** Station hum playing from frame 0; cargo machinery active in background
- **FRIEND:** Kael Davan is the first significant NPC — established within 90 seconds regardless of variant
- **Voice register:** Terse, sensory, first-name, operational-aware. No complete sentences unless they end in an operational note.
### Opening Line Pool (enter_location, Terminal, no prerequisites)
These lines fire when the smuggler enters The Terminal at session start. 3-4 lines available; engine selects one based on seed variant.
```yaml
- id: transit_m_s_open_001
character: smuggler
trigger: enter_location
location: the-terminal
text: "Morning shift. Recycled air and cargo lubricant. Home sweet home."
mood: [content, routine]
tags: [opening_hook, canonical_anchor]
- id: transit_m_s_open_002
character: smuggler
trigger: enter_location
location: the-terminal
text: "Span gate's been cycling all night. Can hear it in the walls."
mood: [routine, aware]
tags: [opening_hook]
- id: transit_m_s_open_003
character: smuggler
trigger: enter_location
location: the-terminal
text: "Floor smells like a ship's hold. Someone ran a heavy cargo last night. Not ours."
mood: [routine, observant]
tags: [opening_hook]
- id: transit_m_s_open_004
character: smuggler
trigger: enter_location
location: the-terminal
text: "Early shift crew's still clearing out. Good — gives us some runway before handoff."
mood: [operational, content]
tags: [opening_hook, variant_c_d]
```
### Kael Sighting Lines (observe_npc, Kael, Phase 1 relationship)
These fire when the smuggler sees Kael on the dock floor. Critical FRIEND establishment lines.
```yaml
- id: transit_m_s_open_010
character: smuggler
trigger: observe_npc
target: npc:kael-davan
prerequisites:
relationship:
target: npc:kael-davan
state: friendly
text: "Kael's already at the dock. Good. The day's better when he's on shift."
mood: [content, fond]
tags: [opening_hook, friend_phase_1, canonical_anchor]
- id: transit_m_s_open_011
character: smuggler
trigger: observe_npc
target: npc:kael-davan
prerequisites:
relationship:
target: npc:kael-davan
state: friendly
text: "There he is. Same spot, every morning for two years. Some things you can count on."
mood: [fond, content]
tags: [opening_hook, friend_phase_1]
```
### Variant A: Quiet Morning
**Initial condition:** No deviations. Standard operational day. The run is scheduled for the transition window.
**Opening state:** Kael is at his station. Voss is present but not focused on the dock floor. Everything is calm.
**Operational context line (observe_anomaly — schedule board):**
```yaml
- id: transit_m_s_open_020
character: smuggler
trigger: observe_anomaly
location: the-terminal
text: "Transition window opens in fifty minutes. Standard run — nothing complicated."
mood: [operational, content]
tags: [opening_hook, variant_a]
```
**FRIEND Phase 1 dialogue cue (Kael, on approach):**
Kael greets the smuggler warmly. Asks about the weekend. Makes a dry comment about Voss's new clipboard system. Everything is normal.
*Authored dialogue line for Kael (for content pack):*
> "There you are. Voss has a new form now — cargo intake manifest version four. Apparently version three had a column in the wrong place. The man has opinions about spreadsheets."
> `access: [peer, insider], trust: surface, situation: [greeting, shift_start]`
---
### Variant B: Kael Is Late
**Initial condition:** Kael has not arrived at his expected time. His station is empty. The smuggler starts without their anchor.
**Opening state:** Dock floor has other workers but Kael's absence is visible. A small operational tension — where is he? — without any real alarm.
**Absence lines (observe_npc, failed — Kael's station empty):**
```yaml
- id: transit_m_s_open_030
character: smuggler
trigger: observe_anomaly
location: the-terminal
text: "Kael's station is empty. He's always here before me. Maybe I was early."
mood: [routine, mildly_concerned]
tags: [opening_hook, variant_b]
notes: "Phase 1 tell: Kael's routine is so established that absence is noted before alarm."
- id: transit_m_s_open_031
character: smuggler
trigger: time_idle
location: the-terminal
text: "Still no Kael. Probably caught something. He'd have sent a message if it were the run."
mood: [mildly_concerned, operational]
tags: [opening_hook, variant_b]
notes: "Fires if player is near Kael's empty station for >60 seconds."
```
**Kael arrival (late, within 5 minutes of session start):**
Kael arrives slightly disheveled, slightly out of breath. Brief explanation — nothing alarming, just late. But the explanation is slightly vague. This is the first hairline crack from the very start: Kael's lateness is the beginning of Phase 2 tells.
*Authored dialogue line for Kael (late arrival):*
> "Yeah, yeah, I know. Had a thing. Don't give Voss a reason to look at the timestamp."
> `access: [peer, insider], trust: surface, situation: [greeting]`
> *Notes: "a thing" is characteristically vague but reads as personal, not operational.*
---
### Variant C: Heat from Voss
**Initial condition:** Voss is monitoring the dock floor more closely than usual. Something has him on edge — Commission liaison spotted yesterday, or a scheduling discrepancy from last week that hasn't been explained.
**Opening state:** Kael is present but body language is professional-not-warm. Voss is visible and attentive. The usual operational openness is compressed.
**Environmental read lines:**
```yaml
- id: transit_m_s_open_040
character: smuggler
trigger: observe_npc
target: npc:voss
text: "Voss is on the floor. Not his usual post. Something's up."
mood: [aware, cautious]
tags: [opening_hook, variant_c]
- id: transit_m_s_open_041
character: smuggler
trigger: observe_npc
target: npc:voss
text: "He's watching the floor. That's the third time he's passed that section. Something put him on edge."
mood: [operational, cautious]
tags: [opening_hook, variant_c]
notes: "Fires after player has been on the dock floor for 2+ minutes."
```
**Kael adjustment (operational caution):**
*Authored dialogue line for Kael (in variant C):*
> "Normal morning. Don't give him a reason to notice you." [Kael says this quietly, without looking directly at the smuggler]
> `access: [insider], trust: real, situation: [shift_start]`
---
### Variant D: Container Held
**Initial condition:** Operational container 4471 has been in temp storage longer than routing protocol specifies. Not a crisis — a logistical anomaly that the smuggler needs to address.
**Opening state:** Everything looks normal, but the first interaction with the freight system surfaces a held container.
**Container flag lines:**
```yaml
- id: transit_m_s_open_050
character: smuggler
trigger: observe_anomaly
location: the-terminal
text: "4471 is still in temp. That was supposed to move yesterday afternoon."
mood: [operational, concerned]
tags: [opening_hook, variant_d]
- id: transit_m_s_open_051
character: smuggler
trigger: observe_anomaly
location: the-terminal
text: "Scheduling held it. Voss's call. There's a reason, I just don't know it yet."
mood: [operational, analytical]
tags: [opening_hook, variant_d]
notes: "Fires on approach to freight terminal display. Teaches the player that containers are trackable."
```
**Kael conversation (operational problem-solving):**
*Authored dialogue line for Kael (variant D):*
> "I saw. Voss pulled it from the schedule. He'll explain or he won't. Just don't touch it until he does."
> `access: [insider], trust: real, situation: [shift_start, investigation]`
---
## 2. Detective Opening Variants
### Common Elements (All Variants)
All detective openings share:
- **Starting location:** Varies by variant (see below)
- **Ambient:** Station hum playing; institutional feel — cleaner, more structured than the bar or corridors
- **Social temperature:** No warm greetings. NPCs are polite and guarded.
- **Voice register:** Complete sentences. Analytical. "Filing it." "Note it." "That's interesting."
### Opening Line Pool (enter_location, no prerequisites)
Lines that fire when the detective enters their starting location. Selection based on seed variant.
```yaml
- id: transit_m_d_open_001
character: detective
trigger: enter_location
location: the-terminal
text: "Sova Transit District. Third day on station. The briefing said routine compliance review."
mood: [professional, observant]
tags: [opening_hook, canonical_anchor]
- id: transit_m_d_open_002
character: detective
trigger: enter_location
location: the-terminal
text: "The briefing is always wrong about something."
mood: [analytical, wry]
tags: [opening_hook, canonical_anchor]
notes: "Companion line to open_001 — these fire in sequence on Variant A/B/C."
- id: transit_m_d_open_003
character: detective
trigger: enter_location
location: the-last-shift
text: "The bar at 08:30. Sera suggested it. The district has its own rhythms — find them before imposing mine."
mood: [professional, curious]
tags: [opening_hook, variant_a_bar]
- id: transit_m_d_open_004
character: detective
trigger: enter_location
location: the-terminal
text: "Manifest discrepancies clustered around the freight hub. Start there. Watch the shift change."
mood: [analytical, operational]
tags: [opening_hook]
```
### Sera Sighting Lines (observe_npc, Sera, Phase 1 relationship)
Critical FRIEND establishment for the detective.
```yaml
- id: transit_m_d_open_010
character: detective
trigger: observe_npc
target: npc:sera-venn
prerequisites:
relationship:
target: npc:sera-venn
state: friendly
text: "Good to know Sera's here. At least someone in this district speaks my language."
mood: [relieved, warm]
tags: [opening_hook, friend_phase_1, canonical_anchor]
- id: transit_m_d_open_011
character: detective
trigger: observe_npc
target: npc:sera-venn
prerequisites:
relationship:
target: npc:sera-venn
state: friendly
text: "Sera. Same as always — looks like she grew up in a place like this. I'll get there."
mood: [warm, mildly_isolated]
tags: [opening_hook, friend_phase_1]
```
---
### Variant A: Bar First (Warm Arrival)
**Initial condition:** Sera has been contacted ahead of time and suggested meeting at The Last Shift to brief the detective on the district.
**Starting location:** The Last Shift
**Opening state:** Sera is at her usual booth. The bar is opening for morning service — Lera is present but focused on setup. The bar is mostly empty.
**Bar arrival line:**
```yaml
- id: transit_m_d_open_020
character: detective
trigger: enter_location
location: the-last-shift
text: "The bar at 08:30. Only place in this district that doesn't smell like freight lubricant. Marginally."
mood: [wry, observant]
tags: [opening_hook, variant_a]
```
**Lera read (public NPC, establishing social texture):**
```yaml
- id: transit_m_d_open_021
character: detective
trigger: observe_npc
target: npc:lera-sessik
text: "Bar owner. Lera Sessik — Sera mentioned her. Watching me decide whether I'm going to be trouble. Fair assessment."
mood: [observant, professional]
tags: [opening_hook, variant_a]
```
**Sera orientation dialogue cue:**
Sera gives a brief district overview — who the regulars are, what the Terminal is like, what to expect from the shift supervisors. This is the detective's social tutorial through their friend's eyes.
*Authored dialogue line for Sera (opening orientation):*
> "The Terminal crew is tight. They've seen Commission before. Voss will give you what he's required to give you and nothing else. Maret's different — she worries about compliance more than the others. That might be useful."
> `access: [peer], trust: real, situation: [social, investigation]`
---
### Variant B: Terminal First (Cold Arrival)
**Initial condition:** No pre-contact. The detective enters the Terminal directly.
**Starting location:** The Terminal entrance
**Opening state:** The dock floor is active. Dock workers register the Commission presence and adjust. Voss is visible and approaches.
**Terminal read line:**
```yaml
- id: transit_m_d_open_030
character: detective
trigger: enter_location
location: the-terminal
text: "Logistics hub. Standard prefab, heavy foot traffic. Let's see what the shift change tells me."
mood: [analytical, professional]
tags: [opening_hook, variant_b]
```
**Voss approach (first adversarial NPC):**
```yaml
- id: transit_m_d_open_031
character: detective
trigger: observe_npc
target: npc:voss
text: "There's the shift supervisor. Saw me come in. He's deciding whether to approach or wait. Approaching. Good — I was going to find him anyway."
mood: [analytical, professional]
tags: [opening_hook, variant_b]
```
**Voss opening exchange (institutional friction):**
*Authored dialogue line for Voss (territorial greeting):*
> "Commission liaison. Right. I was told you'd be coming by. I've got the manifest copies from last quarter ready. Standard compliance package."
> `access: [authority], trust: surface, situation: [greeting, investigation]`
*Detective response cue (not authored — player choice from dialogue options):*
Standard question options: "Thank you, I'll need those." / "Tell me about your shift crew." / [Observe without responding]
---
### Variant C: Evidence in Hand
**Initial condition:** The detective has reviewed additional case file data overnight and arrives with two specific manifest discrepancies already flagged — container 4471 and one other.
**Starting location:** The Terminal, arriving at the manifest terminal directly
**Opening state:** The detective enters with a purpose they didn't have on day one. Something from the overnight analysis sharpened the investigation.
**Investigative focus line:**
```yaml
- id: transit_m_d_open_040
character: detective
trigger: enter_location
location: the-terminal
text: "Container 4471. Three separate appearances in the anomaly cluster. Same container ID, different cargo entries. That's not clerical error."
mood: [analytical, focused]
tags: [opening_hook, variant_c]
- id: transit_m_d_open_041
character: detective
trigger: observe_anomaly
location: the-terminal
text: "The manifest terminal. Maret's territory. She processes the corrections here. I want to see what corrections look like in the unfiltered log."
mood: [analytical, investigative]
tags: [opening_hook, variant_c]
```
---
### Variant D: Institutional Escort
**Initial condition:** A Commission compliance officer accompanies the detective for the first 10 minutes — standard procedure for new postings. The escort is Torek Lintar.
**Starting location:** Terminal entrance, arriving with Torek
**Opening state:** The district sees the badge + escort together. Social walls go up faster than normal. Torek is helpful but slightly evasive about district social dynamics.
**Escort read lines:**
```yaml
- id: transit_m_d_open_050
character: detective
trigger: observe_npc
target: npc:torek-lintar
text: "Lintar. Commission compliance, four years on Sova. He'll know the social texture better than any case file."
mood: [professional, curious]
tags: [opening_hook, variant_d]
- id: transit_m_d_open_051
character: detective
trigger: observe_npc
target: npc:torek-lintar
text: "He's being helpful but steering me away from something. Not sure what yet. File it."
mood: [analytical, suspicious]
tags: [opening_hook, variant_d]
notes: "Fires after 3+ minutes with Torek. The detective is already reading Torek as slightly off."
```
**Torek orientation dialogue cue:**
*Authored dialogue line for Torek (escort mode):*
> "The Terminal's well-run. Voss has been here eight years — knows his people. Maret handles scheduling, she's thorough. Drin does inspections, solid record. You'll find it's a tight operation."
> `access: [authority], trust: surface, situation: [social, investigation]`
> *Notes: The complimentary framing is slightly too smooth. The detective's monologue (open_051) flags this.*
---
## 3. Cross-Character Notes for Content Authors
### What both characters encounter in the first 3-4 minutes
| Element | Smuggler | Detective |
|---------|---------|-----------|
| Station ambient | Active from frame 0 | Active from frame 0 |
| Entity color landscape | Green (Kael), teal (other known NPCs) | Grey/teal (mostly Unknown/Known) |
| First NPC interaction | Kael (warm, first-name) | Voss or Sera (institutional or pre-established) |
| First insert use | Schedule board (operational) | Case file (investigative) |
| First monologue chime | Within 15 seconds | Within 15 seconds |
| Emotional tone | Comfort in familiarity | Alertness in unfamiliarity |
### What the opening must NOT do
- Use the name "Kael" before the player has LOS to Kael (smuggler)
- Use the surname "Davan" when the smuggler addresses Kael (always first-name)
- Use the first name "Sera" before the detective has established visual contact
- Reference operational facts the smuggler knows ("the ring") in any way that reads as exposition
- Reference investigation facts the detective knows (manifest discrepancies) in opening lines before arriving at the manifest terminal
- Have any NPC behave unexpectedly in the first 90 seconds — baseline must be established before deviation
---
*Ticket #260 — Opening hook content per character. v0.1 scope. Unblocked by #259.*
+234
View File
@@ -0,0 +1,234 @@
# The Settled Reach — Placeholder Art Specification v0.1
**Version:** v0.1 (Sprint 12)
**Author:** Araminta (Visual Designer)
**Date:** 2026-02-19
**Status:** Active — unblocks #133 (placeholder art pipeline)
**Cross-reference:** `docs/design/visual-grammar-v01.md` (visual grammar — this document derives from it)
This specification defines the technical requirements for placeholder art assets. v0.1 target: **zero image files**. All placeholder art is procedural — colored rectangles, styled text, shader overlays. This spec defines what the placeholder pipeline must produce so it can be swapped for real art without architectural changes.
---
## 1. Grid and Tile Dimensions
### 1.1 Dual-Scale Grid (D-066)
| Scale | Unit | Size | Used by |
|-------|------|------|---------|
| Simulation tile | 0.5m per tile | 32×32px (internal) | Server: movement, LOS, pathfinding, occupancy |
| Visual tile | 1m per tile = 2×2 sim tiles | **64×64px at runtime** | Client: all art authoring, tile placement |
**Author all art at 1m visual tile scale = 64×64px per tile.** The simulation subdivides automatically. The pipeline does not need to know about 32×32 sim tiles.
### 1.2 Runtime Resolution
| Context | Value |
|---------|-------|
| Visual tile size | 64×64 px |
| Game base resolution | 1920×1080 (1080p) |
| Godot camera | Orthographic, no DPI scaling applied to tile grid |
| Camera zoom | 1:1 at 1080p (1 viewport pixel = 1 art pixel) |
Do not target Retina/HiDPI resolutions with placeholder art — that's a production phase concern.
---
## 2. Tile Art Requirements
### 2.1 Structural Tiles (Floor, Wall, Door)
All structural tiles are **solid colored rectangles** in v0.1. No texture, no grain. Color palette from visual grammar §1.21.4.
| Tile type | Size | Color | Outline |
|-----------|------|-------|---------|
| Floor — Logistics Hub | 64×64 px | `#1a1e24` | None |
| Floor — Bar | 64×64 px | `#1e1912` | None |
| Floor — Corridor | 64×64 px | `#181818` | None |
| Wall face | 64×64 px | Zone wall face color (see visual grammar §1) | None (outlines added via LightOccluder2D geometry) |
| Wall top | 64×64 px | Zone wall top color (see visual grammar §1) | None. Rendered on z-layer 4 at ~70% opacity (D-049). Creates overhead occlusion. |
| Door (closed) | 64×64 px | Same as wall face, 4px lighter | 1px `#333340` |
| Door (open) | 64×64 px | Same as floor | 1px `#333340` threshold outline baked into the sprite at the doorway edge |
| Transition strip | 64×64 px | 50/50 horizontal split between adjacent zone floor colors | None. Procedurally generated per zone boundary — not a hand-authored asset. |
**Transition strips** (zone boundary tiles): generated procedurally at runtime as a horizontal 50/50 split between the two adjacent zone floor colors. No hand-authored asset required — the pipeline produces these from zone adjacency data. If a simple split reads poorly, upgrade to a 4-pixel dithered blend.
### 2.2 Object Tiles (Furniture, Equipment)
Objects are **tinted rectangles with a text label** in v0.1. Label identifies the object type.
| Object category | Size (visual tiles) | Fill color | Outline | Label |
|----------------|--------------------|----|---------|-------|
| Small item | 1×1 | `#8b8ba0` at 60% opacity | 1px `#333340` | Object name, 10px Michroma |
| Furniture | 1×1 or 2×1 | `#8b8ba0` at 70% opacity | 1px `#333340` | Object name, 10px Michroma |
| Large furniture | 2×1 or 3×1 | `#8b8ba0` at 70% opacity | 1px `#333340` | Object name, 10px Michroma |
| Terminal | 1×1 | `#4a5a7a` at 80% opacity | 1px `#4a9ebb` | "TERMINAL", 10px Michroma |
| Cargo crate | 1×1 or 2×1 | `#6a5a3a` at 70% opacity | 1px `#8b7a5a` | "CARGO", 10px Michroma |
Label placement: centered in the object rectangle. If the object is too small for a label, omit it. Legibility over completeness.
---
## 3. Entity Sprite Requirements
### 3.1 Sprite Canvas and Art Area
| Property | Value |
|----------|-------|
| Canvas size | 64×64 px (= 2×2 sim tiles, 1×1 visual tile footprint) |
| Art area | 24×32 px, centered within canvas |
| Dead space | 20px left, 20px right, 16px top, 16px bottom |
| Outline | 2px in D-033 relationship color |
Dead space is intentional — it provides clearance so entities don't visually merge when sharing adjacent tiles.
### 3.2 Placeholder Entity Shape
In v0.1, all entities are **tinted rectangle + outline + name label**:
- Rectangle: 24×32px, centered in 64×64 canvas
- Fill: D-033 relationship color at 90% opacity
- Outline: 2px D-033 relationship color at 100% opacity
- Label: NPC name or "Player", 11px Michroma, `#e8eaf0`, centered below rectangle (within canvas dead space)
The label goes **below** the art rectangle, within the canvas — it's the name badge of the placeholder, not a floating HUD element. When sprites replace rectangles, labels are removed from the canvas and handled separately by the HUD.
### 3.3 Color Source
Entity fill and outline color are **driven entirely by the client's derived relationship state** — not baked into the asset. The placeholder pipeline produces one generic rectangle shape; color is applied as a Godot shader parameter at runtime. This ensures the placeholder and the sprite system are architecturally identical.
Implementation: `entity_color` shader uniform, set per entity per frame from `ObserverSnapshot.relationship_state`.
### 3.4 Player Character Placeholder
Same spec as NPC placeholder, with these overrides:
| Property | Detective | Smuggler |
|----------|-----------|---------|
| Fill color | `#e0e8ff` at 90% | `#e8e0d0` at 90% |
| Outline | 2px `#e0e8ff` | 2px `#e8e0d0` |
| Label | "YOU (DET)" | "YOU (SMU)" |
---
## 4. Animation Frame Requirements
### 4.1 v0.1 Animation: None
All entities in v0.1 are **static placeholder sprites** — no animation. The animation tier system (D-047, visual grammar §6) defines what gets built for production art, not for placeholders.
The placeholder pipeline must produce single-frame sprites. Do not build animation infrastructure for placeholder sprites.
### 4.2 Production Target (for pipeline planning)
When placeholder sprites are replaced, the pipeline must support:
| Tier | Entity type | Frame budget | Directions |
|------|------------|-------------|------------|
| Tier 1 (Active NPC) | Named + generic | 8 frames walk, 23 idle | 2 drawn (N/S), 2 mirrored (E/W) |
| Tier 2 (Background NPC) | Generic only | 1 frame (static) | 1 direction, no mirroring needed |
| Player | Both characters | Same as Tier 1 NPC | 2 drawn + 2 mirrored |
Frame format for production sprites: PNG strip (horizontal), 64×64px per frame, filename convention: `entity_{name}_{direction}_{action}.png` (e.g., `entity_kael_south_walk.png`).
---
## 5. Color Palette Constraints
### 5.1 What the Placeholder Pipeline Produces
The pipeline does not bake colors into assets. It produces **shape templates** that accept color as a runtime parameter (shader uniform or Godot modulate). This matches the production principle from D-043: "Sprites are shape templates that the lighting system completes."
### 5.2 Color Sources
| Asset type | Color source |
|-----------|-------------|
| Entity fill + outline | `entity_color` uniform from `RelationshipState` (per entity, per frame) |
| Object fill | Fixed per object type (see §2.2 table) |
| Floor tiles | Fixed per zone (see visual grammar §1) |
| Insert overlay | `#c8d0e0` chrome + D-033 relationship colors for entity markers |
| Fog shader | Driven by LOS mask, not a color constant |
### 5.3 What Not to Do
- Do not hardcode entity colors in art assets
- Do not add zone-specific color variants of the same object shape — zone lighting handles the emotional temperature
- Do not add object colors that compete with entity saturation (see visual grammar §3.6)
---
## 6. File Format and Asset Organization
### 6.1 Format
| Context | Format | Notes |
|---------|--------|-------|
| All placeholder sprites | PNG | Alpha channel required for entity canvas dead space |
| Production sprites (future) | PNG | Same. DDS conversion deferred to Phase 4 |
| Shader assets | `.gdshader` in `client/shaders/` | Not part of placeholder art pipeline |
### 6.2 Asset Directory Structure
```
client/assets/
art/
placeholder/
tiles/
floor_logistics.png # 64×64px, solid #1a1e24
floor_bar.png # 64×64px, solid #1e1912
floor_corridor.png # 64×64px, solid #181818
wall_face.png # 64×64px, solid (zone-parameterized at runtime)
wall_top.png # 64×64px, solid (zone wall top color, rendered at ~70% opacity on z-layer 4)
door_closed.png # 64×64px
door_open.png # 64×64px (threshold outline baked in)
objects/
obj_generic.png # 64×64px grey rectangle (color via modulate)
obj_terminal.png # 64×64px tinted, "TERMINAL" label
obj_crate.png # 64×64px tinted, "CARGO" label
entities/
entity_generic.png # 64×64px, 24×32 art area (color via shader)
entity_player.png # 64×64px, same — player uses same shape
production/ # Empty in v0.1, populated in Phase 24
```
### 6.3 Naming Convention
`{category}_{identifier}.png`
- Category: `floor_`, `wall_`, `obj_`, `entity_`, `door_`
- Identifier: zone or type name, lowercase, underscores
- No version suffixes in filename — versions are tracked by git
### 6.4 Source Dimensions
Placeholder art is authored at **64×64px final size** — no resolution chain. Production art will use the 1024px → 256px → 64px resolution chain (D-043) when AI-generated assets enter the pipeline. The placeholder pipeline does not need to support this chain.
---
## 7. Lighting Constraints for Placeholder Art
Sprites must be **authored as flat, neutral-lit shapes** so Godot's Light2D pipeline completes the mood. Do not bake:
- Shadows (directional or point-source)
- Lighting gradients (bright on one side, dark on other)
- Ambient occlusion
- Any lighting that implies a specific light source direction
The PointLight2D pipeline (D-046) does all of this at runtime. Baked lighting in sprites will fight the runtime lighting and produce incoherent results as the player moves.
**Test:** Drop a placeholder tile into a dark Godot scene with a single PointLight2D. The tile should look correct — lit on the side facing the light, dark on the far side. If it looks wrong, the sprite has baked lighting.
---
## 8. Phases Reference
From the briefing — for pipeline planning:
| Phase | Focus | Target |
|-------|-------|--------|
| **Phase 1 (current)** | Structural | Tile floor plans, entity rectangles, fog rendering. Blocks gameplay testing. |
| **Phase 2** | Information | Insert overlay, monologue display, dialogue panel, sound indicators. Blocks content testing. |
| **Phase 3** | Atmosphere | Zone palettes, lighting, span gate glow, character insert variants. Blocks experience testing. |
| **Phase 4** | Polish | Sprite replacements, animations, particles. Pre-release. |
**v0.1 target: Phases 13.** Zero image files — everything procedural. This spec covers Phase 1 asset requirements. Phases 2 and 3 derive from the visual grammar and will be addressed in subsequent tickets.
+186
View File
@@ -0,0 +1,186 @@
# Sova Station Profile — Station Beyond Transit District
**Ticket:** #320
**Author:** Miri
**Status:** canonical
**Last updated:** 2026-02-19
Reference: D-036 (Sova setting), D-050 (Velen/Krenn system)
Blocks: #335 (Meridian Coverage Map for Sova Transit)
---
## Purpose
Sova Transit District is not all of Station Sova. NPCs reference the wider station constantly — medical services are "at the main station," a certain district has a better bar, administrative offices are "upstation," off-ring contacts live in other sectors. Dialogue needs to know what's there without the game needing to implement any of it in v0.1.
This document defines the parts of Station Sova that exist beyond the playable Transit District. It is a reference document for content authors, not a design specification for future development.
---
## Overview
**Station name:** Station Sova
**System:** Krenn System
**Orbit:** Station Sova is a permanent orbital facility around Velen, the Krenn System's primary habitable world
**Population:** ~12,000 permanent residents; fluctuating transient population of 1,000-3,000 (freight workers, visitors, crew)
**Primary function:** Logistics hub for Krenn System freight operations — the transit point between Velen's planetary freight depot and outgoing freight routes
**Governance:** Station Administrator (appointed by Krenn System Concord Compact, confirmed by regional Concord Assembly representation)
**Construction:** Original core built during early Krenn settlement (~150 years ago). Significant expansion ~80 years ago. Current configuration largely stable for ~40 years.
**Architectural character:** Classic mid-Reach logistics station — functional, layered, mixed-era. Not elegant. Not derelict. The kind of structure that works because it's been maintained, not because it was designed to last.
---
## Station Districts
Station Sova is divided into six administrative districts. Only the Transit District is playable in v0.1; the others exist as references in dialogue and establishing context.
### 1. Transit District (Playable — v0.1)
The freight logistics hub. Span gate terminal, cargo handling, customs processing, and the service economy that serves freight workers. Full documentation in D-036 and `docs/design/sova-texture-appendix.md`.
**Population:** ~800 (workers and service economy residents)
**Character:** Working-class freight district. "Quotidian-with-undertow."
**Key locations:** The Terminal (logistics hub), The Last Shift (bar), maintenance corridors
**Meridian coverage:** Strong in institutional spaces, degraded in older sections, minimal in maintenance corridors
---
### 2. Residential Core
The largest district by physical space. Station Sova's main housing blocks, organized by construction era and (informally) by occupational affiliation.
**Population:** ~6,000
**Character:** Unremarkable mid-Reach residential. Standard housing modules, shared communal spaces, a mix of long-term residents and contract workers. Not comfortable enough to be pleasant, not uncomfortable enough to drive people away. Home.
**Relevant to v0.1 content:**
- Most Transit District workers live here, not in the Transit District itself. They commute (a short internal transit ride).
- NPCs who've left the bar for the evening have gone here, not immediately to bed — there's a residential stop in between.
- When NPCs say "I'm heading home," this is where they're going.
- Station housing is assigned through the Administration; moving districts requires paperwork. Workers who've been here for years have established housing they don't want to lose — another reason not to attract institutional attention.
**Off-limit in v0.1 content:** Don't invent specific addresses or named locations in the Residential Core. Reference it generically.
---
### 3. Administrative Hub (Upstation)
Station Administration offices, Commission regional office, Concord Assembly liaison office, main station security, and the formal infrastructure of governance.
**Population:** ~1,000 (administrative staff, Commission personnel, security, liaisons)
**Character:** Institutional. Cleaner than the rest of the station. Better maintained. The kind of spaces where people wear official-issue everything and speak more carefully.
**Relevant to v0.1 content:**
- When the detective needs to file reports, pull Commission records, or deal with institutional bureaucracy, it happens here (or via Meridian queries routed to here).
- The detective's direct superior has an office here. Their interactions with the chain of command mostly happen via Meridian, but physical visits are possible.
- The ring's greatest institutional threat: if the Commission regional office takes interest in Transit District operations, the scale of scrutiny increases dramatically. The district's current intermittent Commission presence is favorable; Commission attention from Upstation is not.
- Workers from the Transit District rarely go to the Administrative Hub voluntarily. Going there attracts attention. Being seen going there — if you're ring-adjacent — is a tell.
**Shorthand in NPC dialogue:** "Upstation" — any NPC in the Transit District uses this term to mean the Administrative Hub and surrounding institutional spaces. "They went upstation about it" means they escalated to official channels.
---
### 4. Medical Center
The station's primary healthcare facility, including re-embodiment services.
**Population:** ~500 (medical staff, patients, support)
**Character:** Functional medical institutional. Better equipment than the Transit District's small clinic. More staff. Commission-accredited re-embodiment facility.
**Relevant to v0.1 content:**
- Serious medical issues that the district clinic can't handle are referred here: "They sent him to the main medical center."
- Re-embodiment services: the full facility is here. The Transit District clinic can stabilize, not restore. People who die in the Transit District are recovered and restored at the main medical center, assuming they've maintained backup status.
- Medical-grade lattice replacement: legitimate lattice component replacement (as opposed to the ring's aftermarket version) is performed here under Commission oversight. The wait list is long. The price is high. The ring's medical-grade diverted components exist in part because this facility's legitimate access is priced beyond working-class reach.
- One detail for Naia's situation (if referenced in content): Naia was quoted a legitimate replacement timeline and price at the main medical center. The timeline was 8-12 months waiting list. The price was prohibitive. Kael's ring activity began shortly after.
---
### 5. Commercial Quarter
Retail, food service, entertainment, and light commercial activity. The station's "main street."
**Population:** ~1,500 (residents, visitors, commercial staff)
**Character:** Mid-range commercial. Better quality than the Transit District's bar and food vendors. More expensive. For Transit District workers, a visit to the Commercial Quarter is a mild treat — it costs more, but the food is better and the drinks have more options.
**Relevant to v0.1 content:**
- Workers occasionally reference going to the Commercial Quarter for something specific ("better than that place in the Quarter"). The Transit District workers have mild pride about their district's bar vs. what they perceive as the more pretentious options in the Commercial Quarter.
- The ring does not operate in the Commercial Quarter. Too much Meridian coverage. Too many eyes. Their business stays in the Transit District.
- A few NPCs (Maret Korr, for instance) might occasionally reference shopping in the Commercial Quarter for specific items. It's a small social marker: people who go to the Quarter regularly have slightly more disposable income.
---
### 6. Industrial Sector
Manufacturing and processing facilities: station power generation, environmental systems, primary maintenance, water recycling, atmospheric management.
**Population:** ~700 (industrial and maintenance staff)
**Character:** Heavy industrial. Workers here are different from freight workers — more technical, more specialized, more solitary. They service the systems that keep everyone alive.
**Relevant to v0.1 content:**
- Station maintenance workers (like Drin) work in the Transit District but have a professional connection to the Industrial Sector. Their guild structure, if any, is organized across both.
- The ventilation issue in Transit District Sector 3 is technically under the Industrial Sector's maintenance jurisdiction. The reason repairs are delayed is a combination of Industrial Sector queue management and Transit District Administration's low priority in maintenance allocation. Neither district's administration has the authority to force the other to act. This is the bureaucratic reality behind the local complaint.
- If a significant system failure affects the station, it originates here. The span gate terminal's power supply, for instance, runs through Industrial Sector infrastructure.
---
## Governance Structure
**Station Administrator:** Appointed position. Currently Administrator Hassel (known by name in institutional circles, by title to most residents, not known at all to many workers in the Transit District). Oversees all district administration.
**District Administrators:** Each district has a sub-administrator who reports to the Station Administrator. The Transit District's sub-administrator is rarely physically present in the district — they manage via Meridian reports and quarterly visits.
**Commission Regional Office (Upstation):** The station's Commission presence reports to the regional Commission office (off-station). The regional office sets investigative priorities. The station office executes them. The investigative mandate that brought the detective to Sova Transit was issued by the regional Commission office, routed through the station Commission office, executed as a "logistics compliance review." The chain is: Regional Commission → Station Commission → Station Administrator → Transit District Administration.
**Security:** Station security is organized through the Administrative Hub. The Transit District has a minimal security presence (one officer on shift, available by Meridian call). Major security responses come from the Administrative Hub. The ring knows this response time and plans around it.
---
## What's Off-Station (References Only)
NPCs may reference locations and entities beyond Sova Station. These are real but out-of-scope for v0.1 playable content.
**Velen:** The Krenn System's primary habitable world. Temperate-maritime climate. The span gate connects Sova Station to a planetary freight depot on Velen's surface. Most cargo that flows through Sova Transit either originates or terminates on Velen. The planet is visible through the station's viewports — a blue-green sphere with frequent cloud cover. Residents call it simply "the surface" or "downwell." Most station workers have been to Velen; some lived there before moving to the station for work.
**Velen Freight Depot:** The planet-side destination of the Sova span gate. A functional logistics hub that mirrors the Transit District's function. The ring's product goes through here on its way to planet-side distribution. The ring has contacts in the Velen depot — logistics workers in an identical job to the ring's members, running the same kind of operation.
**Other Krenn System stations:** The Krenn System has two other smaller orbital facilities (mining support station and an administrative relay). They're referenced occasionally in news tickers and operational scheduling. Not relevant to v0.1.
**The horizon gate:** Sova Station has a connection to the Reach's horizon gate network — the interstellar transport infrastructure. The horizon gate terminal is in the Administrative Hub district, not the Transit District. Characters with legitimate need can book transit to other systems. This connection is what makes Sova relevant to a larger smuggling network; contraband doesn't originate in-system, it comes from elsewhere via horizon gate and moves through Sova's span gate to Velen.
---
## Meridian Coverage Notes
(Relevant for Ticket #335 — Meridian Coverage Map for Sova Transit, which this document unblocks)
**Station-wide coverage status:**
| District | Coverage | Notes |
|---|---|---|
| Administrative Hub | Comprehensive | Commission-grade infrastructure. Full monitoring in all public spaces. |
| Commercial Quarter | Comprehensive | Commercial monitoring standards. Dense coverage for transaction tracking. |
| Medical Center | Comprehensive | Patient record and equipment monitoring. Full coverage. |
| Residential Core | Standard | Full coverage in common areas, standard-grade residential coverage in units. |
| Industrial Sector | Standard with gaps | Monitoring in operational areas; some older infrastructure zones with reduced coverage. |
| **Transit District** | **Degraded (see detail)** | **See `docs/design/sova-texture-appendix.md` Section 5 and Ticket #335** |
The Transit District's degraded coverage is partly structural (older sections, mixed infrastructure) and partly the result of the ring's ongoing Severance tech deployment in specific dead spots. The ring did not create the dead spots — they identified and exploited existing coverage gaps, then maintained and selectively deepened them using Severance equipment.
---
## Content Authoring Notes
**When referencing off-Transit-District locations:**
- Keep references vague and experiential: "the main medical center," "upstation," "the Quarter," "the industrial section" — never specific room numbers or facility names for out-of-scope locations
- NPCs should reference these places as workers refer to any adjacent district: familiarly but not in detail
- The detective may pull Commission records that reference other districts — these can include district names (Administrative Hub, Medical Center) but should not include fabricated detail about their internal layout
**What NPCs know:**
- Transit District workers know the station layout in the way workers know where they work: they know their commute route, the medical center, the Commercial Quarter, and have vague awareness of everything else
- NPCs who've lived here longer know more (Lera would know who works at specific Commercial Quarter establishments; Drin would know the Industrial Sector workers by name)
- The detective's insert can provide district maps at a general level — they know Sova Station's layout from Commission briefing files, but not at the ground-level social detail that residents carry
---
*This document establishes the off-district context that NPC dialogue requires. The playable Transit District is embedded in a larger station that has its own rhythms, hierarchies, and institutions. That context should be ambient in content — felt as background, not foregrounded.*
*Miri — Sprint 12*
+253
View File
@@ -0,0 +1,253 @@
# Sova Texture Appendix
**Ticket:** #302
**Author:** Miri
**Status:** canonical
**Last updated:** 2026-02-19
Companion to: `docs/design/cultural-generation-guide.md`
Reference: D-036 (setting), D-037 (contraband)
---
## Purpose
This appendix provides the sensory, linguistic, and social texture of Sova Transit District. It is the reference Mellanie uses for environmental text, NPC dialogue, and monologue authoring. It is also the reference Araminta uses for signage content and decorative detail.
Everything here is canonical for v0.1. Any divergence from this appendix in authored content should be flagged and resolved.
---
## Section 1: Slang Glossary — 20 Terms
Sova Transit District has developed its own working vocabulary over 40 years. This is not exotic alien slang — it is the language of people who do physical work in the same building every day, who have developed shortcuts for things they say constantly, and who occasionally use vocabulary to mark in-group membership.
### Work and Schedule
| Term | Meaning | Usage | Notes |
|---|---|---|---|
| **marks** | Credits (the standard currency unit) | "That run paid out four hundred marks." | Krenn System local usage. Not reach-wide. "Credits" is the formal/institutional term. |
| **shift** | Work period (typically 8 hours) | "I've got another four hours on shift." | Never say "rotation" or "tour" — those are military register, wrong for a freight district. |
| **the after** | Off-shift social time | "See you in the after." / "Heading to Lera's in the after." | Specific to Sova Transit — the period after the main shift wave when people decompress at the bar. |
| **the gate** | Span gate terminal (informally) | "Two more transits through the gate this evening." | "The span gate terminal" is institutional. "The gate" is how everyone actually refers to it. |
| **a push** | A particularly heavy cargo day (high transit volume) | "It's a push today, we're running behind." | Derives from the physical feeling of high-volume freight work. |
| **running light** | Low-cargo day, easy shift | "Gate traffic's down — running light this afternoon." | Opposite of "a push." |
| **the floor** | The main logistics hub workspace | "Meet me on the floor at 0900." | Distinguishes the active workspace from offices/break room. |
| **sign-off** | End-of-shift handover | "Waiting on sign-off from Voss before I can leave." | The formal process of handing off responsibility. Used literally but also as general end-of-process term. |
### People and Social
| Term | Meaning | Usage | Notes |
|---|---|---|---|
| **on-book** | Within official channels, traceable | "Keep this on-book — I don't want complications." | Implies its opposite (off-book) exists and is known. |
| **off-book** | Outside official channels, not traceable | "That container moved off-book last night." | Ring usage primarily, but anyone in the district understands the term. Using it casually signals in-group awareness. |
| **a regular** | A bar regular, someone who's been coming to The Last Shift for years | "Ask Drin — he's a regular, he'd know." | Social status marker. Regulars are trusted community members. |
| **Commission eyes** | Commission presence (investigative or surveillance) | "There were Commission eyes on the floor yesterday." | Not hostile exactly — just careful. The phrase itself is a warning. |
| **tight mouth** | Someone who doesn't talk about things they shouldn't | "Sess is a tight mouth — you can say what you need." | Compliment in Sova. Being a tight mouth is trustworthy. |
| **loose** | Someone whose reliability or discretion is questionable | "Torek's been loose lately. Something's wrong with him." | Can mean careless, nervous, unreliable, or compromised. Context-dependent. |
### District and Infrastructure
| Term | Meaning | Usage | Notes |
|---|---|---|---|
| **Sector 3** | Older section of the district with chronic ventilation issues | "It's Sector 3 in here today." (meaning: unpleasantly warm and stale) | Local complaint made cultural shorthand. Used for any space with poor air quality. |
| **the gap** | The unofficial maintenance corridors between old and new construction | "Meet me in the gap at 2300." | Ring terminology primarily, but maintenance workers also use it. |
| **the feed** | The Meridian news ticker in the bar and hub lobby | "You see what was on the feed this morning?" | The communal information source. Not watched actively but referenced constantly. |
| **dead air** | A Meridian dead spot (poor or absent coverage) | "Careful pinging from the break room — the far corner is dead air." | Important to know for the detective (coverage gaps limit investigation capability) and the ring (dead air zones are safe communication spaces). |
| **the clinic** | Small medical facility in the district | "Took him to the clinic, they sent him to the main station for anything serious." | Establishing that serious medical needs require leaving the Transit District. |
| **a re-em** | Re-embodiment (colloquial) | "He almost didn't come back from that one — had to do a re-em." | Casual reference to the setting's death/resurrection technology. The matter-of-fact tone reflects how normalized this is in the Reach. |
---
## Section 2: Informal Decoration — The Human Mark on Institutional Space
The Transit District is 40 years old. In that time, workers have personalized the spaces they inhabit daily. This personalization is the human layer over institutional structure — and it's readable.
### Locker Areas (Hub Break Room)
- **Personal items on display:** Workers with long tenure often leave small decorative items on top of their lockers — a small potted plant (maintenance workers are proud of anything growing), a printed family image in a cheap clip frame, a figurine. These are subtle status markers: the longer your items have survived, the more established you are.
- **Stickers on lockers:** Union notices, small humorous prints, Meridian feed screenshots of something funny, a schedule drawn by hand in someone's own ink because the Meridian calendar didn't show what they needed to see. Not excessive. A few per locker, if any.
- **Hand-lettered notices:** Shift swap requests. Lost item reports ("Missing: insulated mug, blue, my name on the bottom. Please return."). Notices for informal events. These are pinned near the locker room entrance on a community board that is technically for official notices and has been completely colonized by informal ones.
### The Last Shift (Bar Interior)
- **The back bar shelf:** Lera keeps a curated collection of objects on the shelf behind the bar. Some are gifts from regulars. A small figure. A transit token from a gate that doesn't exist anymore (it closed; the token is a reminder). An industrial ceramic mug that's been there since before Lera owned the place. These are not decoration — they are the bar's memory.
- **The corner booth:** Someone carved initials into the underside of the table years ago. It's become a tradition. The underside of the corner booth table is covered in initials and short phrases, none of which are visible unless you deliberately look.
- **Graffiti (external only):** The district's external corridor walls, especially near maintenance access points, have accumulated graffiti over the years. It ranges from tagging (names in stylized Krenn script) to short phrases and symbols associated with the Guardians of Severance (counter-surveillance imagery — not political slogans, just visual signals that someone was here who didn't want to be found).
- **Meridian displays:** The bar's Meridian screen — the one that runs the news feed — has been locally configured with a custom background that no one knows the origin of. It shows a slightly washed-out image of a star seen from orbit, probably from an old transit recording. It cycles in the background behind the feed text. Nobody has changed it in years.
### Hub Workspace (The Floor)
- **Workstation personalization:** Minimal (supervisors don't officially allow it but don't enforce the rule). A sticker on a scanner terminal. A hand-drawn reminder taped to a container staging area ("THIS BAY — PRIORITY FREIGHT ONLY — NO EXCEPTIONS"). The reminder has been there for so long it's yellowed.
- **Informal signage:** Near the break room entrance, someone has written "0600 - 0800 MORNING RUSH — BE PATIENT WITH EACH OTHER" in block letters on a whiteboard that has been there long enough the letters are permanent. Nobody removes it. Nobody discusses it. It's just true.
- **Scanner bay markings:** Workers have developed an unofficial color-coding system for scanner bay status that supplements (and sometimes contradicts) the official Meridian-logged system. A piece of yellow material tied to a handle means "bay running but slow today." A red piece means "don't use this bay — something's off." The ring uses a version of this system for operational purposes.
---
## Section 3: Sensory Details — Smell, Sound, Light, Temperature
### Smells
| Location | Primary | Secondary | Notes |
|---|---|---|---|
| **Logistics hub (the floor)** | Industrial lubricant, faintly metallic | Container seal compound (sharp, chemical), recycled air with a cargo edge | The smell of work. Familiar, ignorable, fundamentally unglamorous. |
| **Break room / lockers** | Recycled air, mild human presence | Someone's lunch, coffee-equivalent from the dispenser, old coat in a locker | Warmer than the floor. Less industrial. The air is breathed-in. |
| **Maintenance corridors** | Dust, metal, old sealant | Ventilation system exhaust (musty in the older sections) | Sector 3's ventilation smell is noticeably worse — stale air with a faint biological undertone. Not dangerous. Just unpleasant. |
| **The Last Shift (bar)** | Cooked food, drink, human warmth | Cleaning compound underneath, Meridian music electronics if you're near the speakers | Comfortable. The smell of a place that's used. |
| **Near span gate terminal** | Ozone, electrical, slight metallic ionization | The "Interstitium trace" — a distinctive scent that clings to everything that passes through the gate. Not quite describable. Metallic? Cold? | Distinctive enough that people who've traveled through gates recognize it. Those who grew up near the terminal have stopped noticing it entirely. |
**Monologue note:** The Interstitium trace is monologue-useful. A character who grew up without gate access would notice it. A character who's worked the terminal for years wouldn't. It's one of the details that differentiates characters who are from Sova versus those who've just arrived.
### Sound
| Sound | Source | Rhythm | Character Notes |
|---|---|---|---|
| **The span gate hum** | Gate terminal, deep subsonic vibration | Constant, with variations: rises before transit, peaks during transit, settles after | This is the district's heartbeat. Everyone has tuned it out. When it stops for maintenance, the silence is unsettling in a way most people can't immediately identify. |
| **Grav-lift motors** | Container handling equipment on the floor | Intermittent, in waves matching freight cycles | Brief, purposeful. Not loud. A background to conversation on the floor. |
| **Scanner pings** | Freight scanners during container checks | Rhythmic during active scanning, sporadic otherwise | Individual pings are short, clean tones. A scanner in full operation sounds like irregular dripping. |
| **Ventilation system** | Throughout the district | Constant | Sector 3's ventilation makes a cycling noise — a low grinding before it settles back into hum. Everyone knows the pattern. It means nothing. It also means: maintenance hasn't fixed it. |
| **Bar audio** | Conversation, glass surfaces, Meridian music feed | Builds through evening hours | The Meridian music feed is generic, low-volume, not curated. Nobody talks about it. It's just there. Background radiation. |
| **Lattice notifications (player character only)** | Internal — only the recipient hears | Varies | Visual tell: an NPC checking their lattice is always a *visual* behavior, not auditory. To external observation, they just paused or looked at nothing for a moment. |
### Lighting
| Location | Type | Quality | Notes |
|---|---|---|---|
| **Gate terminal approach** | Institutional white | Clean, consistent | The newest infrastructure. Maintained to Commission standards. |
| **Hub floor** | Industrial functional | Even, harsh | Good for working. Not comfortable for extended conversation. |
| **Break room** | Warmer, slightly dimmer | Softer than the floor | Someone at some point replaced the standard bulbs with something warmer. It stayed. |
| **Older district sections** | Mixed — institutional fixtures mixed with aftermarket additions | Irregular, pooled | As you move away from the gate terminal, lighting becomes less uniform. Some fixtures have been out for weeks. Some sections have added personal lighting (a clip-on, a strip). |
| **Maintenance corridors** | Minimal — emergency strips only | Low, directional | "The gap" specifically has only the emergency lighting. Dark enough to be useful for operational purposes. Not so dark as to be suspicious. |
| **The Last Shift** | Warm, slightly dim, inconsistent | Intimate by accident more than design | The bar's lighting was never professionally designed. It's comfortable because the fixtures are old and warm, not because someone planned it. |
### Temperature
| Area | Temperature | Notes |
|---|---|---|
| **Hub floor** | Slightly warm | Machinery heat. Workers dress for it. |
| **Corridors** | Slightly cool | Better ventilation than interior spaces, plus the gate terminal's cooling systems. |
| **Sector 3** | Inconsistently warmer | Poor ventilation traps heat. Worse in a push. |
| **Break room** | Comfortable, stable | Climate-controlled, one of the few consistently comfortable spaces in the district. |
| **The Last Shift** | Comfortable, slightly warm in the back | A small luxury: the bar maintains comfortable temperature as a business practice. The back near the kitchen is warmer. |
---
## Section 4: The Local Complaint
Every working community has a shared grievance — one specific, named, persistently unresolved problem that serves as social glue. You can bond with anyone in Sova Transit by mentioning it.
**The ventilation in Sector 3.**
The older sections of the Sova Transit District were built with ventilation systems that were adequate in the original design. Forty years of modification, additional occupants, and patched infrastructure have reduced Sector 3's ventilation to something that only technically functions.
- It's been on the maintenance list for at least seven years.
- Station Administration has reviewed and scheduled it twice. Both times, the crew that showed up found a "more pressing issue" elsewhere and never came back.
- In summer (when Sova's station systems run warmer), Sector 3 is noticeably unpleasant — not dangerous, just stuffy and slightly too warm, with an undertone of recycled air that's been recycled one too many times.
- The workers who use Sector 3 storage regularly have developed coping strategies: they time their work there for cooler parts of the day, they keep the access doors cracked despite the regulation, they've developed a grim pride in the complaint itself.
- The complaint is universal. Everyone knows it. Everyone has an opinion about why it hasn't been fixed. The actual reasons (budget allocation, maintenance crew priorities, bureaucratic backlog) are less interesting than the many explanations people have invented.
**Authoring utility:** The ventilation complaint is the universal safe topic for establishing a conversational rapport. Any NPC can mention it. Any player character can use it as an opener. It is also the safest deflection: an NPC who's uncomfortable with a line of questioning can change the subject to the Sector 3 ventilation with perfect plausibility. The detective should know this.
---
## Section 5: Meridian Self-Censorship Rules
The Meridian is the Reach's information network — think of it as the internet, but woven through every lattice implant. What you say on Meridian-logged channels is, technically, stored. In practice, the logs are rarely reviewed unless something flags. But everyone knows they exist.
This creates a behavioral layer in Sova Transit: things that are said, and things that are not said, specifically because of the Meridian.
**What people don't say on Meridian channels:**
- **Anything ring-related, obviously.** The ring communicates through Meridian dead spots, physical dead drops, and coded language when forced to use logged channels.
- **Personal grievances about Commission personnel.** Not because anyone is monitoring, but because you never know when they start.
- **Specific speculation about cargo contents.** A dock worker might suspect a container is non-standard. They won't say so on a Meridian-logged ping.
- **Honest assessments of work quality.** "That scanner is giving false clears" — not in a ping. In person, sure.
- **Off-book financial discussions.** The ring aside, plenty of small informal economies operate in Sova Transit. None of them live on Meridian channels.
**What people do say on Meridian channels:**
- Logistics coordination. Schedule confirmations. Cargo status.
- Social plans. "Heading to Lera's after. You coming?"
- Complaints about the ventilation in Sector 3.
- Anything they'd say with a Commission investigator standing next to them.
**Code language (ring use only):**
The ring has developed a minimal vocabulary for Meridian communication when dead air isn't available:
| Ring Code | Meaning | How It Sounds |
|---|---|---|
| "the usual pickup" | An operation is scheduled | "Did you confirm the usual pickup for tonight?" |
| "the manifest is clean" | Shipment cleared successfully | "Manifest is clean on my end." |
| "Drin was asking" | Someone is paying attention / possible surveillance | "Drin was asking about Corridor 7B again." |
| "running maintenance" | Conducting ring business | "I'll be running maintenance late tonight." |
| "the cans are ready" | Contraband loaded and staged | "Cans are ready in the usual place." |
**Authoring note:** The code language is invisible to the detective until they have enough context to parse it. A line like "I'll be running maintenance late tonight" triggers nothing for the detective on first hearing. It triggers recognition later, once they understand what "running maintenance" means. This is one mechanism for the invisible locked dialogue options (D-062) — not locked by UI, but locked by player knowledge.
---
## Section 6: Food and Drink
The bar exists. The food vendors exist. These things have to have names, textures, and enough specificity to feel real without becoming a treatise on speculative food science.
### The Last Shift
**Drinks:**
| Item | Type | Notes |
|---|---|---|
| **Station ale** | Generic draft, slightly warm, session-strength | The default. If you order "the usual," Sess gives you this unless they know you specifically. |
| **Krenn grain spirit** | Local distillation, strong, slightly earthy | The district's working-class prestige drink. When someone buys you one, it means something. |
| **Lera's blend** | A house-mixed drink, recipes vary, always strong | Lera makes it differently each time, claims it's the same. It isn't. Nobody complains. |
| **Synthetic grape** | Non-alcoholic, sweet, slightly off | For people who don't drink. Available but not enthusiastically served. |
| **Commission-grade tea** | Institutional tea from a packet | The term is mock-institutional. It means unremarkable tea that nobody actually wants. |
**Food:**
| Item | Description | Notes |
|---|---|---|
| **Station bread with pressed meat** | The house staple | What almost everyone orders if they're eating. Reliable. Not memorable. |
| **Grain bowl with pickled vegetables** | Vegetable-forward, filling | Maret Korr's regular order. |
| **The late-night plate** | Whatever Sess has left. Inconsistent. Usually fine. | After 2200, this is your only food option. |
### Food Vendors (District, Not Bar)
Quick-service stalls in the district's transit corridor. Workers grab food before or between shifts.
- **Pressed ration pouches:** The fast option. Pre-packaged. Efficient. Nutritionally complete and deeply uninspiring. Nobody calls them ration pouches to their face — they call them "pouches" or just "a pack."
- **Hot grain dishes:** Made to order, fast. Typically seasoned with synthetic spice compounds (the Reach's spice trade is functional but limited compared to agricultural worlds). Good enough.
- **Re-brew:** A coffee-equivalent stimulant drink made from reconstituted compounds. It is called "re-brew" because it is, technically, brewed from recycled stimulant compounds. Workers know this. They drink it anyway.
---
## Section 7: Recurring Social Rituals
These are the behavioral patterns that give Sova Transit District its texture as a living community.
### Shift-End Ritual
When a shift ends and the handoff is complete, the outgoing workers have a brief period of transition before they can leave or go to the bar. This transition has an informal ritual quality: there's a particular spot in the break room — a bench near the locker area — where people sit for five minutes and do nothing. Nobody talks about this ritual. Nobody calls it what it is. It's just what you do before you become an off-shift person instead of an on-shift person.
**Authoring utility:** A character who skips this ritual (leaves immediately after sign-off) is in a hurry about something. The ring uses this window for brief one-on-one conversations without looking conspicuous — two workers sitting in the transition space, talking quietly, is unremarkable.
### The Card Game
Every other cycle-end (roughly every two weeks), a small group of regulars plays cards at the corner table of The Last Shift. It's the same people. The game is the same game. The game itself is less the point than the habit of gathering.
Lera reserves the corner table. Non-participants know not to take it on card game nights. The detective doesn't know this convention when they first arrive — another social marker of being a newcomer.
**Authoring utility:** Card game nights are an opportunity for the detective to observe established social patterns without them being staged for their benefit. The smuggler would know about card nights; the detective only knows if they've been in the district long enough to see it happen, or if someone tells them.
### The Gate Pause
When the span gate does a major transit — a large cargo push through the gate — there's a moment where the hum changes pitch. Workers who've been here long enough pause, briefly, in what they're doing. It's not acknowledged. It's not a ritual. It's just a moment where the district's heartbeat is audible, and everyone who's lived next to it knows what it means.
**Authoring utility:** This pause is a monologue opportunity. The smuggler might reflect on the operational significance of the transit (what was in that push?). The detective might notice which workers paused and which didn't — the ones who paused have been here long enough to notice. The ones who didn't are newer.
---
*This appendix is canonical for v0.1. If content authors need details not covered here — a specific vendor name, a particular food item, a third bar drink — they should add it and flag it to Miri for consistency check.*
*Companion document: `docs/design/cultural-generation-guide.md`*
*Miri — Sprint 12*
+211
View File
@@ -0,0 +1,211 @@
# Span Gate Transit Schedule — Sova Transit District
**Ticket:** #336
**Author:** Miri
**Status:** canonical
**Last updated:** 2026-02-19
Reference: D-036 (setting), D-050 (Velen/Krenn), D-031 (time system)
---
## Purpose
The span gate transit schedule is the freight district's heartbeat. Every NPC routine in Sova Transit District is organized around the gate's rhythm. The ring's operational calendar is synchronized to maintenance windows and low-scrutiny transition periods. The detective's investigation timeline maps to freight movement patterns.
This document establishes the canonical schedule so NPC routines, ring operations, and investigation events are internally consistent.
---
## The Span Gate — Operational Notes
The span gate connecting Sova Station to Velen's freight depot is a local transit gate. It does not open a continuous passage — it cycles, processing transit events in discrete windows. Between cycles, the gate is in standby: the hum is present but lower, the transit ring is inactive, no cargo moves.
**Cycle time:** Each transit event takes approximately 4-6 minutes from gate activation to transit completion and reset. The gate is not available for another transit for approximately 12-15 minutes after the previous one (cooling, realignment, manifest logging).
**Daily capacity:** The gate can process approximately 40-50 transit events per standard day under normal operations. In practice, 30-35 are scheduled, leaving buffer for delays, priority transits, and maintenance.
**Direction:** The gate handles bidirectional transit in the same physical ring — one direction at a time. The schedule alternates blocks of Sova-to-Velen transits with blocks of Velen-to-Sova transits, with coordination managed by the Transit District logistics hub's Meridian scheduling system.
---
## Standard Daily Schedule
Station Sova operates on a 24-hour standard day cycle. Times are in station time (Krenn System Standard).
### Night Maintenance Window (0200-0500)
**Transit activity:** None. Gate offline.
The gate's automated maintenance cycle runs from 0200 to 0430. Diagnostic, realignment, calibration. Occasionally extended to 0500 if issues are flagged. The gate control room has a skeleton crew of two: one systems technician, one logistics coordinator.
**Why it matters for the ring:**
This is the ring's primary operational window. No cargo movement means no one is watching the floor. The maintenance crew is in the control room. The hub's day crew won't arrive until 0600. This 2-hour window (0200-0430) is when the ring moves product in or out of the staging areas where it will be processed in the morning. Product placed during the maintenance window appears to be standard freight waiting for morning processing.
**Why it matters for the detective:**
Any cargo that arrives in staging between the prior sign-off and morning shift start is "off-schedule" — it shouldn't be there. The detective's manifest analysis should flag containers that appear in staging without a corresponding late-shift transit event. This is one of the early investigative tells.
---
### Morning Startup (0500-0600)
**Transit activity:** None. Pre-shift preparation.
Gate comes back online at 0500 after maintenance. Diagnostics confirm operational status. Day shift logistics crew begins arriving 0545-0600. The first transits are scheduled to begin at 0630.
**NPC activity during this window:**
- Early arrivals (maintenance tech Drin, often Kael who has a habit of arriving early) are in the hub
- Break room coffee dispenser runs heavily from 0545 onward
- Ring members who placed product during the maintenance window have already left; the product sits in staging looking normal
---
### Morning Freight Wave (0600-1200)
**Transit activity:** Heavy. Approximately 12-15 transit events in this window.
The first major transit block of the day. Cargo moves from Velen to Sova Station (incoming) and from Sova Station to Velen (outgoing). The logistics hub is fully staffed. Manifest verification is active. Scanner bays are all running.
**Block 1 (0600-0800): Incoming priority freight**
- Inbound cargo from Velen: agricultural products, manufactured goods, supplies for station consumption
- High scrutiny: first transit block of the day gets more attention. Supervisors are fresh. Commission kiosk reports are reviewed.
- **Ring operations: avoid this window.** Contraband does not move on incoming high-scrutiny Block 1 unless the coordinator has a specific reason.
**Block 2 (0800-1000): Outbound priority freight**
- Outbound cargo from Sova: processed goods, manufactured components, commercial freight bound for Velen distribution
- Moderate scrutiny: the day has settled into rhythm. Supervisors are managing the floor rather than observing it.
- **Ring operations: low-priority. Possible for experienced operations, not standard.**
**Block 3 (1000-1200): Mixed transit, midday run**
- Mixed direction. Lower-priority commercial freight. Station consumption items.
- Standard scrutiny, lower volume.
- **Ring operations: possible. The midday run is the ring's secondary window — lower volume means less cover, but also less scrutiny. Used for smaller movements.**
---
### Midday Break (1200-1400)
**Transit activity:** Minimal. 1-2 scheduled transits; rest is buffer.
Lunch period. Logistics crew rotation. One shift overlaps with the next for the 1200-1400 block. The gate processes standing orders and scheduled maintenance transits during this period.
**Why it matters:**
This is a social observation window. The bar has lunch trade. Workers who've been on shift since 0600 are rotating through the break room or heading to the bar. The 1300-1400 period is particularly useful for the detective — this is when overheard conversations in the break room or bar are richest, as workers are discussing the morning's work.
---
### Afternoon Freight Wave (1400-1800)
**Transit activity:** Moderate. Approximately 10-12 transit events.
**Block 4 (1400-1600): Standard commercial transit**
- Routine freight. Mixed direction. No particular scrutiny focus.
- This is the most operationally neutral window — not high enough volume for chaos, not low enough scrutiny for easy movement.
- **Ring operations: standard. This block is used for moderate-volume operations when the morning maintenance window wasn't viable.**
**Block 5 (1600-1800): End-of-day priority freight**
- Higher-value or time-sensitive cargo that needs to reach its destination before end of business on Velen.
- Scanner scrutiny increases slightly for high-value designations.
- Shift change happens at 1600 and again at 1800 — overlapping crews, attention divided, handoff procedures absorbing focus.
- **Ring operations: the shift change exploit. The 1600 shift change is the ring's secondary operational window during active transit hours. Scanner coverage during handoff is the lowest it gets during a transit block. An experienced ring member can route a container through the right bay during the 15-minute shift handoff overlap.**
---
### Evening Lull (1800-2000)
**Transit activity:** Low. 2-3 transits. Primarily inbound station supplies.
The freight day is essentially over. Night shift has started. The hub has skeleton crew for monitoring and late transit coordination.
**NPC activity:**
- Most of the day shift is now in the bar or heading home.
- This is peak bar time: 1800-2200.
- The ring's members may gather in the bar, ostensibly socially, actually checking in on operational status through conversation.
---
### Night Freight Window (2000-0200)
**Transit activity:** Light. 4-6 scheduled transits, primarily overnight freight contracts.
Not nothing — the gate runs overnight. But volume is low, personnel is minimal, and scrutiny is diffuse.
**Block 6 (2000-2300): Overnight freight**
- Freight that needs to transit overnight for morning delivery on the other end.
- Two logistics workers on floor, one supervisor. One scanner bay running.
- **Ring operations: viable but visible. Less cover than maintenance window, but earlier timing. Used when the coordinator needs a movement that can't wait for the 0200 window.**
**Block 7 (2300-0200): Late night**
- Minimal. 1-2 transits, sometimes none. One logistics worker on floor.
- The quietest freight period before the maintenance window.
- **Ring operations: highest-risk standard window. Very few people around, but that cuts both ways — if anything irregular is noticed, there are fewer witnesses but also fewer people to provide cover.**
---
## Maintenance Schedule (Recurring)
Beyond the nightly maintenance window, the span gate has scheduled maintenance events.
### Weekly Extended Maintenance
**When:** Every 7 standard days, starting at 0000, duration 6-8 hours.
**Gate status:** Offline.
This is when significant maintenance work happens. The gate team (larger crew than the nightly skeleton) does calibration, component replacement, software updates, and anything that can't be done in the 2.5-hour nightly window.
**Why it matters:** The weekly maintenance is an extended window with reduced staff focused on technical work. The ring has historically used weekly maintenance for larger-volume movements, knowing that the unusual activity of maintenance crews makes the hub harder to monitor normally.
The weekly maintenance timing rotates — it's not always the same day of the week. This is standard practice to prevent predictability, but it means the ring has to know the schedule, which requires access to the maintenance planning documents. One of the ring's information assets is knowing the maintenance schedule in advance.
**For the detective:** Discovering that the ring has foreknowledge of maintenance windows is an investigative milestone. It implies an information source inside the logistics coordination system.
### Quarterly Commission Compliance Audit
**When:** Approximately every 90 days. Duration 2-3 days.
**Gate status:** Online, but with additional scrutiny.
Commission inspectors visit Sova Transit District approximately quarterly to review manifest records, scanner calibration logs, and customs compliance. During an audit, scanner scrutiny increases across all bays, Commission personnel observe loading and unloading, and manifest anomalies are reviewed directly rather than flagged for routine review.
**Why it matters:** The ring essentially pauses operations during a Commission audit. No product moves while inspectors are present. This 2-3 day pause is when ring members are most cautious about behavior, conversation, and associations.
The most recent audit was 8 weeks ago. The next is due in approximately 5-7 weeks. This timing matters: the detective has arrived into a window where the ring has been operating freely for 8 weeks post-audit and is 5-7 weeks from the next one. They're in their most active and potentially most careless period.
---
## The Ring's Operational Calendar (Derived)
From the transit schedule, the ring's typical operational pattern:
| Window | Frequency | Risk Level | Notes |
|---|---|---|---|
| Nightly maintenance (0200-0430) | Every night | Low | Primary window. Product staged for morning processing. |
| 1600 shift change | Opportunistic | Moderate | Used when small volumes need to move during active transit. |
| Weekly extended maintenance | Weekly | Low | Larger volumes. Requires advance schedule knowledge. |
| Midday run (1000-1200) | Irregular | Moderate-Low | Secondary option when morning window wasn't viable. |
| Overnight freight (2000-2300) | Irregular | Moderate | Used when coordinator needs faster turnaround than maintenance window allows. |
---
## NPC Routine Anchors
The transit schedule determines when NPCs are where. These are the anchors that NPC routine design (Paula + Gestalt) should use:
| Time | Hub NPCs | Bar NPCs | Maintenance NPCs |
|---|---|---|---|
| 0545-0600 | Kael (early), maintenance crew finishing | Empty or closing | Drin finishing night maintenance |
| 0600-0800 | Full day crew. Voss present. Ring members at normal posts. | Closed | Drin gone home |
| 1200-1300 | Rotation crew only. Most workers at lunch. | Light lunch trade beginning | Varies |
| 1600-1800 | Shift change chaos. Handoff period. | Starting to fill as early day-shift ends | - |
| 1800-2200 | Skeleton crew | Peak hours. Ring members present. Detector present. Social intelligence peak. | - |
| 2200-0200 | Night skeleton (2 workers) | Late regulars, card game nights | Night maintenance crew on call |
| 0200-0430 | Maintenance tech in control room only | Closed | Full maintenance crew |
---
## Monologue Anchors — Transit Events as Narrative Triggers
The span gate hum changes with transit events. These changes are monologue triggers:
| Hum State | Meaning | Smuggler Monologue Register | Detective Monologue Register |
|---|---|---|---|
| Baseline hum | Standby / normal | Background. Ignored. | Background. Logged. |
| Rising pitch (pre-transit) | Transit event beginning | Functional awareness: "Gate's spinning up." | Investigative note: "Transit in 3 minutes. What's going through?" |
| Peak/transit | Active transit | Operational: "There it goes." / Risk moment if contraband is moving. | Analysis: "Manifest says machine parts. Logged." |
| Settling post-transit | Transit complete | Relief if operational, neutral if not. | Follow-up: "Transit logged. Check against staging inventory." |
| Absence (maintenance) | Gate offline | Calibrated awareness of the window. | "Two hours until gate comes back online. Right. What is anyone doing at 0230 in the hub?" |
---
*The transit schedule is the district's skeleton. Every significant NPC routine moment, every operational window, every investigative milestone is anchored to gate activity. When the gate is busy, the district is busy. When the gate sleeps, the district's secrets move.*
*Miri — Sprint 12*
+239
View File
@@ -0,0 +1,239 @@
# Spatial Layout: The Last Shift (Bar)
**Ticket:** #312
**Date:** 2026-02-19
**Author:** Araminta (Visual Designer)
**Status:** v0.1 — wireframe quality, unblocks copy #191
**Grid:** 1 cell = 1m visual tile (visual grammar §2.1). Simulation operates at 0.5m; each visual tile = 2×2 sim tiles.
**Map area:** 28m wide × 22m deep (28×22 visual tiles) + 6m eastern extension (bathroom corridor)
**Zone palette:** Warm dark amber (visual grammar §1.3). Irregular layout is intentional — "functional warmth."
---
## Spatial Character
The Last Shift was converted from a maintenance staging bay roughly 15 years ago by Lera, who bought the decommissioned space and did most of the fit-out herself. The irregular east wall (where the bathroom corridor juts out as a later addition) and the uneven booth placement reflect Lera's accumulated decisions rather than an architect's plan. This is the design principle: **functional warmth** (D-043) means the bar looks settled, not designed.
The space has two distinct social temperature zones:
- **North half** (entry side): Higher visibility, news ticker, casual seating — the public face of the bar
- **South half** (bar counter and beyond): Lower visibility, card table, the back room — where the actual social work happens
---
## Floor Plan
```
N (district — main entrance from transit corridor)
|
1111111111222222222233
1234567890123456789012345678901
####[D]#############[D]###### row 01 <- main entrance (west) + emergency exit (east)
#[BB] . . . . [NT][NT] # row 02 <- NW corner booth + news ticker (NE)
#[BB] . . . . . . . . . . # row 03 ACCESS: PUBLIC
#[BB] . . . . . . . . . . . # row 04 <- corner booth, 3 rows deep, faces SE
# . . . . . . . . . . . . . # row 05
# [T] . . . [T] . . [T] . # row 06 <- scattered tables (T = table/chairs)
# . . . . . . . . . . . . . # row 07
# [T] . . . . . . . . [T] . # row 08
# . . . . . . . . . . . . . # row 09
# . . . . . [CT] . . . . . . # row 10 <- card table (CT) south-center
# . . . . . [CT] . . . . . . # row 11
####[BAR COUNTER]###########[D]#[D]## row 12 <- long bar counter; E door = BC entry; far E = alley
# . . SERVING AREA . . . . . # BC # row 13 <- bathroom corridor (east extension)
# . . . . . . . . . . . . . # [B]# row 14 ACCESS: SEMI-PRIVATE
# . . . . . . . . . . . . . # [B]# row 15 <- 2 bathroom stalls
# . . . . . . . . . . [T][T] # [D]# row 16 <- serving area tables; door to back room
################[D]############[BR]# row 17 <- back room door; back room east side
# BACK ROOM (STAFF ONLY) . # [D]# row 18 <- back room alley exit
# [T][LERA DESK] . . . . . . ###### row 19 ACCESS: PRIVATE
########[D]#################### row 20 <- alley door (south)
(alley = maintenance access, also secondary exit)
|
S (station maintenance alley — secondary exit route)
```
**Legend:**
```
# Wall (solid, blocks LOS and movement)
. Open walkable floor
[D] Doorway (traversable)
[BB] Corner booth seating (fixed furniture)
[T] Table with chairs (movable; typical seating cluster)
[CT] Card table (semi-permanent fixture; Torek's usual spot)
[NT] News ticker display (wall-mounted, east wall north side)
BC Bathroom corridor (narrow, east extension)
[B] Bathroom stall
[BR] Back room
```
---
## Zone Breakdown
### Zone 1 — Entry / North Public Area (rows 0105)
**Access tier:** Public
**Purpose:** The entry side. First impressions: warm amber light from the sconces Lera installed, the news ticker running at low volume on the east wall, the bar counter visible ahead. Most new arrivals pause here — it's the social reading zone where you assess who's in.
**NPC traffic:** Variable — busy at shift-change peaks, thin midday. Ren works the door informally (they're a regular who's made themselves useful to Lera, not official staff). Near the news ticker (east), a cluster often forms when something is on — planetary news, trade reports, sports events. This cluster is a natural eavesdropping point.
**LOS notes:** Open floor, full east-west LOS across the entry area. From the entrance you can see the corner booth (NW), the news ticker cluster (NE), and most of the scattered tables. You CANNOT see the card table from the entrance — it's south of the bar counter.
### Zone 2 — Corner Booth (NW, rows 0204)
**Access tier:** Public (any patron can sit here; regulars treat it as first-come territory)
**Purpose:** The key player observation position. Regulars know this booth sees everything — so it's informally "first-come" seating, and arriving early to claim it is a recognizable move. Sitting here signals you want to observe, not be observed.
**NPC traffic:** Occupied most evenings by one regular or another. The slot near the wall ([BB] row 04) is the deepest position — most concealed, best LOS. Torek occasionally sits here when the card table isn't running.
**LOS notes:** From the deepest corner of the booth (row 04, column 01-02):
- **ENTRANCE (row 01, center):** ✓ Direct LOS — you see everyone who walks in
- **News ticker cluster (row 02-03, east):** ✓ Direct LOS — you can read reactions
- **Bar counter (row 12):** ✓ Direct LOS — you see Lera and everyone at the bar
- **Card table (row 10-11, center):** ✓ LOS clears the open floor — you see the game
- **Back room door (row 17):** ✓ Partial LOS — you see movement to/from back room
- **Bathroom corridor entry (row 12, east):** ✓ you see who uses the secondary route
**This position is the investigation hub of The Last Shift.** A player who establishes themselves here has passive observation of all key NPC positions.
### Zone 3 — Scattered Tables (rows 0511, main floor)
**Access tier:** Public
**Purpose:** The general seating area. Tables are grouped loosely — Lera moved them over the years to accommodate different crowd sizes. There's no strict layout. Adjacency to the bar counter determines social temperature: tables at rows 0811 are the "settled in" zone; tables at rows 0507 are more transient (people who came for one drink).
**NPC traffic:** Distributed, varies by time. Kael sits at a table in the central zone when he comes in (not the booth — he doesn't want to be prominent, but can't help being readable to the smuggler PC who knows him).
**LOS notes:** The open floor gives most table positions LOS to the bar counter and to each other. The card table [CT] at rows 1011 is visible from the north (booth, tables, entry) but not from inside the bathroom corridor or back room.
### Zone 4 — Card Table (rows 1011, center-south)
**Access tier:** Public (nominally), but functionally semi-private
**Purpose:** The card game that runs most evenings. Torek organizes it. The participants shift but the table is a social fixture — it draws a ring of watchers even when not playing. The game itself is low-stakes (this is a low-income worker district), but the social dynamics are high-stakes: people talk during cards.
**NPC traffic:** Torek (Triangle 3) most evenings. Ren (Triangle 3) plays occasionally. Other regulars rotate in.
**LOS notes:** Visible from corner booth and most of the main floor. NOT visible from the entrance — the bar counter partially occludes it for someone just entering. This spatial ambiguity means a newcomer might not realize there's a card game until they commit to entering further. An NPC watching from the entrance can see the table only by moving to mid-room.
**Narrative note:** The card table is where Triangle 3 (Bar Tensions: Lera Torek Ren) runs hot. The physical proximity forces NPC spatial navigation around who's in the game and who's watching.
### Zone 5 — Bar Counter (row 12)
**Access tier:** Public (customer side) / Semi-private (Lera's side — serving area)
**Purpose:** The axis of the space. Lera is here most of the time she's working. The bar counter is the highest-visibility position in The Last Shift — every patron in the north half of the bar can see it, and Lera can see every patron. It's also the social routing point: people come to the bar to order, but stay to talk.
**NPC traffic:** Lera is nearly always here during open hours. The counter is a social magnet — NPCs drift to it when they want to be heard, or when they want to say something to Lera specifically.
**LOS notes:** From behind the bar (Lera's side): full LOS across the main floor, corner booth, card table, and entry area. Also: Lera can see the bathroom corridor door and the back room door from her position. She is the most observant NPC in the building by default.
**Key feature:** Lera's knowledge is encyclopedic within this space. She has seen every Triangle interaction that happened in this bar. Whether she shares that knowledge depends on her relationship to the player character.
### Zone 6 — Serving Area South (rows 1316)
**Access tier:** Semi-private (staff and card table adjacent)
**Purpose:** The back half of the bar's public area, behind the counter. Lower ambient light (fewer sconces). This is where patrons who want more privacy settle — the [T] tables at rows 1516 feel secluded even though they're not truly enclosed. Kael's secondary position (when he's not at the central tables and wants to avoid being too visible to Torek).
**NPC traffic:** Sparse to moderate. Lower traffic than the north half.
**LOS notes:** LOS back to bar counter through the serving gap. LOS to the bathroom corridor door. NOT visible from the entrance — someone entering from the north cannot see these tables without advancing to mid-room.
### Zone 7 — Bathroom Corridor (east extension, rows 1216)
**Access tier:** Semi-private (any patron, but narrow and away from main space)
**Purpose:** Functional corridor to the two bathroom stalls. Also: the secondary exit from the bar, accessible without crossing the main floor. NPCs use this corridor for private exchanges precisely because it's off the main visual axis — the bar counter and the corner booth don't have LOS into the corridor itself (they see the *door* but not the *interior*).
**NPC traffic:** Intermittent — quick in-and-out for bathrooms. Occasional lingering for private conversations.
**LOS notes:** The corridor interior is NOT visible from any position in the main bar. You see the door opening and closing; you see someone entering or exiting. What happens in the corridor is invisible. This is the **primary private conversation space in The Last Shift** — used by the ring for brief exchanges that can't happen at the bar or tables.
**Dead-drop location:** See §313 spatial layout.
### Zone 8 — Back Room (rows 1719)
**Access tier:** Private (Lera's staff space)
**Purpose:** Lera's personal office-storage hybrid. Her desk, supply storage, the alley door. She takes breaks here. Has the bar's financial records. Torek has been in here — Lera owes him something, or he knows something about her operation, or both (Triangle 3 detail for Paula to develop).
**NPC traffic:** Lera only, mostly. Torek has had access. No other patron should be back here.
**LOS notes:** No windows. The back room is visually isolated from the main bar. You see the door to the back room from the serving area (rows 1316) and from the corner booth. You see nothing from outside once the door is closed.
**Key feature:** The alley exit (south door, row 20) connects to the maintenance alley running along the south side of the district. This is a secondary exit from the bar that doesn't go through the main entrance — relevant to the smuggling corridor layout (#313).
---
## Key Observation Positions
| Position | Code | LOS coverage | Why it matters |
|----------|------|-------------|----------------|
| Corner booth (NW, deepest) | POS-B1 | Entrance, news ticker, bar, card table, back room door | Primary investigation post. Passive observation of all major social zones. |
| News ticker cluster (NE, row 02-03) | POS-B2 | Entry, corner booth, north tables | Natural social gathering point. Overhear conversations; be overheard. |
| Bar counter (customer side, row 12) | POS-B3 | Entire north floor, card table, back room door | Social hub. Lera is here; conversations happen to you at the bar. |
| Card table (as player/observer) | POS-B4 | Corner booth, bar counter, surrounding tables | Playing cards = legitimate reason to sit and watch for extended time. |
| Bathroom corridor entrance (row 12E) | POS-B5 | Who enters/exits corridor | Monitoring this door reveals if NPCs are meeting in the corridor. |
---
## Sightline Analysis
```
FROM → Booth(NW) Entry Bar Counter Card Tbl Serving S Bthrm Corr Back Rm
TO ↓
Corner booth SELF ✓ ✓ ✓ ✗ door only door
Entry area ✓ SELF ✓ ✗(bar occludes) ✗ door only ✗
Bar counter ✓ ✓ SELF ✓ ✓ door door
Card table ✓ ✗(bar) ✓ SELF ✓ ✗ ✗
Serving south ✗ ✗ ✓ (bar gap) ✓ SELF door door
Bathroom corr. door only door only door ✗ door SELF ✗
Back room door only ✗ door ✗ door ✗ SELF
✓ = direct LOS ✗ = wall blocks door = see the door but not interior
```
**Critical sightline: Corner booth → card table + back room door**
The booth is the only position that observes both the card table (Triangle 3 social space) and movement to/from the back room (private ring-adjacent activity). Establishing in the booth is the highest-leverage opening move in The Last Shift.
**Critical sightline gap: Bathroom corridor interior**
Nothing in the main bar has LOS into the corridor interior. This gap is exploitable — by NPCs for private exchanges, and by the player for unobserved movement if they need to reach the alley exit without crossing the main floor.
---
## Access Tier Map
```
PUBLIC SEMI-PRIVATE PRIVATE
────── ──────────── ───────
Entry area Serving south Back room
Corner booth Bathroom corr. (Lera + ring-adjacent)
Main tables
Card table
Bar counter (customer side)
```
The bathroom corridor is semi-private by convention rather than by lock — any patron can use it. But its ambiguity (near the bar exit, not directly observable) makes it functionally private for brief interactions.
---
## NPC Traffic Density Annotations
| Time | Entry | Main Tables | Corner Booth | Card Table | Bar Counter | Bthrm Corr | Back Room |
|------|-------|-------------|--------------|------------|-------------|------------|-----------|
| Dawn (05-07) | closed | — | — | — | Lera (prep) | — | Lera |
| Morning (07-10) | sparse | sparse | sparse | — | Lera | very sparse | — |
| Midday (10-14) | moderate | moderate | moderate | — | busy | sparse | — |
| Afternoon (14-17) | sparse | sparse | sparse | — | Lera | sparse | — |
| Shift change (17-20) | busy | busy | moderate (claimed) | sparse | busy | moderate | Lera/Torek |
| Evening peak (20-23) | busy | busy | occupied | busy | busy | moderate | varies |
| Late night (23-02) | sparse | sparse | sparse | winding down | Lera + last drinkers | sparse | varies |
| Close (02-05) | closed | — | — | — | Lera (cleanup) | — | Lera |
**Ring window:** Late night (23-01) or very early morning. The bathroom corridor and back room are quieter; fewer eyes on movement between zones. Lera's presence at close is the primary risk.
---
## Irregular Layout Notes (D-043 "Functional Warmth")
The following spatial imperfections are **intentional** and should be preserved in implementation:
1. **The corner booth is oversized relative to the space.** It takes up slightly more NW floor than a "designed" bar would allocate, because Lera built it first and designed around it.
2. **Tables are not in a grid.** They're in clusters that emerged from patron habits. The card table's [CT] position (center-south) was placed there because it was the right distance from the bar and the bathrooms.
3. **The bathroom corridor juts east** beyond the main bar footprint, creating the irregular eastern edge. It was a later addition — Lera broke through the original east wall.
4. **There is no stage or performance area.** The news ticker is the only media element. The bar doesn't put on entertainment; the patrons are the entertainment.
5. **Lighting is pooled, not even** (visual grammar §1.3, fixture radius 5-6 tiles). The card table and bathroom corridor door are in the transition zone between light pools — intentionally lower visibility.
---
## Narrative Triangle Service Notes
### Triangle 3 — Bar Tensions (Lera Torek Ren)
- **Spatial expression:** Lera at the bar counter (watching). Torek at the card table (running his social network). Ren between them (the triangle's instability node — closer to Torek emotionally, but Lera knows things about Ren that give her leverage).
- **Key positions:** Bar counter (Lera's domain), card table (Torek's domain), the booth (contested — whoever gets there first has the observation advantage).
- **Tension staging:** Torek going to the back room is the high-stakes move — it means he and Lera are talking privately. Ren watching that door from the main floor is the signal that Triangle 3 is hot.
### Triangle 4 — Worried Partner (Naia Kael Sera)
- **Kael in The Last Shift:** He comes in after long shifts, sits at the central tables (not the booth — he doesn't want to be seen watching). He's a quiet drinker when here. His late nights are visible to other regulars.
- **Spatial tension:** Kael sitting at the table while others observe him (Lera noting his pattern, Naia's concern expressed to Sera elsewhere) is a spatial staging problem — how do you show worry through placement? The answer is: Kael always sits in the same spot, at the same time. Pattern = visible.
---
## Notes for Copy Team (#191 — Bar Content Pack)
1. **The corner booth is "the watching spot."** Regulars know it. Anyone sitting there is understood to be observing. NPC reactions to player claiming the booth should reflect this — Lera might comment. Ren might avoid eye contact.
2. **News ticker is a topic generator.** Whatever is running on the ticker is a natural conversation opener. Mellanie should specify what's playing on different days (planetary news, trade prices, sports) — it gives NPCs something to react to that grounds the scene.
3. **The bar counter is Lera's domain.** Dialogue at the counter is different from dialogue at a table — you're in Lera's space. She controls the pace, she can end the conversation by moving away to serve someone else. Write Lera's bar counter lines accordingly.
4. **Bathroom corridor conversations are brief and tense.** No ambient to cover prolonged conversation. The setting is: narrow, low light, the sound of ventilation. Any dialogue here should be 2-3 exchanges maximum.
5. **The card table has its own social grammar.** Asking to watch (spectate) is different from asking to play. Being asked to deal is a trust signal. Mellanie should differentiate these as distinct dialogue situations.
6. **Ambient detail — Lera's accumulated decisions:** Signs she's placed over the years (no tab system, specific house rules, a framed maintenance permit from when the corridor was added). These are environmental text opportunities (visual grammar §5.2, 11px at 70% opacity, z-layer 2-4).
@@ -0,0 +1,310 @@
# Spatial Layout: Smuggling Spaces and Transition Corridors
**Ticket:** #313
**Date:** 2026-02-19
**Author:** Araminta (Visual Designer)
**Status:** v0.1 — wireframe quality, unblocks copy #192
**Grid:** 1 cell = 1m visual tile. Transition corridor is shown at reduced scale (1 cell = 2m) due to 40m length.
**Zone palette:** Neutral dark, dim irregular lighting (visual grammar §1.4, Zone 3 — Transition Corridors)
**Cross-reference:** This document overlays on #311 (Terminal layout) and #312 (Bar layout). Read both before this document.
---
## Design Principle: Invisible Infrastructure
The smuggling ring has no dedicated space. This is the core spatial insight: **every ring location serves a mundane purpose, and the criminal function is only apparent if you know what to look for.** The player (smuggler PC) knows. The detective PC can learn. An NPC observing in isolation sees nothing unusual.
This means the spatial layouts here are **interpretive layers on top of existing spaces**, not new spaces. The restricted storage is officially overflow cargo. The maintenance corridors are officially maintenance corridors. The dead-drops are officially: a locker, a junction box, a shelf marker.
**Same physical space. Different player understanding.**
---
## Overview: The Ring's Operational Footprint
The smuggling operation spans four categories of space:
| Category | Location | Function | Discovery vector |
|----------|----------|----------|-----------------|
| **Restricted storage** | Terminal, south zone (S of cargo floor) | Primary staging — contraband lives here | Restricted access; manifest discrepancy; visual observation of access pattern |
| **Maintenance corridors** | Behind both major buildings; east service spine | Transit — ring members move goods and exchange information here without crossing public zones | Physical discovery (hatch access); sound detection (movement behind walls); NPC behavior pattern |
| **Dead-drops** | 3 fixed locations (see §3) | Information and small-item exchange; no person-to-person contact | Insert observation; pattern recognition; monologue flagging |
| **Transition corridor** | ~40m link between Terminal and bar district | Physical transit; unavoidable chokepoint for ring members moving between locations | Observation of NPC pattern; insert tracking; time-of-day analysis |
---
## 1. Restricted Storage (Embedded in Terminal Layout)
**Location:** Terminal, rows 2730 (south of cargo floor). Access via single coded door from cargo floor.
```
From Terminal layout (#311), zoomed:
[C][C][C][C][C] <- cargo floor (rows 24-26)
|
[D](CODED) <- row 26, center — the access door
|
############################# row 27
# RESTRICTED STORAGE # row 28
# [RS][RS][RS][RS][RS][RS] # row 29 <- Rack A (east side)
# . . . . . . . . . . . . # row 30 <- staging floor (center)
# [RS][RS][RS][RS][RS][RS] # row 31 <- Rack B (west side)
# [DD-1] # row 32 <- DEAD-DROP LOCATION 1 (see §3)
############################# row 33
```
**Official function:** High-value or flagged cargo overflow. Access restricted to authorized hub personnel.
**Ring function:** Primary staging point. Contraband enters as mislabeled standard cargo, moves here under cover of a legitimate manifest entry for the rack slot.
**LOS exposure:** The coded entry door is visible from the cargo floor AND from the supervisor's window (see Terminal sightline analysis). Any approach to the door can be observed by Voss if he's at his desk. The door itself has no window — once inside, you're not visible from outside.
**Operational risk:** The access code is held by Voss and a small list of authorized personnel. A ring member with legitimate access (one of them has a work reason to occasionally enter) must make the access pattern look normal. Frequency, timing, and duration all create anomalies detectable by the detective's analytical lattice.
---
## 2. Maintenance Corridors
The maintenance corridors are the ring's secondary movement network. They are the spatial infrastructure that makes the operation functionally invisible: a ring member moving goods from the Terminal to the bar district via the public transit corridor is observable. Via the maintenance corridor, they're not.
### 2.1 Corridor Network Map (schematic, not to visual-grid scale)
```
[TERMINAL BUILDING]
|
M-HATCH-T (maintenance hatch, east wall of restricted storage)
|
EAST SERVICE SPINE (maintenance corridor)
| <- narrow, 2m wide, dim lighting, Era 1 original construction
|
JUNCTION-1 (4-way junction: terminal, span gate service, district spine, Velen depot access)
|
DISTRICT SPINE (maintenance corridor)
| <- runs parallel to transit corridor, ~40m, grating floor
|
M-HATCH-B (maintenance hatch, north wall of bar's bathroom corridor)
|
[BAR BUILDING]
```
### 2.2 Maintenance Corridor Layout (section view)
```
Scale: 1 cell = 1m visual tile. Corridor width: 2m (2 cells).
N = Terminal side. S = Bar side.
## M-HATCH-T ## <- entry from restricted storage (locked hatch)
## . . . . . . ## row 01 <- 2m wide; grating floor
## . . . . . . ## row 02 ACCESS: PRIVATE
## . . [JB-1]. ## row 03 <- [JB-1] = junction box (DEAD-DROP 2, see §3)
## . . . . . . ## row 04
## [JUNC-1] ## row 05 <- 4-way junction. Branching maintained here.
## . . . . . . ## row 06
## . . . . . . ## row 07
## . . [SH-1]. ## row 08 <- [SH-1] = shelf marker (DEAD-DROP 3, see §3)
## . . . . . . ## row 09
## . . . . . . ## row 10
## . . . . . . ## row 11
## M-HATCH-B ## <- exit into bar's bathroom corridor (locked hatch, key-based)
(Grating underfoot. Dim cold-white maintenance-grade lighting. D-046 reference:
maintenance lighting is sparse, uneven; gaps between light pools are wider.)
```
**Official function:** Station infrastructure maintenance access. Utility routing (power, climate, comms).
**Ring function:** Personnel transit. Small-item movement (packages that fit in a standard duffel). Not large cargo — restricted storage-to-terminal is the cargo staging point; the corridor handles distribution of smaller items for personal delivery.
**LOS exposure:** Zero. No windows. No cameras (era 1 construction, never upgraded). The only way to know someone is in the maintenance corridor is: (a) hear them (footsteps carry differently on grating — detectable as distinct sound signature from outside if you're listening in an adjacent space), or (b) witness them entering or exiting a hatch.
**Hatch visibility:**
- M-HATCH-T opens inside restricted storage. Access visible only to someone in restricted storage.
- M-HATCH-B opens inside the bathroom corridor. Access visible only to someone in the bathroom corridor at that moment.
**Critical insight for investigation:** The detective cannot observe the maintenance corridor interior without entering it. All evidence is inferential: hearing footsteps behind walls, observing the hatch-T entry from restricted storage, or timing Kael's movements to when maintenance corridor transit would explain his presence pattern.
---
## 3. Dead-Drop Locations
Three fixed dead-drops in v0.1. Each serves as an information-only exchange point — no in-person meeting required. Ring members leave items or encoded data; ring members retrieve them. There is no face-to-face contact at a dead-drop by design.
### Dead-Drop 1 — "The Shelf" (Restricted Storage, Terminal)
**Location:** Restricted storage, south wall, specific rack position (row 32, center of south wall as marked in §1 layout).
**Mundane appearance:** A standard cargo rack position. A specific physical marker (a cargo tag with a non-standard orientation) signals to ring members that something is there.
**Ring use:** Small items. Encoded instructions. The only dead-drop that requires access to restricted storage — the highest-security location.
**Discovery vector:** The detective with the analytical lattice can flag an anomalous tag orientation as "pattern mismatch" if they've spent time in the storage area. Monologue: *"That tag's rotated 90 degrees. The others aren't. Could be nothing."* Knowledge graph flag: `Suspects` confidence on "dead-drop usage" topic.
**NPC who uses it:** Kael (delivery side). Voss has visual access to the room but doesn't know what to look for in the rack (he's not ring-aware at this level).
### Dead-Drop 2 — "The Box" (Maintenance Corridor, Junction 1 area)
**Location:** Junction box JB-1 on the corridor east wall, row 03 of the maintenance corridor section (roughly 15m from the Terminal hatch).
**Mundane appearance:** A standard maintenance utility junction. These boxes line every maintenance corridor on Station Sova — this one has a specific compartment that's not documented in the station maintenance schema.
**Ring use:** Information transfer. Small data chips. No physical bulk.
**Discovery vector:** Physical discovery (the detective finds this junction box while traversing the corridor; insert observation flags "this junction is not in the station maintenance schema"). Or: sound model approach — close-range audio places the player near the box when a ring member accesses it.
**NPC who uses it:** Drin (Triangle 2 — the connection between Drin's worried knowledge and the ring's operation runs through what Drin knows about this drop location).
### Dead-Drop 3 — "The Mark" (Maintenance Corridor, District Spine)
**Location:** Shelf marker SH-1, row 08 of the maintenance corridor section (roughly 28m from Terminal hatch, 12m from bar hatch). A small painted mark at eye-height on the west wall, positioned at a shelf bracket that holds nothing.
**Mundane appearance:** A maintenance alignment mark. Dozens exist in maintenance corridors for spatial orientation. This one has a secondary mark (a barely visible secondary line) that ring members read as a go/no-go signal.
**Ring use:** Status signal. "Go" = operation is running tonight. "No-go" = abort, someone is watching. Changed by the ring coordinator. No item exchange.
**Discovery vector:** Monologue (the smuggler PC notices the mark and recognizes its meaning; the detective PC sees it and can't read it — but the insert can flag "mark has been altered in the past N hours" if they have lattice scanning active). This is an information asymmetry showcase: the smuggler reads meaning; the detective reads only an anomaly.
**NPC who uses it:** The ring coordinator (unnamed, off-stage in v0.1). Kael and Drin are users but not setters of this signal.
---
## 4. Transition Corridor
The ~40m corridor connecting the Terminal district to the bar district is the ring's unavoidable exposure zone. There is no maintenance corridor equivalent for this stretch — the public transit corridor is the only route between the two buildings.
### 4.1 Corridor Layout
```
Scale: 1 cell = 2m (corridor is ~40m; shown at half scale = 20 cells long).
Width: 6m wide (3 cells at this scale).
[TERMINAL DISTRICT]
|
↓ south
############# T = 0m
# . . . . . #
# . . . . . # <- Zone A: Terminal-side (near scanner bay exit)
# . . . . . #
# . . . . . #
#####[D]##### T = 10m <- Service alcove (maintenance access door, one side)
# . . . . . #
# [BENCH] # <- seating alcove (mid-corridor), Zone B: mid-corridor
# . . . . . #
# . . . . . #
# . . . . . # T = 20m
# . . . . . #
#####[SV]#### T = 24m <- Surveillance camera (station security, visible)
# . . . . . #
# . . . . . # <- Zone C: bar-side
# . . . . . #
# . . . [NT]# T = 32m <- News ticker relay (same feed as bar's ticker)
# . . . . . #
# . . . . . #
# . . . . . # T = 40m
|
↓ south
[BAR DISTRICT]
```
### 4.2 Zone Breakdown
| Zone | Distance | Character | Notes |
|------|----------|-----------|-------|
| Zone A (Terminal side) | 012m | Industrial. Well-lit functional lighting. Near scanner bay traffic. | NPCs emerging from Terminal work zone pass through here. |
| Mid-corridor | 1228m | Transitional. Seating alcove used by workers waiting for shift transport. | The camera at T=24m covers this stretch. |
| Zone C (Bar side) | 2840m | Warmer ambient at far end as bar lighting bleeds around the corner. | Most relaxed zone — "off shift" feeling begins here. |
### 4.3 The Surveillance Camera
The station security camera at T=24m is a **key plot element**. It is:
- **Visible to the player** (rendered as a mounted fixture, z-layer 4)
- **Known to the ring** — ring members do not exchange goods or signals in this corridor
- **Accessible to the detective** — with the right lattice query, the detective can pull the camera feed (perception mode: camera feeds, D-017)
- **Gameable** — camera feeds can be looped or spoofed. The ring doesn't currently loop this camera (that would itself be an anomaly), but the possibility exists.
**Investigation note:** The camera's timestamp data can establish when ring members used this corridor. Pattern analysis across days reveals the ring's operational schedule. This is the detective's primary hard evidence vector for the transition corridor.
### 4.4 Traffic Density
```
TERMINAL → BAR direction (south) BAR → TERMINAL direction (north)
Dawn (05-07) very sparse very sparse
Morning shift (07-09) busy (workers leaving Terminal) sparse
Mid-morning (09-12) moderate moderate
Lunch (12-13) moderate (bar-going) moderate
Afternoon (13-17) sparse sparse
Evening shift (17-19) busy (shift end) busy (new arrivals)
Evening social (19-23) moderate (bar-goers) moderate (early leavers)
Late night (23-02) sparse sparse
Deep night (02-05) VERY SPARSE VERY SPARSE
```
**Ring window:** Deep night (23-02) southbound, for ring members heading to bar-side handoffs. Very sparse traffic means a ring member in the corridor is conspicuous if observed by anyone else. The camera cannot be avoided.
**Investigative implication:** At night, the corridor is so sparse that any NPC on camera is worth noting. The detective PC who reviews camera logs from deep night windows has a short list of suspects.
---
## 5. Spatial Overlay: Ring Operation Flow
How a standard shipment moves through the spatial system:
```
Inbound cargo (arrives via span gate)
[Scanner Bay B — Kael scans and clears (SB-B, Terminal)]
↓ manifest entry routes contraband to restricted storage slot
[Restricted Storage — cargo staged (Terminal, rows 27-30)]
↓ when clear of Voss + ring-safe timing
[Dead-Drop 1 confirms go-signal (restricted storage, south wall)]
↓ maintenance hatch access
[Maintenance Corridor — ring carrier moves item (M-HATCH-T exit)]
↓ checks go/no-go signal
[Dead-Drop 3 — "The Mark" read mid-corridor]
↓ if GO:
[Maintenance Corridor continues to bar side (M-HATCH-B entry)]
↓ enters bathroom corridor
[Bathroom Corridor — brief transit through bar (Last Shift east zone)]
[Back Room (Lera's space) — item transfer or staging for buyer pickup]
```
**What makes this invisible:** Each step has a mundane explanation:
- Kael scanning cargo = job
- Restricted storage access = authorized worker entering authorized space
- Someone in the maintenance corridor = maintenance worker
- Someone in the bathroom corridor = using the bathroom
- Someone in the back room = talking to Lera
Only the full pattern, observed across multiple instances, reveals the operation. No single observation is suspicious. This is asymmetric information as a spatial design.
---
## 6. Discovery Path Design
For investigation design purposes, the spatial layout supports two discovery approaches for the detective PC:
### Path A — Pattern Recognition (insert-heavy)
1. Corridor camera logs → Kael's deep-night transit pattern established
2. Manifest database access (Terminal, manifest processing) → anomalous cargo entries flagged
3. Restricted storage access pattern (timing) → anomalous frequency established
4. Full picture: regular person, regular location, abnormal schedule = ring member
### Path B — Physical Traversal (exploration-heavy)
1. Break room conversation → floor worker mentions Kael's odd hours
2. Physical discovery of maintenance hatch (via break room adjacent) → access discovered
3. Maintenance corridor traversal → Dead-Drop 2 or 3 discovered
4. Dead-Drop 3 mark analysis → insert flags "recently altered"
5. Full picture: someone is using these corridors with intent
**Both paths are equally valid.** The detective PC's analytical lattice makes Path A faster; a detective playing with insert-off or minimal lattice activation discovers Path B organically. Neither path requires confrontation before late investigation stages.
---
## Dead-Drop Summary Table
| Drop | Location | Container type | Ring function | Discovery route |
|------|----------|---------------|---------------|-----------------|
| DD-1 — "The Shelf" | Restricted storage, south rack | Cargo tag (physical marker) | Small item / instruction transfer | Restricted storage access + insert pattern flag |
| DD-2 — "The Box" | Maintenance corridor, near Terminal hatch | Junction box compartment | Data chip / information transfer | Physical discovery in corridor; or sound proximity |
| DD-3 — "The Mark" | Maintenance corridor, mid-run (28m) | Wall mark (go/no-go signal) | Status signal — operation go/no-go | Smuggler: reads meaning. Detective: insert flags alteration timestamp. |
---
## Notes for Copy Team (#192 — Smuggling Ring Content Pack)
1. **Ring communications are never explicit.** NPCs don't say "this is a dead-drop" or "the operation moves tonight." The ring's operational knowledge is encoded spatially (the mark) and physically (the drop). Dialogue about ring operations is oblique — NPC lines should use the same mundane vocabulary: "the cargo," "the back section," "the late run."
2. **The maintenance corridor has sensory character.** Era 1 construction: grating underfoot (distinct resonant sound per D-074), cold maintenance-grade lighting with longer gaps between pools, the hum of station infrastructure (power conduits, climate ducting). Monologue lines set in the corridor should use sensory grounding — this is physically different from the Terminal or bar.
3. **Dead-Drop 3 is the most narratively rich.** It's the one where the two player characters have asymmetric understanding. The smuggler PC reads the mark and gets information; the detective PC sees an anomaly and must reason. Mellanie's lines for this location should fork hard on player character: smuggler lines are direct ("Go. Tonight."), detective lines are interpretive ("That secondary mark. Something changed it recently.").
4. **The transition corridor is a tonal between-space.** Moving through it is moving between two social temperatures — the Terminal's institutional cool to the bar's warm amber. Mellanie's monologue lines for corridor transit should reflect this transition: leaving one context, entering another, the 40m walk as a beat of reflection.
5. **Voss's blind spot is spatial.** His window looks south into the cargo floor. He cannot see the main corridor or the maintenance hatch. This spatial fact — not a character flaw, not a relationship — is what makes the ring's operation feasible. The player who understands the building layout understands why Kael chose this job.
6. **The camera in the transition corridor is the ring's known constraint.** Ring members know it's there and operate around it. Any NPC dialogue about moving goods or meeting times will contain an implicit reference to timing around the camera window — again, oblique. "We're better early" = "before the camera maintenance window." Mellanie should build this into Kael's dialogue without making it explicit.
+226
View File
@@ -0,0 +1,226 @@
# Spatial Layout: The Terminal (Sova Logistics Hub)
**Ticket:** #311
**Date:** 2026-02-19
**Author:** Araminta (Visual Designer)
**Status:** v0.1 — wireframe quality, unblocks copy #190
**Grid:** 1 cell = 1m visual tile (visual grammar §2.1). Simulation operates at 0.5m; each visual tile = 2×2 sim tiles.
**Map area:** 44m wide × 28m deep (44×28 visual tiles)
**Zone palette:** Cool grey-navy institutional (visual grammar §1.2)
---
## Floor Plan
```
N (district entry — public side)
|
1111111111222222222233333333334444444
1234567890123456789012345678901234567890123456
#############[D]##########[D]############# row 01 <- entry facade
# # row 02
# ENTRY LOBBY # row 03 ACCESS: PUBLIC
# . . . . . . . . . . . # row 04
####[D]########[D]########[D]############# row 05 <- scanner bay entry doors
# SB-A # SB-B # SB-C # row 06
# [T] . # [T] . # [T] . . . . # row 07 ACCESS: PUBLIC (monitored)
# . . . . # . . . . # . . . . . . . # row 08
####[D]####[D]########[D]####[D]########### row 09 <- into main corridor
# . . . . . . . . MAIN CORRIDOR . . . .# row 10 CHOKEPOINT (see §5)
# . . . . . . . . . . . . . . . . . . .# row 11
####[D]############[D]#########[D]######### row 12 <- into work zones
# MANIFEST # CARGO FLOOR # SUPVR # row 13
# PROC. # # OFFICE # row 14 ACCESS: SEMI-PRIVATE / PRIVATE
# [T][T][T] # [C][C] [C][C] # [=][=]=# row 15 <- SO window onto cargo floor
# [T][T][T] # # [=][=]=# row 16
# [T] # [C][C] [C][C] # [T] # row 17
# # # [DESK] # row 18
# [T] # [C][C] [C][C] # . [D] # row 19 <- SO door to cargo floor
# . . . . . # ######## # row 20
########[D]## . . . . . . . . # row 21 <- break room door
# BREAK RM # [C][C] [C][C] # row 22 ACCESS: SEMI-PRIVATE
# [T][ ] # # row 23
# [ ][ ] # [C][C] [C][C] # row 24
# [ ] # ^ # row 25 <- ^ = restricted storage door (locked)
# . . . . # #########[D]########### # row 26
########### # # RESTRICTED STORAGE # # row 27 ACCESS: PRIVATE
# # [RS][RS][RS][RS] # # row 28
# # [RS][RS][RS][RS] # # row 29
# ###################### # row 30
############################## row 31
|
S (maintenance access, back-of-station)
```
**Legend:**
```
# Wall (solid, blocks LOS and movement)
. Open walkable floor
[D] Doorway (traversable, no door sprite in v0.1)
[=]= Window (transparent wall — LOS passes through, movement blocked)
[T] Terminal / workstation
[C] Cargo container (furniture-scale object, blocks LOS at z-layer 2)
[ ] Table / chair / seating
SB-x Scanner bay x (inbound/outbound cargo scanning station)
SUPVR Supervisor office
MFST Manifest processing
RS Restricted storage rack
```
---
## Zone Breakdown
### Zone 1 — Entry Lobby (rows 0105)
**Access tier:** Public
**Purpose:** The public-facing entry into the logistics hub. Anyone with business on Sova Transit can enter here. Cargo and workers flow through the scanner bays; visitors wait or pass through.
**NPC traffic:** Busy on shift change (morning, evening), moderate midday, sparse nights.
**LOS notes:** Open area. Full LOS across lobby. No cover.
**Key feature:** From the entry lobby you can observe which scanner bay is active and who is being scanned. But you can't see the main corridor from here — the scanner bays break sightlines.
### Zone 2 — Scanner Bays (SB-A, SB-B, SB-C) (rows 0609)
**Access tier:** Public (monitored)
**Purpose:** All cargo entering or leaving the hub passes through a scanner bay. Kael is stationed at SB-B (primary inbound cargo, the busiest channel). SB-A and SB-C handle outbound and overflow.
**NPC traffic:** Continuous flow during work hours; Kael has no reason to leave the bay unless break or errand.
**LOS notes:** Each bay is separated by wall partitions. From SB-B you can see into SB-A and SB-C only through the shared doorway at row 09 into the main corridor. Workers in scanner bays have NO LOS to the cargo floor or supervisor office.
**Key feature:** The scan terminals have records of all cargo passing through (manifest discrepancies are visible here). Investigation entry point.
### Zone 3 — Main Corridor (rows 1012) — THE CHOKEPOINT
**Access tier:** Semi-public (worker access expected, visitors uncommon)
**Purpose:** The east-west throughway connecting scanner bays (north) to all back-of-house zones (south). Every worker moving between entry and the work area passes through this corridor. Every NPC going to the break room, manifest processing, cargo floor, or supervisor office crosses here.
**NPC traffic:** Moderate-busy during work hours; near-zero during nights or off-shift periods.
**LOS notes:** FULL EAST-WEST LOS. From any position in the corridor you can observe all four south-facing doorways (manifest processing, cargo floor west, cargo floor east, supervisor's corridor). You can ALSO see through the supervisor's windows [=] into the office if positioned in the east section of the corridor. This is the single highest-information position in The Terminal.
**Investigation note:** Standing in the main corridor and doing nothing is suspicious. The player needs a cover reason (idle near a crate, appear to be consulting an insert map, Sneak stance). Voss's office window faces south into the cargo floor, not north — he cannot see the main corridor. This gives the corridor a surveillance gap relative to authority.
### Zone 4 — Manifest Processing (rows 1326, west side)
**Access tier:** Semi-private (workers only, no public reason to enter)
**Purpose:** Where manifests are processed, discrepancies flagged, and cargo logs maintained. Maret works here. Drin passes through occasionally (their role puts them in contact with Maret — Triangle 2 tension originates in their shared knowledge of a manifest irregularity).
**NPC traffic:** Low foot traffic. Maret is usually here during work hours. Drin visits intermittently. Rare for floor workers to enter.
**LOS notes:** Door at row 12 opens onto the main corridor. From the manifest processing area, you can see into the main corridor when the door is open, but NOT into the cargo floor (solid wall partition). The [T] terminals along the east wall face the partition — Maret works with her back to the corridor, visible from the door, unaware of who's watching from the corridor.
**Key feature:** Manifest processing has access to the full cargo database. A player who reaches a terminal here can surface manifest discrepancies that are invisible from the scanner bays.
### Zone 5 — Cargo Floor (rows 1330, center)
**Access tier:** Semi-private (workers only)
**Purpose:** Where freight is staged, sorted, and held before redistribution. Kael, Nils, and other floor workers spend most of their shift here. [C] blocks represent standard cargo container staging racks.
**NPC traffic:** Busy during work hours. Workers move between container rows. Moderate on breaks. Supervisor spot-checks from the door or window (rows 19 and 15-16).
**LOS notes:** The cargo containers [C] create partial cover. A player or NPC crouching behind a container stack has concealment from the supervisor's window. However, the container layout leaves clear aisles — movement through the floor is visible if Voss is watching. The restricted storage door (row 26, center) is visible from the cargo floor and from the supervisor's window.
**Key feature:** The restricted storage entrance is at the back of the cargo floor. Reaching it from the cargo floor is observable from the supervisor's window. Any unauthorized access attempt is visible to Voss unless the player ensures Voss is away from his desk (or the player uses the break room timing window, during which floor traffic decreases).
### Zone 6 — Supervisor's Office (rows 1320, east side)
**Access tier:** Private (Voss only, or by invitation/authority)
**Purpose:** Voss manages the hub from here. The [=] windows (rows 15-16) give him a direct visual line across the cargo floor. His desk faces the window — he works while watching.
**NPC traffic:** Voss is here most of the shift. He exits occasionally to walk the floor (social/authority move) or to attend to something in manifest processing. The door at row 19 opens directly to the cargo floor.
**LOS notes:** From the supervisor's window, Voss has LOS across the entire cargo floor including the restricted storage door. He CANNOT see: the main corridor (windows face south/west into cargo floor only), the break room, or the manifest processing interior. His blind spots are deliberate — the floor is his domain, administration is not.
**Key feature:** The supervisor's window is two-way. Voss can see the cargo floor; cargo floor workers can see whether Voss is at his desk. This creates behavioral signals: workers (including Kael) act differently when the office light is on. Insert observation from the main corridor can detect whether Voss is watching.
### Zone 7 — Break Room (rows 2226, west side)
**Access tier:** Semi-private (workers only; informal, unwatched)
**Purpose:** The off-shift space. Workers eat here, complain here, and sometimes say things they wouldn't say on the floor. Informal social hub. No surveillance. Voss never enters (it would kill the informal atmosphere he relies on — floor workers stop talking candidly when management appears).
**NPC traffic:** High during break periods, near-zero during work hours. Key NPC conversations happen here.
**LOS notes:** No windows. Solid walls on all sides. Completely visually isolated from cargo floor and corridor. A player inside the break room knows nothing about what's happening on the floor. **This is the only space in The Terminal where NPCs will say things they wouldn't say anywhere else.**
**Dead-drop location:** See §313 spatial layout.
### Zone 8 — Restricted Storage (rows 2730, center-south)
**Access tier:** Private (access-coded, ring members only)
**Purpose:** Officially: overflow storage for high-value or flagged cargo. In practice: the staging point for contraband (unlicensed lattice components, D-037). Access requires a code that only certain personnel have.
**NPC traffic:** Very sparse. A legitimate visit from Voss is possible but rare. Ring members visit in small windows of low supervisor attention.
**LOS notes:** The single access door is visible from the cargo floor and from the supervisor's window. There is no back exit. Anyone entering restricted storage can be observed doing so.
**Key feature:** The chokepoint for the smuggling arc. The ring has to move cargo through this room at some point. The detect/observe/intervene loop for the detective character runs through this door.
---
## Key Observation Positions
| Position | Code | LOS coverage | Why it matters |
|----------|------|-------------|----------------|
| Main corridor (center) | POS-1 | All four south-facing doors; SO windows [=] | Highest-information position. Every NPC in the Terminal passes here. |
| Scanner bay B door (row 09, center) | POS-2 | Main corridor + partial SB-A and SB-C | Kael's natural position. He can observe corridor traffic while appearing to work. |
| Behind cargo containers (row 20-21, center) | POS-3 | Restricted storage door; SO window side | Concealed position. Can observe restricted storage access without being in open floor. |
| Break room door (row 21, west) | POS-4 | Main corridor through door; break room interior | Transitional. Can eavesdrop without fully entering. |
| Manifest processing door (row 12, west) | POS-5 | Main corridor; manifest processing interior | Observe Maret's terminal activity from corridor without entering. |
---
## Sightline Analysis
```
FROM → Corridor Cargo Fl. Supvr Off. Manifest Break Rm Restr.Stor.
TO ↓
Main corridor SELF via door NO (SO→S) via door via door NO
Cargo floor via door SELF [=] window NO wall NO wall via door
Supervisor off. NO (faces S) [=] SELF NO NO [=] partial
Manifest proc. via door NO wall NO SELF NO NO
Break room via door NO wall NO NO SELF NO
Restr. storage NO via door [=] partial NO NO SELF
[=] = line of sight through window (one-way for gameplay unless player enters SO)
"NO wall" = wall blocks LOS
"via door" = LOS exists when door is open (all doors open in v0.1)
```
**Critical sightline: Supervisor window → restricted storage door**
Voss can see every approach to restricted storage from his desk. This is the core spatial tension of the smuggling arc. The ring must work around this observation.
---
## Access Tier Map
```
PUBLIC SEMI-PUBLIC SEMI-PRIVATE PRIVATE
────── ─────────── ──────────── ───────
Entry lobby Main corridor Cargo floor Supervisor office
Scanner bays (anyone with Manifest proc. Restricted storage
(monitored) worker reason Break room
to be there)
```
Movement between tiers is possible but increasingly flagged:
- Public → semi-public: unremarkable
- Semi-public → semi-private: player needs walk reason (worker NPC → trivial, detective with badge → possible, smuggler → requires familiarity with someone inside)
- Semi-private → private: requires access code or authority (Voss's invitation)
---
## NPC Traffic Density Annotations
| Time | Entry Lobby | Scanner Bays | Main Corridor | Cargo Floor | Break Room | Restricted |
|------|------------|-------------|---------------|-------------|------------|------------|
| Dawn (05-07) | sparse | sparse | sparse | sparse | sparse | very sparse |
| Morning shift change (07-09) | busy | busy | busy | busy | sparse | very sparse |
| Mid-morning (09-12) | moderate | moderate | moderate | busy | sparse | very sparse |
| Lunch break (12-13) | sparse | sparse | sparse | sparse | busy | very sparse |
| Afternoon (13-17) | moderate | moderate | moderate | busy | sparse | very sparse |
| Evening shift change (17-19) | busy | moderate | busy | moderate | moderate | sparse |
| Night (19-23) | sparse | sparse | sparse | sparse | sparse | sparse |
| Deep night (23-05) | very sparse | very sparse | very sparse | very sparse | very sparse | rare |
**Ring window:** Deep night (23-02). Traffic on the cargo floor is very sparse. Restricted storage access is least likely to be observed by non-ring personnel. Voss is off-shift (office dark). The main corridor is nearly empty.
---
## Narrative Triangle Service Notes
### Triangle 1 — Hub Power (Voss Kael Nils)
- **Spatial expression:** Voss watches from his window; Kael and Nils work the cargo floor below. The observation geometry makes the power relationship spatial: Voss is elevated and apart, workers are on the floor in his line of sight.
- **Tension locations:** Main corridor (where Voss gives directives), cargo floor (where Kael works under observation), supervisor's office door (the private conversation space for Triangle 1 escalation).
- **Sightline implications:** Nils can see Kael talking to Voss at the SO door. Kael can see Nils noticing. Everything on the cargo floor is visible to multiple parties.
### Triangle 2 — Worried Knowledge (Maret Drin Voss)
- **Spatial expression:** Maret in manifest processing sees the discrepancies. Drin has access to Maret's area (reason to visit). Voss's office is a floor away — Maret can't easily speak to him without being observed crossing the main corridor.
- **Tension locations:** Manifest processing (where Maret's knowledge lives), main corridor (the watched crossing between Maret and Voss), supervisor's office (the destination Maret fears and needs).
- **Sightline implications:** Maret going to the SO door is visible from the corridor and from the cargo floor. It's a public act. This is the spatial cost of Maret choosing to act.
---
## Notes for Copy Team (#190 — Workplace Content Pack)
1. **Scanner bay is Kael's home base.** Dialogue lines set "at the scanner" should describe the ambient of machinery hum, cargo transport belts, distant supervisor's window (is the light on?).
2. **Main corridor is the natural meeting-in-passing location.** NPCs cross here with purpose; lingering draws attention. Lines set "in the corridor" are brief, can-be-overheard exchanges.
3. **Break room is the candid space.** This is where workers say things they'd never say on the floor. Lena's worry about Kael can be voiced here (but Lena isn't a Terminal character — this is for floor workers, Nils especially).
4. **Supervisor's office conversations are formal and asymmetric.** Voss speaks from the window-side, which puts him with light behind him. Workers face into the light. Staging for any SO dialogue should use this power geometry.
5. **Restricted storage is never described directly in overheard NPC dialogue.** NPCs refer to it obliquely: "the back," "the overflow section," "the access." The player learns the word "restricted storage" from signs and the insert, not from NPC speech.
6. **Time of day matters.** Cargo floor is empty enough at deep night that footsteps echo. This is qualitatively different from midday. Mellanie's monologue lines for Terminal exploration should have time-tagged variants.
+386
View File
@@ -0,0 +1,386 @@
# The Settled Reach — v0.1 Visual Grammar
**Version:** v0.1 (Sprint 12)
**Author:** Araminta (Visual Designer)
**Date:** 2026-02-19
**Status:** Active — unblocks #304, #311, #312, #313, #314, #315, #316, #317, #318, #334
This document is the source of truth for all visual decisions in v0.1. Every subsequent visual deliverable references this document. If a decision here conflicts with a local assumption, this document wins. Open a ticket to amend it if the assumption is right.
**Core principle (D-043):** "Sprites are shape templates that the lighting system completes." No baked shadows, no baked lighting, no baked mood. The world is lit by Godot's Light2D pipeline; sprites carry shape and saturation, not atmosphere.
---
## 1. Color Palette — Zone Palettes and Global Constants
### 1.1 What the Environment Communicates
Per D-045 (environmental neutrality), the world layer **never shifts** in response to narrative state, conspiracy activation, or investigation progress. Zone palettes are fixed. The only information signals that can change are entity colors (§3), insert overlay density (§7), and monologue urgency. A player walking into the bar after discovering the conspiracy finds it **still warm and amber**. That indifference is intentional.
What communicates narrative state: **entities** (D-033 colors). What communicates mood: **lighting** (D-046 three-reference model). The environment communicates location, not drama.
### 1.2 Zone 1 — Logistics Hub (The Terminal)
Character: cool grey-navy institutional. Era 1 and Era 2 construction. CanvasModulate is the global ambient between light pools; all visible floor and wall color is revealed by PointLight2D fixtures at cool white temperature.
| Element | Hex | Notes |
|---------|-----|-------|
| Floor tile | `#1a1e24` | Dark cool grey-navy, Era 1 composite panels |
| Wall face | `#2a3040` | Medium navy-grey, visible under tilt angle |
| Wall top | `#222838` | Slightly darker than face; top surface of wall |
| Ambient (CanvasModulate) | `#0e1218` | The darkness *between* light pools. Very dark cool blue-grey. |
| Fixture light color | `#c8d8f0` | Cool institutional white-blue |
| Fixture radius | 810 visual tiles | Wide, efficient workplace coverage |
| Outline (global) | `#333340` | Dark blue-grey. Applied at 48px at 256px working resolution. |
Era 2 modifications (retrofits, junction boxes, modified partitions) share the same floor tile but use `#6d7178` for surface-mounted elements — slightly warmer than Era 1's `#7a7f85`, reflecting different production run.
### 1.3 Zone 2 — Bar (The Last Shift)
Character: warm dark, amber-lit, Era 3 construction. Irregular by design (D-043, "functional warmth"). The warmth is Lera's accumulated decisions, not zone theming — placement and density carry the feeling.
| Element | Hex | Notes |
|---------|-----|-------|
| Floor tile | `#1e1912` | Dark warm brown-grey, worn composite panels |
| Wall face | `#2c2018` | Medium warm brown, Era 3 variation |
| Wall top | `#221810` | Darker warm brown |
| Ambient (CanvasModulate) | `#100c08` | Warm near-black. The bar is darker between pools — intimacy, not gloom. |
| Fixture light color | `#f0b840` | Warm amber — social, inhabited (D-046 Blade Runner reference) |
| Fixture radius | 56 visual tiles | Pooled, intimate. Light doesn't reach every corner. |
| Outline (global) | `#333340` | Same outline as all zones — consistency is non-negotiable |
### 1.4 Zone 3 — Transition Corridors
Character: neutral dark, dim irregular lighting. Original Era 1 construction, maintained not renovated. Emptier because no one settled it (D-051 — "settling is placement").
| Element | Hex | Notes |
|---------|-----|-------|
| Floor tile | `#181818` | Neutral dark grey, industrial grating |
| Wall face | `#242424` | Medium neutral dark |
| Wall top | `#1a1a1a` | Marginally darker |
| Ambient (CanvasModulate) | `#0c0c0c` | Near-neutral black. The coldest ambient. |
| Fixture light color | `#d0d8e0` | Cold dim white. Maintenance grade. |
| Fixture radius | 45 visual tiles | Sparse, uneven. Gaps between pools are wider. |
| Outline (global) | `#333340` | Same as all zones |
### 1.5 Global Color Constants
These are not zone-specific. They apply everywhere.
| Constant | Hex | Usage |
|----------|-----|-------|
| Outline standard | `#333340` | All sprites, all zones, all layers |
| Insert chrome | `#c8d0e0` | Default insert HUD color (cursor, grid lines, non-relationship data) |
| Unexplored wireframe | `#333340` | Same as outline — geometric map app aesthetic |
| Unexplored (no map) | `#12141a` | Solid near-black. Information zero. |
### 1.6 What Doesn't Get a Color
Danger, safety, tension, conspiracy — none of these get palette representation in the environment. They're carried by entities (§3) and insert density (§7). If you find yourself wanting to tint a tile orange because something bad happened there, that's the environment doing work it's not supposed to do.
---
## 2. Entity Sizing and Proportions
### 2.1 Grid Context
The dual-scale grid (D-066) separates simulation resolution from visual presentation:
- **Simulation tile:** 0.5m. All server-side logic (movement, LOS, pathfinding, occupancy) operates here.
- **Visual tile:** 1m = 2×2 sim tiles. All client-side art is authored at this scale. **1 visual tile = 64×64 pixels at runtime.**
- **Retina factor:** 2×. The simulation knows nothing about visual tiles — that's a client rendering convention.
### 2.2 Entity Sprite Dimensions
| Property | Value | Notes |
|----------|-------|-------|
| Sprite footprint | 2×2 sim tiles | Entities visually span one full visual tile (64×64px) |
| Sprite art area | 24×32 px | Active art within the 64px canvas. Clear figure-ground against tile. |
| Occupancy | 1×1 sim tile | Character occupies half a visual tile for sub-tile positioning precision |
| Player sprite | 24×32 px art, 64×64 canvas | Same as NPC — no special sizing |
| Named NPC | 24×32 px art, 64×64 canvas + 1 silhouette feature | Kael: vest ridge. Lera: apron shape. Sera: uniform collar. |
| Generic NPC | 24×32 px art, 64×64 canvas, 34 template silhouettes | Rotated/mirrored from template set |
**Why 24×32 and not 32×32 or 64×64:** Entities must read clearly against the background without visually consuming the tile. Too large = visual clutter when multiple entities share an area. Too small = unreadable at a glance. 24×32 is the figure-ground minimum that keeps silhouette readable.
**Tilt implication (D-019 amendment):** The Rimworld shallow tilt (~1520° from vertical) means sprites are authored with south-facing front faces. Draw 2 sprite directions: front-facing (south) and back-facing (north). Left/right are mirrors of these. The Godot camera is purely orthographic; the tilt is a drawn convention in the art.
### 2.3 Static Object Sizing
| Object type | Minimum footprint | Outline |
|-------------|------------------|---------|
| Small items (mug, comm, datapad) | 1×1 visual tile | 1px `#333340` |
| Furniture (chair, terminal, crate) | 1×1 to 2×2 visual tiles | 1px `#333340` |
| Large furniture (bar counter, desk array) | 2×1 to 3×1 visual tiles | 1px `#333340` |
| Structural (walls, doors) | 2×2 sim tile minimum (= 1×1 visual tile) | Minimal to none |
**Hard rule (D-066):** All walls, furniture, crates, and doors occupy a minimum of 2×2 sim tiles. This ensures visual truth and sim truth agree on cover and LOS occlusion. No structural element is narrower than one visual tile.
### 2.4 Player Indicator
Both player characters use their D-033 base color as primary identification (see §3). There is no separate player indicator sprite in v0.1 — the player character is just an entity with distinct D-033 coloring and, eventually, a distinctive silhouette.
| Character | Entity sprite color | Notes |
|-----------|-------------------|-------|
| Detective | `#e0e8ff` (cool, faint blue-white) | Institutional, analytical register |
| Smuggler | `#e8e0d0` (warm, faint amber-cream) | Social, camouflage register |
These are deliberately desaturated. The player should read clearly but not dominate the palette.
---
## 3. Entity Color System
*This section covers #304 content. Ticket #304 is closed by this document.*
### 3.1 Core Principle
Entity color encodes the **player character's subjective relationship** to an NPC — not an objective property of the NPC. The same NPC can appear as different colors to the detective and the smuggler. Color is a client-side derivation from `RelationshipState` component + knowledge state (D-033). The server never sends "this NPC is red" — it sends the information that the client derives color from.
This is asymmetric information rendered visually. It's the most important system in the game to get right.
### 3.2 Relationship Color Palette
| Relationship state | Color | Hex | Visual quality |
|-------------------|-------|-----|----------------|
| Unknown / Neutral | Cool teal | `#4a9ebb` | Default. Unassessed. Player knows they exist. |
| Known / Friendly | Soft green | `#6bc9a6` | Trusted. Known person. Could still be THE FRIEND. |
| Person of Interest | Warm amber | `#e8c547` | Something flagged this person. Monologue or case file. |
| Hostile / Dangerous | Muted red | `#d45d5d` | Player character perceives *subjective* danger. Not omniscient. |
| Static objects | Muted grey | `#8b8ba0` | Furniture, equipment, environmental. Non-person. |
**Red means danger to your character**, not danger in the abstract. A detective might see amber (person of interest) where the smuggler sees green (trusted colleague) for the exact same NPC. Both are correct — from their position.
### 3.3 Player Characters
| Character | Color | Hex |
|-----------|-------|-----|
| Detective | Cool blue-white | `#e0e8ff` |
| Smuggler | Warm cream | `#e8e0d0` |
Player colors are deliberately near-neutral. They should read as "self" without the loaded meaning of the NPC colors.
### 3.4 Transition Behavior
Color shifts are **0.5-second smooth fades**. Never instant. The visual transition IS the information delivery — the player reads the relationship changing, not a sudden color swap.
**THE FRIEND's first shift** (green → amber) must be the **first relationship color change** in the session. Stage the opening 2025 minutes so no other relationship state change precedes it. This is wow moment #3 (D-039); the visual punch depends on the player having already internalized what green means.
### 3.5 Interaction with Insert Overlay
In the insert overlay (z-layer 6, §7), entity D-033 colors gain soft halos (23px gaussian blur at ~40% blend). In the natural vision layer (z-layer 3), they're rendered with 2px outline in the relationship color — hard edge, no bloom. The bloom is the insert's interpretation; the hard edge is the player's naked eye.
### 3.6 Saturation Hierarchy
Entity D-033 colors are the most saturated elements in any scene. Object-layer favorite colors (D-052) must stay below D-033 entity saturation to preserve visual hierarchy.
| Hierarchy tier | Saturation range | Examples |
|----------------|-----------------|---------|
| Entity (D-033) | High: 4060% | `#4a9ebb`, `#6bc9a6`, `#e8c547`, `#d45d5d` |
| Objects (D-052 favorites) | Moderate: 1530% | Dusty blue, warm terracotta, faded olive (muted register) |
| Structure (zone palette) | Low: 515% | Zone floors, wall faces, tiles |
Never introduce an object color that competes with entity color saturation.
---
## 4. Z-Level Rendering Stack
### 4.1 The 8-Layer Stack
Per D-049. Layers are Godot CanvasLayer z-indices. Higher index = renders on top.
| Z-index | Name | Contents | Notes |
|---------|------|----------|-------|
| **0** | Floor tiles | Zone floor panels, grating, transition strips | Muted zone palette. Lowest saturation. No outlines on floors. |
| **1** | Floor objects | Cosmetic floor detail (dirt, markings, decals) | Walked over. No gameplay interaction. |
| **2** | Furniture / placed objects | Desks, chairs, crates, bar counter, terminals | Y-sorted with z-layer 3 for overlap resolution |
| **3** | Entity sprites | All characters (player + NPCs) | Y-sorted with z-layer 2. D-033 colored. 2px outline. |
| **4** | Overhead / wall tops | Pipes, ducts, signage, lighting fixtures, wall tops | Semi-transparent partial occlusion (~70% opacity). Creates information gaps within known spaces. |
| **5** | Fog of perception | Vision cone mask + fog shader + sound ping sprites | Affects layers 04. Does NOT affect layers 67. |
| **6** | Insert overlay | Lattice HUD elements, relationship markers, cursor, interaction labels | Bloom-rendered. NOT masked by fog — computational, not perceptual. |
| **7** | Monologue / UI | Internal monologue text, dialogue box, world menu, HUD chrome, cursor (D-056) | Always visible. Top of stack. |
### 4.2 Y-Sorting Rule
Layers 2 and 3 are y-sorted together. An entity moving behind a desk disappears correctly. An entity standing in front of a desk renders in front. The y-sort key is the entity's or object's bottom-most sim tile position.
**Never override y-sort manually** for specific entity/object pairs. The rule applies universally.
### 4.3 Wall Rendering
- **Structural walls (load-bearing, room dividers):** Option B — visible top face + south-facing wall face (visible under tilt angle). Top face on z-layer 4, face on z-layer 2 boundary.
- **Interior partitions (half-walls, furniture dividers):** Option A — boundary lines only. Minimal visual weight.
### 4.4 Overhead Occlusion
Layer 4 objects render at ~70% opacity when the player is beneath them. This creates **local information gaps within otherwise known spaces** (Gore's observation, D-049). An NPC behind a shelving unit in a fully explored room still partially disappears — the exploration context (fog state) doesn't grant omniscience about what's behind objects.
This is a thematic system as much as a rendering one. Don't eliminate it for "clarity."
---
## 5. Typography Baseline
### 5.1 Typeface
**Michroma** (Google Fonts, Regular 400, free for commercial use) is the sole typeface for all player-facing text in v0.1.
Michroma is geometric sans-serif — technical, clean, forward-facing without being aggressive. It fits the insert aesthetic: the character is reading their own neural lattice output. All text, including dialogue and environmental labels, renders as if through this lens. There is no "game UI font" vs "world font" — Michroma is both.
### 5.2 Text Size and Role Specifications
All sizes are in pixels at 1080p (1920×1080) base resolution. Godot 4 handles DPI scaling.
| Text role | Size | Opacity | Color | Z-layer | Notes |
|-----------|------|---------|-------|---------|-------|
| Dialogue — NPC speech | 16px | 100% | `#e8eaf0` | 7 | Max width ~70% screen width. Left-aligned. |
| Dialogue — player response | 14px | 90% | `#c0c8d8` | 7 | Below NPC speech. Up to 3 options visible. |
| Monologue (standard) | 13px | 85% | `#d0d4e0` | 7 | Floats above dialogue box. Inner voice — slightly dimmer than dialogue. |
| Monologue (urgent) | 13px | 100% | `#e0e8f8` | 7 | Same size, full opacity, slight bloom pulse. Urgent chime accompanies. |
| Environmental text | 11px | 70% | `#8899aa` | 24 | Labels on terminals, signs, lockers. Part of the world. Muted. |
| HUD / insert chrome | 12px | 80% passive, 100% active | `#c8d0e0` | 6 | Time display, insert labels, menu chrome |
| Interaction verbs | 13px | 95% | D-033 color of target | 6 | Rendered on insert z-layer. Disappears when insert off. |
| Confrontation weight | 15px | 100% | `#e0e8f0` | 7 | Same box as dialogue but heavier visual weight — wider, higher contrast frame |
### 5.3 Dialogue Box Layout (D-061)
- **Position:** Bottom of screen, full screen width (max-width)
- **Max height:** 20% of screen height
- **Layout:** NPC speech on top line, response options stacked below, left-aligned
- **Max visible response options:** 3
- **Locked options:** Invisible — player never sees what they can't access
- **No portraits:** The NPC is on screen. A portrait is redundant.
Monologue on z-layer 7 floats above the dialogue box, **not inside it**. This spatial separation allows monologue to contradict dialogue visually — the character thinks one thing (above, inner) while the NPC speaks another (below, outer).
### 5.4 Text That Does NOT Get Michroma
Nothing. All text goes through Michroma. Handwritten signs, terminal readouts, personal notes — all render in Michroma at appropriate size and opacity. The fiction is that everything is mediated through the player character's neural insert perception layer.
---
## 6. Animation Tier Baseline
### 6.1 Two-Tier System (D-047)
The animation system has two tiers, and the boundary between them is **invisible to the player**. This invisibility is the mechanic: routine is readable, so deviation is noticeable, so the player can't be sure what they're seeing.
**Tier 1 — Clear (Active NPCs):**
Publicly motivated behavior. Instantly readable from a glance. Player can determine intent.
| Animation | Frames | Notes |
|-----------|--------|-------|
| Idle | 23 frames | Subtle breathing cycle. Not distracting. |
| Walk | 8 frames (2 directions × 4 frames, mirrored for L/R) | Interpretation of D-047's "46 directional frames" range: 4 frames per drawn direction (N/S), mirrored for E/W = 8 total frames in the animation strip. |
| Working at terminal | 23 states | Key-tap, pause, review. Readable as "doing their job." |
| Handling cargo | 23 states | Lift, carry, set down. |
| Eating / drinking | 23 states | Raise, consume, lower. Readable as "on break." |
| Talking | 23 states | Slight lean, gesture. Distinguishable from "standing near." |
| Sleeping | 1 state | Static prone. Unambiguous. |
**Tier 2 — Ambiguous (Background NPCs + deliberate Tier 1 ambiguity):**
Privately motivated behavior. Observable but not interpretable. The player sees the action but cannot determine the intention.
| Behavior | Appearance | Ambiguity |
|----------|-----------|-----------|
| Pausing | Standing idle | Break? Waiting? Watching? |
| Looking around | Head turn (1 frame) | Nervous? Checking for someone? Habit? |
| Lingering near location | Idle near an object | Interested? Positioned? Coincidence? |
| Changing direction | Mid-path turn | Remembered something? Avoiding someone? |
| Proximity without interaction | Near entity, no Tier 1 state | Protecting? Monitoring? Coincidence? |
**The line between tiers is the investigation mechanic.** Tier 1 is the baseline everyone reads. Tier 2 is what creates the signal-from-noise problem that makes investigation interesting.
### 6.2 v0.1 Animation Scope
In v0.1, all entities start as colored rectangles with labels (D-014: "functional boxes with labels"). The animation system defines what gets built when we graduate past rectangles.
- Phase 1 (current): Colored rectangle + name label. No animation.
- Phase 2 (sprite replacement): Tier 1 walk cycle + idle for Active NPCs. Static sprite for Background NPCs.
- Phase 4 (polish): Full Tier 1 set, Tier 2 behavioral states, player character expressions.
**Background NPCs (Tier 2 only) never get animation in v0.1.** They are static sprites. The cost of animating 5002,000 Background-tier NPCs is not justified when their visual presence is already deliberate and readable.
---
## 7. Neural Insert Overlay
### 7.1 Visual Language
The insert overlay (z-layer 6) is a **geometric data layer** rendered with an **organic delivery**:
- Data structure: precise positioning, clean geometric lines, structured information layout
- Neural delivery: ~23px gaussian blur at ~40% blend on the insert CanvasLayer
- Result: "amber feels like a vague sense of concern, not a data flag" (Araminta, D-048)
The insert is not an HUD bolted onto a game. It IS the character's neural lattice. If switching it off would feel like going deaf rather than closing a window, it's working. After 10 minutes, the player should forget it's there.
### 7.2 Character Variants
Both characters use the same underlying system. The `lattice_profile` parameter on the `ObserverSnapshot` determines which variant renders.
| Property | Detective (`lattice_augmented`) | Smuggler (`lattice_baseline`) |
|----------|--------------------------------|------------------------------|
| Line weight | 12px | 1px |
| Annotation density | Dense — more markers, more connection lines | Sparse — essential markers only |
| Passive opacity | 80% | 60% |
| Active opacity | 100% | 85% |
| Bloom intensity | 40% blend at 23px gaussian | 40% blend at 23px gaussian (same — different density, same feel) |
| Color | D-033 relationship colors + `#c8d0e0` chrome | Same |
The detective has augmented lattice hardware — they see more, more precisely. The smuggler has baseline — they see what they need, no more. The asymmetry isn't which colors appear, it's how much annotation the overlay generates from the same underlying data.
### 7.3 Fog Interaction
**The insert overlay is NOT affected by the fog shader.** Fog lives on z-layer 5 and masks z-layers 04. The insert (z-layer 6) and UI (z-layer 7) are not masked.
Insert markers can appear in fogged areas if the lattice has that data. A known-friendly NPC behind a wall can still appear as a green halo in the fog — the character's lattice tracks the last known position. This is insert data, not visual data. The character knows, even if they can't see.
### 7.4 Soft Degradation
At high perception load (multiple overlapping modes), the insert shows **diegetic scan-line interference** — subtle horizontal distortion at ~5% intensity. No hard cap on simultaneous perception modes. The degradation is visual warning, not mechanical punishment. Player reads it as "the insert is working hard." (D-059, D-048)
### 7.5 Insert-Off Behavior (D-056/D-057 — OQ-07 Resolution)
When `insert_active == false`:
- Cursor shape still changes (entity hover → bracket shape, object hover → X-shape) — the character's body responds subconsciously to proximity
- Interaction labels (z-layer 6) are suppressed — the insert doesn't process targets without activation
- `should_show_interactions()` returns false
- Insert markers in fog disappear — the lattice isn't running position tracking
- **Bloom disappears** — no insert, no neural rendering pass
The character still moves and physically orients toward entities. The insert stops annotating.
---
## Appendix A — Decision Cross-References
| Decision | Relevance |
|----------|-----------|
| D-033 | Entity color = relationship to player. Defines all NPC and player entity colors. |
| D-043 | Art direction — "functional warmth." Style, asset pipeline, production principle. |
| D-044 | Visual hierarchy (entity > object > structure). Outline weights, saturation rules. |
| D-045 | Environmental neutrality — strict zero shift. Zone palettes never change. |
| D-046 | Lighting — three-reference model (Darkwood, BR2049, Hopper). Zone light temperatures. |
| D-047 | Two-tier animation system. Tier 1 clear / Tier 2 ambiguous. |
| D-048 | Neural insert overlay visual design. Bloom spec, character variants. |
| D-049 | Z-level rendering stack — 8 layers. |
| D-052 | Character favorite colors — object-layer identification. Saturation constraint. |
| D-056 | Cursor states — insert-styled geometric. Four states, 150ms transitions. |
| D-057 | Entity interaction — vertical list, insert-styled. |
| D-059 | Fog shader — 5 layers, knowledge-graph-driven. Fog entities spec. |
| D-061 | Dialogue box — bottom screen, max 20% height, no portraits. |
| D-066 | Dual-scale grid — 0.5m simulation, 1m visual, 64x64px visual tiles. |
## Appendix B — What This Document Does Not Cover
The following are out of scope for v0.1 visual grammar and will be specified in separate documents when unblocked:
- **Insert/HUD wireframe** — dual character variants in detail (#314, blocked by this doc)
- **Monologue display system** — typography animation, stacking, voice differentiation (#315)
- **Text display hierarchy** — 4 content pipelines (#316)
- **Sound indicator visual design** — fog-edge ping pulse (#317)
- **THE FRIEND visual treatment** — after Paula's NPC profiles (#318)
- **Environmental text standards** — sign conventions, terminal text (#334)
- **Spatial layouts** — tile-level floor plans (#311, #312, #313, all blocked by this doc)
+43
View File
@@ -0,0 +1,43 @@
# Sprint 13: Sound — Audio Tasks
**Goal:** Wire the full audio architecture end-to-end — AudioManager, zone crossfades, dip profiles, recognition chime, and conversation murmur — and complete v0.1 content authoring so opening hooks, tutorial monologue, and all content packs are done.
**Branch:** `audio`
**Agents:** Inigo (sound design)
## Carry-over from Sprint 12
None. Sprint 12 audio tickets done (#440 interaction audio assets, #453 Sprint 7 asset revision).
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #532 | NPC conversation murmur asset — event-driven World SFX bus wiring | — |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/perception.md` — D-072 (universal event-driven conversation murmur), D-071 (ListeningFocus boost — eavesdrop conspicuousness), D-038 (audio in v0.1 scope — 8 files via Stable Audio Open)
- `decisions/content.md` — D-074 (audio aesthetic identity — insert-tech vs organic)
## Notes
- **#532 — NPC conversation murmur asset:** Produce a single universal NPC proximity murmur asset via Stable Audio Open per D-072. This is NOT a zone-specific asset — zone ambient determines conspicuousness naturally (bar's dense ambient buries it, corridor's sparse ambient exposes it). The asset should feel like: human voices in an adjacent room — direction and cadence distinguishable, words not. Duration target: 4-8s loopable or a set of short one-shot clips (2-3s each). Route: World SFX bus. Aesthetic per D-074: the audio identity pairs "insert-tech" (synthetic, lattice-flavored for UI/insert sounds) with "organic" (human, environmental for world sounds). The murmur is an organic world sound — warm, human, lived-in. No lattice processing on this one. Deliver to `audio/` directory as `sfx_npc_murmur.ogg` (or similar descriptive name matching the directory-scan pattern the client expects). Signal to client team when asset is committed so they can unblock #533.
## Dependency Chain
```
#532 (murmur asset) → unblocks client #533 (murmur wiring)
```
Standalone. Can start immediately.
## PR Workflow
When ready to submit, create a PR with `tea` CLI. All flags are required to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(audio): NPC conversation murmur asset" --description "body" --base main --head audio
```
+60
View File
@@ -0,0 +1,60 @@
# Sprint 13: Sound — Client Tasks
**Goal:** Wire the full audio architecture end-to-end — AudioManager, zone crossfades, dip profiles, recognition chime, and conversation murmur — and complete v0.1 content authoring so opening hooks, tutorial monologue, and all content packs are done.
**Branch:** `client`
**Agents:** Stig (client dev), Tyre (architect), Hoshe (QA)
## Carry-over from Sprint 12
None. All Sprint 12 client tickets done.
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #528 | AudioManager full implementation — 5-bus setup, directory registry, volume sliders | — |
| #529 | Zone crossfade implementation — 1.5-2s tween on zone_id change | #528 |
| #530 | Audio dip profiles — dialogue, confrontation, ListeningFocus boost | #528 |
| #531 | Recognition chime — fires at cognitive delay onset (D-067) | #528 |
| #533 | Wire NPC conversation murmur to World SFX bus — event-driven playback | #528, #532 (audio) |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/architecture.md` — D-068 (5-bus audio architecture), D-073 (zone crossfade approach)
- `decisions/perception.md` — D-067 (recognition chime timing), D-069 (audio dip profiles), D-070 (confrontation as cognitive vulnerability), D-071 (ListeningFocus boost), D-072 (NPC conversation murmur)
## Notes
- **#528 — AudioManager full implementation:** `client/scripts/autoloads/audio_manager.gd` already exists and is substantially built. The 5-bus setup, directory-scan registry, `play()`, `play_at()`, `apply_dip()`, `clear_dip()`, and `set_volume()` methods are all implemented. What remains: (1) `set_zone()` is a stub (`pass`) — this becomes the zone crossfade implementation for #529; (2) volume slider persistence (save/load player preferences); (3) settings UI bindings for the 5 sliders; (4) ensure the Godot project's AudioServer bus layout matches the 5 bus names in code. Before starting, verify bus names in `Project > Audio` match `BUS_MUSIC`, `BUS_AMBIENT`, `BUS_WORLD_SFX`, `BUS_PLAYER_ACTIONS`, `BUS_UI_SOUNDS`.
- **#529 — Zone crossfade:** Implement `set_zone(zone_id: String)` in `audio_manager.gd`. `SimBridge` already receives tile data from `ObserverSnapshot` — determine the player's current tile zone and call `AudioManager.set_zone()` on zone change. Crossfade logic: kill any active ambient tweens, start a 1.5-2s `create_tween()` that fades out the current `_ambient_players` entry and fades in the new zone asset. Zone-to-asset mapping for v0.1: hub/workplace → `amb_hub_layer`, bar → `amb_bar_layer`, smuggling corridor → `amb_corridor_layer` (names must match filenames in `res://audio/`). Crossfade is interruptible — walking back across a boundary starts the reverse tween.
- **#530 — Audio dip profiles:** `DIP_SPECS` and `apply_dip()` / `clear_dip()` are already implemented in `audio_manager.gd`. What remains: wire call sites. Dialogue open/close → `apply_dip("dialogue")` / `clear_dip()`. Confrontation start/end → `apply_dip("confrontation")` / `clear_dip()`. ListeningFocus: the tick gate (30+ stationary ticks) is the caller's responsibility (per existing comment in `audio_manager.gd`); `sim_bridge.gd` or `game_state.gd` must track `stationary_ticks` from the server's `ListeningFocus` field in the snapshot and call `apply_dip("listening_focus")` / `clear_dip()` accordingly. D-070: no UI indicator — the muffling is felt, not announced.
- **#531 — Recognition chime:** `CHIME_RECOGNITION = "sfx_monologue_chime"` is already defined in `audio_manager.gd`. Wire it: when `fog_entities.gd` or `entity_renderer.gd` begins a recognition sequence (detecting `RecognitionTrigger` in the snapshot), call `AudioManager.play(AudioManager.CHIME_RECOGNITION)` at the ONSET of the cognitive delay — not at completion. The chime routes to the `UISounds` bus automatically (default bus in `play()`). Full sequence per D-067: entity detected → chime plays (300-400ms) → 0.6s cognitive delay begins concurrently → monologue text appears during delay → blob transitions to D-033 color + silhouette → recognition complete. The chime is the "unresolved" signal — it precedes recognition, does not confirm it.
- **#533 — NPC murmur wiring:** Blocked by #532 (audio team produces the asset). Once the murmur asset exists in `res://audio/`, wire event-driven playback: when `SimBridge` receives conversation/voice events from `ObserverSnapshot`, call `AudioManager.play_at(asset_key, world_pos, BUS_WORLD_SFX)` for proximity or `AudioManager.play(asset_key, BUS_WORLD_SFX)` for ambient murmur. Single universal asset — zone ambient provides natural conspicuousness (bar's `amb_bar_layer` buries it, corridor's low ambient exposes it). No zone-specific murmur variants.
## Dependency Chain
```
#528 (AudioManager — complete set_zone + settings UI + slider persistence)
#529 (zone crossfade — implement set_zone body)
#530 (dip profiles — wire call sites in sim_bridge / game_state)
#531 (recognition chime — wire ONSET trigger in fog_entities / entity_renderer)
#533 (murmur wiring) ← also blocked by #532 (audio team asset)
```
#528 can start immediately. #529, #530, #531 can proceed in parallel once #528 is done.
#533 waits for both #528 and #532.
## PR Workflow
When ready to submit, create a PR with `tea` CLI. All flags are required to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(audio): description" --description "body" --base main --head client
```
+96
View File
@@ -0,0 +1,96 @@
# Sprint 13: Sound — Joint / Integration Notes
**Goal:** Wire the full audio architecture end-to-end — AudioManager, zone crossfades, dip profiles, recognition chime, and conversation murmur — and complete v0.1 content authoring so opening hooks, tutorial monologue, and all content packs are done.
**Sprint 13 ID:** 13
**Status:** planning (activate with `db/connectors/sprint start`)
---
## Pre-Sprint Decisions
The following decisions are implemented this sprint and must be cross-referenced:
| Decision | Domain | Implementing ticket(s) |
|----------|--------|----------------------|
| D-067 (recognition chime at cognitive delay onset) | perception | #531 (client) |
| D-068 (5-bus audio architecture) | architecture | #528 (client) |
| D-069 (audio dip profiles) | perception | #530 (client) |
| D-070 (confrontation as cognitive vulnerability) | perception | #530 (client — no UI indicator) |
| D-071 (ListeningFocus boost for eavesdropping) | perception | #530 (client — 30-tick gate) |
| D-072 (universal NPC conversation murmur) | perception | #532 (audio), #533 (client) |
| D-073 (zone crossfade — hard boundary, soft transition) | architecture | #529 (client) |
| D-074 (audio aesthetic — insert-tech vs organic) | content | #532 (audio — aesthetic guide) |
| OQ-09 (zone temperature memory — server or client) | perception | #523 (server — resolve + implement) |
---
## Cross-Team Dependencies
| Dependency | From | To | Risk |
|------------|------|----|------|
| #532 (murmur asset committed to audio branch) | audio | client #533 | Medium — client can stub; wire when asset arrives |
| #523 (OQ-09 resolution — zone_id in snapshot?) | server | client #529 | Low — zone crossfade uses zone_id; if not yet in snapshot, server adds it |
| #315 (monologue display spec) | visual | future client monologue UI (Sprint 14) | Low — #315 output is a spec doc, does not block Sprint 13 client tickets |
**Coordination protocol:**
- Audio team (#532): Signal to client team when `sfx_npc_murmur.ogg` (or equivalent) is committed to the `audio` branch so #533 can be unblocked.
- Server team (#523): If OQ-09 resolution requires adding `zone_id` to `ObserverSnapshot` tile data, coordinate with client team on the field name before implementing — client's zone crossfade (#529) reads this field.
- Client team (#528 first): All other client tickets depend on AudioManager being complete. Start #528 immediately and signal when done so #529, #530, #531 can proceed in parallel.
---
## Sprint Completion Proof
When Sprint 13 is done, the following is concretely observable:
1. **AudioManager production-ready:** Launch the client. Open Project > Audio in Godot — five buses are present: Music, Ambient, WorldSFX, PlayerActions, UISounds. Navigate to settings — five volume sliders appear, one per bus. Adjust a slider — the in-game volume changes in real time and the setting persists across restart.
2. **Zone crossfades:** In the live game, walk from the hub/workplace area into the bar. Within 1.5-2s of crossing the boundary tile, the ambient audio smoothly transitions from hub ambient to bar ambient. Walk back — it reverses. No audio pops. No crossfade artifacts when crossing rapidly.
3. **Audio dip profiles:** Start a conversation with an NPC. Within 300ms, ambient volume drops 6-8dB (relative to slider setting). End the conversation — ambient restores over 500ms. Initiate a confrontation — ambient drops further with a low-pass filter sweep (muffled feel, D-070). Stand still for 30+ seconds (30+ ticks at 10tps) without conversing — world SFX subtly boosts (eavesdrop bonus, D-071). No UI indicator for any of these states.
4. **Recognition chime:** Move near an entity that is in fog but detectable (heard/sensed). The chime plays at the moment of detection (onset of cognitive delay) — before monologue text appears, before the fog blob resolves to a silhouette. Chime is on UISounds bus. Duration is 300-400ms. Monologue text appears during the delay while the chime is still fading.
5. **NPC murmur:** When two NPCs are in conversation range, a soft murmur sound plays on the WorldSFX bus. In the bar, the murmur blends into ambient and is nearly inaudible. In a corridor, the murmur is clearly audible and draws attention. Single asset — same sound in both zones.
6. **Gauntlet rooms:** Running `make test-server` (or `cargo test` in `server/`) executes `sound_lab_*`, `decay_observatory_*`, and `shift_change_*` tests without failure. Tests are deterministic across runs.
7. **OQ-09 resolved:** `decisions/` contains a new or amended decision recording the zone temperature memory approach. If server-tracked: `ObserverSnapshot` tile data includes a `zone_id` field. If client-only: fog shader applies temperature tint from local visited-zone memory. Either way, deep fog in the bar shows a warm dark tint, hub shows cool dark, corridors neutral dark.
8. **Monologue display spec:** `docs/design/monologue-display-spec.md` exists with all required sections: typography, positioning, stacking rules, priority system, fade animation, character color differentiation (with hex values), and 80-character line length constraint.
---
## Test Plan Alignment (D-030)
Sprint 13 is in the integration phase. Test focus:
- **Client:** AudioManager unit tests — registry scan with mock assets, bus setup, dip apply/clear/interrupt, zone crossfade tween timing. Recognition chime test — verify `CHIME_RECOGNITION` plays at snapshot onset, not at recognition completion. All tests via Hoshe's client QA harness.
- **Server:** Sound Lab, Decay Observatory, Shift Change gauntlet rooms execute correctly. `decay_observatory_*` tests verify BTreeMap-deterministic confidence degradation. `shift_change_*` tests advance the game clock through a phase boundary and verify NPC routine transition.
- **CI:** `make ci` must pass across all branches before PR. `make pre-pr` runs content cross-reference validation and fixture staleness check. No new HashMap usage in simulation crate (clippy ban still active per #343).
---
## Decision Coverage Gaps (remaining after Sprint 13)
The following confirmed decisions still have no implementing tickets after Sprint 13 closes. Flag to Team Leader if any block upcoming work:
- D-031 (time system / game clock) — `server/src/simulation/time.rs` exists; gauntlet shift change room (#505) exercises it. No standalone ticket. Monitor.
- D-032 (separate monologue pools per character) — no ticket yet; relevant to monologue system (future sprint).
- D-033 (entity color = relationship to player) — covered by visual grammar (#303, done) and monologue display spec (#315 this sprint).
- D-036 (Sova Transit District setting) — no implementation ticket; world map authoring is future.
- D-038 (audio assets — 8 files) — assets produced (Sprint 10, #327). Integration via #528 and murmur via #532.
- D-040 (wiki taxonomy) — no ticket; wiki structure is documentation, not code.
---
## Notes for Sprint Start
To activate the sprint once planning is approved:
```bash
db/connectors/sprint start
```
This sets Sprint 13 to `active`. Teams then run `db/connectors/sprint start-work --team <team>` for their full context dump.

Some files were not shown because too many files have changed in this diff Show More