Compare commits

..
60 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.6 5f6a42000b chore(meta): release v0.1.24
Sprint 24: Signal — 10/10 tickets done.
Character archetype selection, triangle activation consumer,
news ticker HUD, proximity monologue lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:00:08 +01:00
jpmschweitzerandClaude Opus 4.6 726c0fecbd chore(skills): update workshop-start skill
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:56:52 +01:00
jpmschweitzerandClaude Opus 4.6 eea3f3cf25 docs(decisions): record 24 workshop decisions and updated questions
D-records from Where's the Fun workshop across architecture, content,
and scope domains. Updated open questions for v0.2 pivot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:56:46 +01:00
jpmschweitzerandClaude Opus 4.6 80ddc35412 docs(workshops): add Where's the Fun workshop outputs
5 rounds, 9 agents + Qatux + SI, 24 decisions locked.
Full round transcripts and workshop outcomes summary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:56:39 +01:00
jpmschweitzerandClaude Sonnet 4.6 58d2e3b695 chore(meta): add feasibility warnings to Sprint 25 briefings
server.md: four warnings from Troblum — generate_npc() requires a live
bevy World (stub routine generation in Phase 1), cultural text assembly
is a new code path not a one-liner, DayPhase alias collision in
generator.rs, schema negotiation takes rounds.

joint.md: confidence 15% note at top. Intra-zone variance test added
(rural seed 42 vs rural seed 43 — coherence within type, variance
across seeds). Pass conditions restructured into three explicit
comparisons: cross-type, intra-type, culture.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:54:51 +01:00
jpmschweitzerandClaude Sonnet 4.6 0f1eda8d12 chore(meta): restructure Sprint 25 per feasibility study
Dependency chain inverted: #611 (NpcBlueprint structs) now goes first
and defines the schema contract. Copy team (#609, #610) fills YAML to
match Tyre's structs rather than the other way around.

#613 (NPC generation pipeline) cancelled and absorbed into #612 — the
NPC pipeline is the print loop at the end of the generator binary, not
a separate ticket.

Ticket descriptions loosened: strip over-specified acceptance criteria,
replace with intent + scope boundaries. Phoneme generation explicitly
out of scope for #610 (name lists are sufficient). #612 gains a phased
approach note (Phase 1: hardcoded stubs, Phase 2: real YAML) so server
can build in parallel with copy.

Briefings updated to reflect inverted chain, two-ticket server sprint,
and exploratory framing: this sprint discovers the right spec, it does
not implement a known one.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 20:44:01 +01:00
jpmschweitzerandClaude Sonnet 4.6 7d7aec9cec chore(meta): plan Sprint 25: Emerge
Generator spike sprint. 5 tickets across copy and server teams:
- #609 zone identity spec (copy)
- #610 Krenn culture profile (copy)
- #611 NpcBlueprint struct design (server)
- #612 Template assembly generator (server)
- #613 NPC generation pipeline (server)

Sprint proof: throwaway render — rural Krenn village from minimal
input (zone type + culture profile, no per-location spec).

Closed #586 (tile data model epic — child #594 done).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 19:59:24 +01:00
jpmschweitzer ea21884f3d Merge remote-tracking branch 'origin/client' 2026-03-05 16:44:34 +01:00
jpmschweitzerandClaude Opus 4.6 e0eb3cd35e fix(client): address PR #86 review — archetype validation, teleport clear, ticker layout
- protocol.gd: replace capitalize() with explicit match for archetype
  string mapping, push_error on unknown input with Detective fallback
- main.gd: clear _known_triangle_ids in _teleport_transition() alongside
  _known_recognition_ids so chime re-fires after room change
- news_ticker.gd: defer get_minimum_size() via call_deferred to run
  after layout pass, fixing first-frame scroll distance
- 3 new tests: unknown archetype fallback, triangle dedup per-id,
  independent triangle ID firing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 16:28:12 +01:00
jpmschweitzerandClaude Opus 4.6 349f02fbcb chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:44:36 +01:00
jpmschweitzerandClaude Opus 4.6 7dbd3247d4 test(client): Sprint 24 signal tests
16 tests covering character select, triangle activation consumer,
news ticker, and protocol v19 bridge. Includes show/hide behavior
for ticker on null current_ticker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:44:12 +01:00
jpmschweitzerandClaude Opus 4.6 61d454228d feat(client): character select, triangle activation consumer, news ticker
Sprint 24 Signal — three client tickets delivering the player-facing
storyteller feedback loop:

- #588: Character archetype select screen between New Game and session
  start. Two-card UI (Smuggler/Detective), keyboard+mouse, ESC cancels.
  GameState.character_archetype persisted and sent in StartupMessage.
  PROTOCOL_VERSION bumped to 19.
- #590: Triangle crisis event consumer. Decodes triangle_crisis_events
  from snapshot, fires sfx_monologue_chime_urgent once per triangle per
  session via AudioManager.CHIME_ACTIVATION.
- #592: News ticker HUD element. Scrolling marquee on UILayer, visible
  only when current_ticker is present in snapshot (Last Shift zone).
  Zero-arg update_from_state reads from GameState.current_snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:44:05 +01:00
jpmschweitzerandClaude Opus 4.6 927f43ae61 chore(db): backup database after planning merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:35:32 +01:00
jpmschweitzerandClaude Opus 4.6 64d3d29913 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:27:11 +01:00
jpmschweitzerandClaude Opus 4.6 63bb6ff7c7 chore(db): replace Commonwealth with Settled Reach in tooling and server
Updated docstrings in sqlite_connector, qdrant_connector,
decisions_sync, schema.sql, and two doc comments in generator.rs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:26:59 +01:00
jpmschweitzerandClaude Opus 4.6 3005294c98 docs(docs): replace Commonwealth with Settled Reach across docs
Updated in-universe "Commonwealth" references to "the Settled Reach"
in decisions, architecture docs, design docs, workshop outputs,
README, and wiki. Kept all references to Hamilton's books as
inspiration/comparison in historical discussions and wiki-review
workshop rounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:26:52 +01:00
jpmschweitzerandClaude Opus 4.6 b492410e39 chore(agents): replace Commonwealth with Settled Reach in agent files
The in-universe setting name is "the Settled Reach", not
"Commonwealth" (Hamilton's protected IP). Updated all 17 agent
description lines and intro paragraphs, plus file-specific
references in araminta, gore, ozzie, paula, and tiger.

Kept book references in miri.md (inspiration) and si.md (namesake).
Also updated pr-review and git-commit skill references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:26:41 +01:00
jpmschweitzer 7ba2be0652 Merge remote-tracking branch 'origin/main' into client 2026-03-05 11:08:31 +01:00
jpmschweitzerandClaude Opus 4.6 1bbc07242e chore(db): backup database after PR #84 and #85 merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 11:06:04 +01:00
jpmschweitzer a9dd93764f Merge remote-tracking branch 'origin/server' 2026-03-05 11:05:40 +01:00
jpmschweitzerandClaude Opus 4.6 9ed6094d69 fix(simulation): address PR #85 review — warnings and polish items
- Ticker rotation: document sliding-window semantics (vs modulus-aligned)
- Ticker zone ID: add warning about Gauntlet vs production zone ID mismatch
- Proof-room movement profile: respect archetype instead of hardcoding smuggler
- Storyteller tie-break: use exact f32 equality (inputs are discrete integers)
- Observer: .map().flatten() → .and_then() (clippy strict)
- Content loader: remove dangling doc comment before section header
- Tests: replace assert!(false, ...) with TODO comments in ignored tests
- Tests: add frame limiter note on 302-update loop in tell expiry test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:59:38 +01:00
jpmschweitzerandClaude Opus 4.6 3a481b32a5 fix(copy): address PR #84 review — pacing, prerequisites, terminology
Review fixes for triangle activation monologue lines:

- CHANGELOG: correct smuggler line count (4 → 5), add D-035 ref
- Smuggler comment: align beat labels to 5-line structure
- Smuggler 040: "looking at" → "seeing" for body-first register
- Detective 043: rewrite to remove implicit manifest knowledge
  reference — line now works without fact prerequisite gate
- Detective 044: soften from near-certainty to enumerated
  possibilities with "insufficient data" qualifier
- Triangle comments: align to D-087 terminology (T1: Kael-
  Smuggler-Ring, T2: Sera-Detective-Commission)
- Schema description: note triangle_activated also missing from
  server Situation enum alongside greeting
- D-035 amendment: register triangle-signal, tell-observation
  tags and npc_in_los prerequisite as conventions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 10:56:50 +01:00
jpmschweitzerandClaude Opus 4.6 37c38c0441 chore(simulation): regenerate msgpack fixtures for protocol v19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:13:31 +01:00
jpmschweitzerandClaude Opus 4.6 fd824a1028 test(simulation): Sprint 24 tests — archetype, tell escalation, ticker, v0.1 playthrough (#593, #595)
- 7 archetype→monologue regression tests (smuggler/detective pool partitioning)
- 3 tell escalation unit tests (RoutineDeviation insertion + expiry)
- 6 news ticker tests (pool loading, SimRng rotation, zone gating)
- 3 live integration tests against real server binary (Layer 3)
- Update existing tests for current_ticker field and protocol v19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:13:24 +01:00
jpmschweitzerandClaude Opus 4.6 b04ad93a0f docs(architecture): D-113 tile data model — extensible per-tile properties (#594)
Tile palette + sparse override design. Zero-migration path for existing
location YAMLs. Runtime: TilePalette resource, TileCell with material_id,
sparse TileOverrideMap. Unblocks post-v0.1 door mechanics and visual variants.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:13:11 +01:00
jpmschweitzerandClaude Opus 4.6 3194a6e491 feat(simulation): character archetype, tell escalation, and news ticker (#587, #589, #591)
- Add character_archetype to StartupMessage with serde default (Detective)
- Bump PROTOCOL_VERSION to 19
- Add escalate_tells_on_activation() and expire_routine_deviations() systems
- RoutineDeviation inserted on triangle NPCs with 300-tick TTL
- Add TickerPool resource with deterministic SimRng rotation (200 ticks)
- Emit current_ticker in ObserverSnapshot when player is in bar zone
- Load ticker YAML from district content directories

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:13:03 +01:00
jpmschweitzerandClaude Opus 4.6 46cfec4183 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:06:14 +01:00
jpmschweitzerandClaude Opus 4.6 0472dcb86c feat(copy): triangle activation proximity monologue lines (#597)
Author 4 smuggler and 5 detective monologue lines that fire when
the player observes triangle anchor NPCs post-TriangleActivated.

Smuggler (Kael Davan): physical observation → rationalization →
doubt → sensory confirmation. Contracted, personal, friend-arc.

Detective (Sera Venn/Torek): pattern recognition → deviation →
hypothesis → inference → procedural next step. Analytical,
institutional, evidence-cataloguing.

All lines: situation: [triangle_activated], trigger: observe_npc,
cooldown: 9999, priority: 8, prerequisite: npc_in_los: true.

Schema updated: triangle_activated added to situation enum,
npc_in_los added to prerequisites in monologue-pool.schema.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 09:05:50 +01:00
jpmschweitzer da0fd7c16c Merge remote-tracking branch 'origin/main' into client
# Conflicts:
#	CLAUDE.md
2026-03-05 08:44:05 +01:00
jpmschweitzerandClaude Opus 4.6 3e1bcd90b2 chore(docs): add git command chaining rule to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:43:24 +01:00
jpmschweitzerandClaude Opus 4.6 fc24de6128 fix(client): test harness tile types, bug report screenshot, time_display maxf
TestHarness: remove deprecated tiles/visible_positions keys, add tile
type (floor/wall/door) to visible_tiles, expand radius to 5. Bug report
dialog: capture viewport screenshot before showing overlay, save as
screenshot.png in report bundle. time_display: use maxf() instead of
max() to match float argument types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:41:18 +01:00
jpmschweitzerandClaude Opus 4.6 ac763fef97 feat(engine): live server visual tests and gauntlet snapshot replay
Add live server lifecycle to tests/run-visual (start/stop server per
scenario, parse LISTENING:{port}). Add MessagePack snapshot replay to
visual_capture.gd via Protocol.decode_snapshot() — exercises the full
client pipeline from wire bytes to rendered fog. Three replay scenarios
(hub_spawn, fog_theater, hub_after_movement) plus one live scenario
(fog_live_hub). Add gen_gauntlet_fixtures.rs to produce .msgpack fixtures
from the Gauntlet test world. Add max_diff_pct threshold to visual-diff.
Makefile: add fixtures-gauntlet target, fix build-client double-import,
preserve .godot cache in clean.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:41:09 +01:00
jpmschweitzerandClaude Opus 4.6 2189b00c6f fix(client): move fog blur to CPU pipeline, fix GL compat bilinear on RGBA8
Replaces GPU 7×7/5×5 Gaussian blur (98 texture reads/px) with CPU-side
Gaussian blur (sigma 2.0) + 4× bilinear upscale + RGBA8 convert in
fog_state.gd. GL compatibility mode doesn't bilinear-filter R8 textures;
RGBA8 at 4× resolution resolves this. Squared exp_fade at the
explored/unexplored boundary keeps fog opaque near tile content edges,
fixing the staircase artifact. Shader now does 2 texture reads per pixel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 08:40:57 +01:00
jpmschweitzerandClaude Opus 4.6 a75d9f6c08 chore(meta): plan Sprint 24: Signal
10 tickets across server (6), client (3), copy (1).
Capstone sprint for v0.1 — everything converges on a full
playthrough from main menu through storyteller activation.

Closed stale epics: #38, #369, #455, #575, #596.
Sprint goal: wire TriangleActivated into player-visible signal,
thread character archetype through session lifecycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:42:30 +01:00
jpmschweitzerandClaude Opus 4.6 f7852b93ac chore(meta): release v0.1.23
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:27:06 +01:00
jpmschweitzer 69d16e1be3 Merge remote-tracking branch 'origin/visual' 2026-03-04 23:24:33 +01:00
jpmschweitzerandClaude Opus 4.6 1037ea2bf7 docs(meta): add testing preferences to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:16:22 +01:00
jpmschweitzerandClaude Opus 4.6 895a77ae68 fix(assets): address PR #83 review — tile map corrections
Review fixes:
- Terminal: fix sightlines comment col 22 → col 28 (actual window position)
- Last Shift: fix door comment col 21 → col 22, add spatial features
  (bar counter W-stubs cols 1-2, table clusters, card table, ticker mount)
- Maintenance corridors: extend transition corridor from 26 to 40 tiles
  per D-093 spec (grid now 58x6)
- Gate ground: expand customs zone from 8 to 10 rows per D-093 spec,
  correct freight (20 tiles west) / ped (10 tiles east) layout with
  6-tile corridor between (grid now 40x34)
- Gate gallery: add staircase entrance tile (col 1 row 0 = R)
- District description: update from "Three social sites" to five locations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:10:48 +01:00
jpmschweitzer f82f83ae0e Merge remote-tracking branch 'origin/maintenance'
# Conflicts:
#	.claude/skills/sprint-start/SKILL.md
2026-03-04 23:08:10 +01:00
jpmschweitzerandClaude Opus 4.6 749e16f379 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:01:26 +01:00
jpmschweitzerandClaude Opus 4.6 1db3677b30 chore(meta): reframe worktree boundaries as team identity
Replace worktree-centric language with team-centric framing across
CLAUDE.md and skills. Agents now identify by $WORKTREE_TEAM env var
instead of resolving git internals. This prevents agents from
following .git pointers back to the main repo and crossing boundaries.

- CLAUDE.md: rename section to "Team boundaries", reference $WORKTREE_TEAM
- sprint-start: add TEAM BOUNDARY rule to agent spawn prompt
- sprint-plan: replace "worktree-relative paths" with "relative paths only"
- pr-review: replace "worktree" with "team directory", note cross-dir
  reading is a main-team privilege only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 23:01:13 +01:00
jpmschweitzerandClaude Opus 4.6 610dd78ff1 feat(assets): author tile maps for Sova Transit District (#582, #583)
Complete tile data for all five locations in the transit district:
- The Terminal (44x28, z=1): logistics hub with supervisor office LOS window
- The Last Shift (34x22, z=1): bar with corner booth and back room alley exit
- Maintenance Corridors (44x6, z=0): restricted storage, hatch room, transition corridor
- Gate Ground (40x32, z=1): aperture chamber, staging, customs lanes, concourse
- Gate Gallery (32x10, z=2): Commission-only observation gallery

Gate corridor split into two files (gate-ground, gate-gallery) because
the Location struct supports only one tile_bounds per file.

Updated district.yaml locations list with gate-ground and gate-gallery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:47:43 +01:00
jpmschweitzerandClaude Opus 4.6 1f52f0b00d chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:32:07 +01:00
jpmschweitzerandClaude Opus 4.6 388df8000a fix(client): debug console review fixes — D-088 pause, settings state, response guard
- Add D-088 Overlay pause/unpause signals to DebugConsole, wire in main.gd
  so sim does not advance while typing debug commands
- Settings dialog reads live DebugConsole.is_enabled() instead of ConfigFile
  directly, preventing checkbox/state divergence
- append_response respects disabled state — no auto-open when user disabled
  console via settings
- tp command warns on invalid z value instead of silently defaulting to 0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:31:52 +01:00
jpmschweitzerandClaude Opus 4.6 1feec914b7 fix(client): bump PROTOCOL_VERSION 17 → 18 to match server
Server #580 bumped to 18 for debug_response field. Client was still
at 17, causing every snapshot to be rejected — game unplayable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:29:38 +01:00
jpmschweitzerandClaude Opus 4.6 e9dba609c9 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:33:32 +01:00
jpmschweitzerandClaude Opus 4.6 33b26c1a15 test(client): add BoundaryWall fog tests and document tile_renderer behavior (#585)
4 new tests in test_fog_sprint22.gd verify BoundaryWall tiles populate
boundary_positions (not visible_positions), get VIS_FORWARD without
EXP_VISIBLE, stay EXP_UNEXPLORED after leaving LOS, and clear on new
snapshot. Comment in tile_renderer.gd documents implicit rendering path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:33:08 +01:00
jpmschweitzerandClaude Sonnet 4.6 475280191c fix(ui): render LOS boundary wall tiles through fog without marking explored (#585)
- game_state.gd: add boundary_positions Dictionary field; BoundaryWall tiles from
  visible_tiles go to boundary_positions instead of visible_positions — rendered by
  tile_renderer but not tracked as explored fog memory
- fog_state.gd: update_from_state() writes VIS_FORWARD for boundary_positions so fog
  lifts over margin wall content; boundary tiles excluded from exploration step so they
  don't persist as EXP_EXPLORED when player turns away
- tile_renderer.gd: no changes needed — renders all visible_tiles by type, sector-agnostic
- test_fog_shader.gd: 4 new tests — boundary excluded from visible_positions, tracked in
  boundary_positions, cleared each snapshot, fog lifts to VIS_FORWARD

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 20:32:32 +01:00
jpmschweitzerandClaude Sonnet 4.6 c3abf32185 feat(ui): add in-game debug console with tilde toggle and command dispatch (#581)
- debug_console.gd: new ModalLayer Control — tilde key toggles bottom-40% panel,
  command history (up/down), SimBridge dispatch for all DebugCommandKind variants:
  ticks, contaminate, tp, activate, triangle, npc, triangles, pop, status, help
- debug_console.tscn: minimal scene node; UI built programmatically in _ready()
- input_mapper.gd: DEBUG_COMMAND action added to Action enum
- sim_bridge.gd: DEBUG_COMMAND → "DebugCommand" wire mapping
- protocol.gd: v18 debug_response decode (command, text, success fields)
- game_state.gd: debug_response field + apply_snapshot one-shot handling
- main.gd: @onready ref, router registration, _consume_debug_response(), settings signal
- settings_dialog.gd: debug_console_toggled signal + CheckButton toggle row (+36px height),
  reads initial state from user://settings.cfg; CheckButton state loaded from PREFS_PATH
- main.tscn: DebugConsole node on ModalLayer, load_steps 27→28

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:50:23 +01:00
jpmschweitzerandClaude Sonnet 4.6 60d2c5bb90 fix(ui): correct Sprite2D/self_modulate assertions in P2 client tests (#574)
Same Sprite2D correction applied to test_client_p2.gd entity color tests
(Terrain, Player) — ColorRect was replaced with Sprite2D in entity_renderer.gd.
Minor comment clarification in test_rendering.gd rotation test.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:50:08 +01:00
jpmschweitzerandClaude Sonnet 4.6 d30ab62bc0 fix(ui): clear color registry on conversation end, add speaker color tests (#573)
- Reset _npc_entity_colors/_npc_entity_id/_next_npc_color in _end_player_conversation()
  to prevent palette exhaustion across long sessions with many unique NPCs
- Re-enforce contrast floor after passive desaturation (_enforce_contrast after _desaturate)
- Add TestDialogueSpeakerColors suite: palette allocation, entity reuse, reset, fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 09:50:02 +01:00
jpmschweitzerandClaude Sonnet 4.6 974bb2b28e feat(ui): bind dialogue speaker colors to entity identity (#573)
Maintains Dict[entity_id → Color] in dialogue_box for player conversations.
On first encounter, assigns a round-robin palette color; reuses on subsequent lines.
Eliminates position-based name-hash coloring for player dialogue.

Changes:
- Add _npc_entity_colors dict, _npc_entity_id, _next_npc_color fields
- Add _assign_npc_color(entity_id) — registers palette color on first encounter
- show_dialogue: accept npc_entity_id param, register entity color
- append_line: optional speaker_entity_id/target_entity_id stored in log entries
- append_player_line: pass _npc_entity_id as target_entity_id
- append_dialogue_response: accept entity_id, register, pass to append_line
- _format_entry else branch: look up _npc_entity_colors before name-hash fallback
- main.gd: pass _last_dialogue_npc_id to show_dialogue, speaker_entity_id to append_dialogue_response

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 02:05:15 +01:00
jpmschweitzerandClaude Sonnet 4.6 87b5cbb6c2 fix(ui): correct entity renderer test assertions for Sprite2D and zero offsets (#574)
Fixes 7 pre-existing failures in test_entity_renderer tests:
- ColorRect → Sprite2D cast; .color → .self_modulate for D-033 color checks
- Position offset: (TILE_SIZE-24)/2 → EntityRenderer.ENTITY_OFFSET_{X,Y} (0.0)
- Rotation accuracy: expected values updated for raw un-normalised Godot rotation
Also adds SoundIndicatorRenderer to global_script_class_cache.cfg so test_rendering.gd parses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-04 02:04:18 +01:00
jpmschweitzerandClaude Opus 4.6 ac68ec6eef fix(simulation): address PR #81 review — critical and high-priority issues
Critical fixes:
- storyteller: replace .expect() with guard + log in activation_pass (Hoshe #1)
- content/loader: validate inverted tile_bounds before iteration (Hoshe #C)
- save_io: persist ActivationState on save/load (Tyre #7)

High-priority fixes:
- storyteller: f64 intermediate for observation_time_ticks scoring (Hoshe #A)
- storyteller: deduplicate copresent entities before scoring (Hoshe #B)
- storyteller: skip activation_pass at tick 0 (Hoshe #G)
- storyteller: explicit .before(advance_tick) ordering (Tyre #9)
- save_state: insert EngagementRecord on NPC deserialize (Tyre #10)
- save_io: reset TriangleActivatedQueue + MovementHistoryBuffer on load (Tyre #8)
- debug: validate teleport target walkability (Hoshe #E)
- debug: reject SkipToContamination when tick past delay (Hoshe #F)
- debug: DebugEnabled defaults to cfg!(debug_assertions) (Hoshe #3)
- debug: log when response overwritten (Hoshe #2)
- content/loader: error on tile dimension mismatch (Hoshe #5)
- content/types: DistrictMeta.description optional (Hoshe #D)
- types: version docs updated to v18 (Tyre #1, #2, #3)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 23:40:59 +01:00
jpmschweitzerandClaude Opus 4.6 eb64f23b77 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:13:12 +01:00
jpmschweitzerandClaude Opus 4.6 c797a72fc2 chore(simulation): update tests and fixtures for Sprint 23
- Add debug_response: None to all ObserverSnapshot constructors in
  integration tests
- Bump PROTOCOL_VERSION assertion 17 → 18 in serialization tests
- Regenerate golden proof_room_tick_10.json (BoundaryWall tiles)
- Regenerate client msgpack fixtures for new snapshot fields
- Fix debug.rs resource optionality (Option<ResMut> for
  ContaminationActive/EventQueue)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:12:19 +01:00
jpmschweitzerandClaude Opus 4.6 c30197db0e feat(simulation): add debug console server and LOS boundary wall margin
Debug and LOS tracks for Sprint 23 (#580, #584):

- DebugCommandKind enum with 10 variants (AdvanceTicks,
  SkipToContamination, TeleportToPosition, ForceContaminationActivate,
  InspectNpc, ListTriangles, ListPopulation, GetContaminationStatus,
  TeleportToLocation, ForceTriangleActivation)
- DebugResponsePayload on ObserverSnapshot, handle_debug_commands
  system gated by DebugEnabled resource
- PROTOCOL_VERSION bumped 17 → 18
- VisibilitySector::BoundaryWall variant — 1-tile wall margin beyond
  LOS boundary included in visible_tiles (not exploration/memory)
- compute_boundary_walls() pass in NaturalVision after FOV+cone

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:12:08 +01:00
jpmschweitzerandClaude Opus 4.6 50f5d6c22e feat(simulation): add storyteller engagement tracking and activation pass
Storyteller chain for Sprint 23 (#570, #571, #572, #579):

- EngagementRecord component: per-NPC observation_time_ticks,
  conversation_count, monologue_trigger_count — incremented by
  perception, dialogue, and monologue systems
- MovementHistoryBuffer resource: ring buffer of player positions
  over last 3000 ticks with npcs_copresent_in_window() query
- Lifecycle rules: single activation per session, no concurrency,
  no cooldown, terminal resolution constants
- activation_pass() system: gate check, proximity query, engagement
  scoring, unentangled-NPC routing, module selection, emits
  TriangleActivatedEvent on 10-tick cadence after contamination

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:11:54 +01:00
jpmschweitzerandClaude Opus 4.6 7fbfc5ae65 feat(simulation): add tile-type layer, content loader, and chunk streaming
Spatial chain for Sprint 23 (#576, #577, #578):

- TileKind enum (Floor/Wall/Void/Restricted) on WalkabilityMap with
  set_tile_kind/tile_kind API, backward-compatible with existing
  is_walkable/set_walkable
- Location YAML tile format: tiles as string arrays (F/W/V/R chars),
  load_location_tiles() stamps tile data onto WalkabilityMap from
  ContentStore on production startup
- Chunk streaming system: ChunkLoadRadius + ChunkStreamingCadence
  resources, loads/unloads chunks around player position on cadence.
  v0.1 radius covers full district (no streaming stutter)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 19:11:42 +01:00
jpmschweitzerandClaude Opus 4.6 b6c255c8cb chore(meta): plan Sprint 23: Terrain
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 17:28:48 +01:00
232 changed files with 28705 additions and 443 deletions
+5 -5
View File
@@ -6,7 +6,7 @@ model: sonnet
memory: project
---
You are ARAMINTA, the Visual Designer on a game development project set in Peter F. Hamilton's Commonwealth universe.
You are ARAMINTA, the Visual Designer on a game development project set in the Settled Reach universe.
## Your personality
@@ -32,8 +32,8 @@ Named after Araminta from the Void Trilogy - practical, good aesthetic instincts
## Design principles
- **Clarity over beauty**: the player needs to READ the game state at a glance. No decoration that obscures information.
- **Diegetic first**: UI elements should feel like they belong in the Commonwealth world (insert overlays, not floating HP bars)
- **Mood through restraint**: the Commonwealth is sleek, advanced, subtle. Not grimdark, not neon. Clean lines, muted palettes, occasional stark contrast for danger.
- **Diegetic first**: UI elements should feel like they belong in the Settled Reach world (insert overlays, not floating HP bars)
- **Mood through restraint**: the Settled Reach is sleek, advanced, subtle. Not grimdark, not neon. Clean lines, muted palettes, occasional stark contrast for danger.
- **Consistency compounds**: small rules applied everywhere create coherence. One accent color for danger, one for opportunity, one for unknown.
- **Scale gracefully**: every visual decision should work at boxes-with-labels AND at full-art fidelity. Don't paint yourself into a corner.
@@ -45,9 +45,9 @@ You have access to the `/asset-gen` skill which uses the `generate_image` MCP to
- Style-consistent assets using prompt prefixes and category templates
The existing skill is configured for a different project (Lords of Ash / CK3 Mistborn mod). You will need to:
1. Create a NEW style guide for the Commonwealth project (`references/style-guide.md`)
1. Create a NEW style guide for the Settled Reach project (`references/style-guide.md`)
2. Create new category templates appropriate for this game's asset types
3. Adapt the prompt assembly workflow for Commonwealth aesthetics
3. Adapt the prompt assembly workflow for Settled Reach aesthetics
**IMPORTANT: Image generation incurs costs on an external API. ALWAYS ask the Team Leader (Jeroen) for explicit permission before generating any images. Never generate assets speculatively or in batch without approval. Present your prompt and intent first, get a go-ahead, then generate.**
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: dudley
description: Server Developer for the Commonwealth game project. STANDBY - activate when simulation implementation begins. Responsible for the game simulation server, entity systems, information boundaries, deterministic tick processing, and all server-side game logic.
description: Server Developer for the Settled Reach game project. STANDBY - activate when simulation implementation begins. Responsible for the game simulation server, entity systems, information boundaries, deterministic tick processing, and all server-side game logic.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are DUDLEY, the Server Developer on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are DUDLEY, the Server Developer on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: gestalt
description: Systems Design and Fun Factor specialist for the Commonwealth game project. Use when designing game mechanics, evaluating whether systems create interesting decisions, mapping concepts to concrete mechanics, defining how systems interact, or when someone needs to ask "is this fun?" Use proactively when implementation discussions need mechanical grounding.
description: Systems Design and Fun Factor specialist for the Settled Reach game project. Use when designing game mechanics, evaluating whether systems create interesting decisions, mapping concepts to concrete mechanics, defining how systems interact, or when someone needs to ask "is this fun?" Use proactively when implementation discussions need mechanical grounding.
tools: Read, Glob, Grep, Edit, Write
model: sonnet
memory: project
---
You are GESTALT, the Systems Designer on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are GESTALT, the Systems Designer on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+3 -3
View File
@@ -1,12 +1,12 @@
---
name: gore
description: Themes and Endgame Design specialist for the Commonwealth game project. Use when discussing ascension paths, the philosophical questions the game explores, what the game is fundamentally ABOUT, late-game transformation mechanics, or when the team needs someone to zoom out and reframe the question at a higher level.
description: Themes and Endgame Design specialist for the Settled Reach game project. Use when discussing ascension paths, the philosophical questions the game explores, what the game is fundamentally ABOUT, late-game transformation mechanics, or when the team needs someone to zoom out and reframe the question at a higher level.
tools: Read, Glob, Grep
model: sonnet
memory: project
---
You are GORE, the Themes and Endgame Design specialist on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are GORE, the Themes and Endgame Design specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
@@ -29,7 +29,7 @@ Named after Gore Burnelli - the dynasty patriarch who sees further than anyone,
- **Evolution of intelligence**: Baseline → Rejuvenated → Higher → ANA → ??? What does your civilization/character become?
- **The price of power**: Every ascension path gives something and takes something. Going Higher means losing some humanity. ANA means leaving physicality. The Void offers everything but threatens the galaxy.
- **Post-scarcity choices**: When survival is solved, what do you DO? The Commonwealth's central question.
- **Post-scarcity choices**: When survival is solved, what do you DO? The Settled Reach's central question.
- **Hubris**: Characters and civilizations that think they've transcended their limits, then discover they haven't.
- **The spectrum of existence**: Silfen (nature/mystery), Raiel (duty/stasis), Anomine (ascension/disappearance), Primes (competition/annihilation) - each represents a different answer to "what is intelligence for?"
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: hoshe
description: QA Engineer and Test specialist for the Commonwealth game project. Use when tests need to be written, test plans created, bugs investigated, test reports generated, or when implementation needs verification against specifications. NOT part of brainstorming discussions - spawned for testing and quality assurance work.
description: QA Engineer and Test specialist for the Settled Reach game project. Use when tests need to be written, test plans created, bugs investigated, test reports generated, or when implementation needs verification against specifications. NOT part of brainstorming discussions - spawned for testing and quality assurance work.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are HOSHE, the QA Engineer on a game development project set in Peter F. Hamilton's Commonwealth universe.
You are HOSHE, the QA Engineer on a game development project set in the Settled Reach universe.
## Your personality
+1 -1
View File
@@ -1,6 +1,6 @@
---
name: inigo
description: Sound Designer for the Commonwealth game project. STANDBY - activate when audio implementation begins. Responsible for soundscape design, ambient audio layers, diegetic sound cues, audio propagation rules, and all player-facing audio. Use when designing sound palettes, defining audio triggers, creating spatial audio specs, or reviewing audio consistency.
description: Sound Designer for the Settled Reach game project. STANDBY - activate when audio implementation begins. Responsible for soundscape design, ambient audio layers, diegetic sound cues, audio propagation rules, and all player-facing audio. Use when designing sound palettes, defining audio triggers, creating spatial audio specs, or reviewing audio consistency.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: justine
description: Polish and Deployment specialist for the Commonwealth game project. STANDBY - activate when builds need packaging, performance needs optimizing, or release preparation begins. Responsible for build pipelines, performance profiling, platform packaging, and release quality.
description: Polish and Deployment specialist for the Settled Reach game project. STANDBY - activate when builds need packaging, performance needs optimizing, or release preparation begins. Responsible for build pipelines, performance profiling, platform packaging, and release quality.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are JUSTINE, the Polish and Deployment specialist on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are JUSTINE, the Polish and Deployment specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: mellanie
description: Copywriter for the Commonwealth game project. STANDBY - activate when game text needs writing - internal monologue lines, dialogue, descriptions, UI text, tutorial text, news ticker content. Responsible for all in-game written content.
description: Copywriter for the Settled Reach game project. STANDBY - activate when game text needs writing - internal monologue lines, dialogue, descriptions, UI text, tutorial text, news ticker content. Responsible for all in-game written content.
tools: Read, Glob, Grep, Edit, Write
model: sonnet
memory: project
---
You are MELLANIE, the Copywriter on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are MELLANIE, the Copywriter on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: nigel
description: Sandbox and Replayability advocate for the Commonwealth game project. Use when evaluating whether features create emergent stories, when discussing how systems produce different experiences across playthroughs, when considering procedural generation, or when the team needs someone to ask "what happens the SECOND time you play this?"
description: Sandbox and Replayability advocate for the Settled Reach game project. Use when evaluating whether features create emergent stories, when discussing how systems produce different experiences across playthroughs, when considering procedural generation, or when the team needs someone to ask "what happens the SECOND time you play this?"
tools: Read, Glob, Grep
model: sonnet
memory: project
---
You are NIGEL, the Sandbox and Replayability advocate on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are NIGEL, the Sandbox and Replayability advocate on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: oscar
description: Networking Developer for the Commonwealth game project. STANDBY - activate when networking/multiplayer work begins. Responsible for client-server communication, network protocol design, sync mechanisms, and ensuring the architecture supports future multiplayer.
description: Networking Developer for the Settled Reach game project. STANDBY - activate when networking/multiplayer work begins. Responsible for client-server communication, network protocol design, sync mechanisms, and ensuring the architecture supports future multiplayer.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are OSCAR, the Networking Developer on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are OSCAR, the Networking Developer on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+3 -3
View File
@@ -1,12 +1,12 @@
---
name: ozzie
description: Player Experience and "Wow Factor" advocate for the Commonwealth game project. Use when evaluating whether features are exciting, when the team needs a gut-check on whether something will feel good to play, or when designs risk being technically correct but emotionally flat. Champions the moments that make players feel something.
description: Player Experience and "Wow Factor" advocate for the Settled Reach game project. Use when evaluating whether features are exciting, when the team needs a gut-check on whether something will feel good to play, or when designs risk being technically correct but emotionally flat. Champions the moments that make players feel something.
tools: Read, Glob, Grep
model: sonnet
memory: project
---
You are OZZIE, the Player Experience and "Wow Factor" advocate on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are OZZIE, the Player Experience and "Wow Factor" advocate on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
@@ -22,7 +22,7 @@ You're named after Ozzie Isaacs - the wanderer, the dreamer, the one who walks t
- Champion the big emotional beats: the Dyson barriers opening, first contact with MorningLightMountain, walking through a wormhole, the Starflyer reveal
- Push back when designs are technically correct but emotionally flat
- Advocate for the player's first impression and ongoing engagement
- Remind the team that the game needs to FEEL like the Commonwealth, not just simulate it
- Remind the team that the game needs to FEEL like the Settled Reach, not just simulate it
- Be the voice of "but what does the player actually DO and does it feel good?"
## What you care about
+3 -3
View File
@@ -1,12 +1,12 @@
---
name: paula
description: Narrative and Political Depth specialist for the Commonwealth game project. Use when designing conversation systems, faction mechanics, character relationships, political intrigue, consequences of player actions, or narrative structure. Focused on the human drama and ensuring choices have meaningful weight.
description: Narrative and Political Depth specialist for the Settled Reach game project. Use when designing conversation systems, faction mechanics, character relationships, political intrigue, consequences of player actions, or narrative structure. Focused on the human drama and ensuring choices have meaningful weight.
tools: Read, Glob, Grep, WebSearch
model: sonnet
memory: project
---
You are PAULA, the Narrative and Political Depth specialist on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are PAULA, the Narrative and Political Depth specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
@@ -22,7 +22,7 @@ Named after Paula Myo - the investigator who never gives up, who follows the thr
- Define faction mechanics and how factions interact, grow, and die
- Ensure character relationships have mechanical depth (not just +/- opinion bars)
- Advocate for consequences - player actions should ripple through the social fabric
- Design the political landscape of the Commonwealth as a playable space
- Design the political landscape of the Settled Reach as a playable space
- Push for narrative moments that emerge from systems, not just scripted events
- Champion the Starflyer conspiracy as a narrative experience
- Ensure the internal monologue system reflects character psychology
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: qatux
description: Documenter and Librarian for the Commonwealth game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains decisions/ domain files, DISCUSSION.md, briefings, and the Qdrant search index.
description: Documenter and Librarian for the Settled Reach game project. Use when discussion decisions need to be recorded, when documents need updating, when the team needs a summary of current state, when open questions need tracking, when searching project history, or when answering "did we already discuss this?". Maintains decisions/ domain files, DISCUSSION.md, briefings, and the Qdrant search index.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are QATUX, the Documenter and Librarian on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are QATUX, the Documenter and Librarian on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: si
description: Project Manager and Scrum Master for the Commonwealth game project. Use when creating or managing tickets, planning sprints, breaking initiatives into epics/stories/tasks, tracking progress, or coordinating work across agents. Primary user of the /ticket skill. Does not participate in design discussions - coordinates execution.
description: Project Manager and Scrum Master for the Settled Reach game project. Use when creating or managing tickets, planning sprints, breaking initiatives into epics/stories/tasks, tracking progress, or coordinating work across agents. Primary user of the /ticket skill. Does not participate in design discussions - coordinates execution.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are SI, the Project Manager and Scrum Master on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are SI, the Project Manager and Scrum Master on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: stig
description: UI Developer for the Commonwealth game project. STANDBY - activate when UI implementation begins. Responsible for insert/minimap UI, perception mode overlays, internal monologue display, HUD elements, and all player-facing interface code.
description: UI Developer for the Settled Reach game project. STANDBY - activate when UI implementation begins. Responsible for insert/minimap UI, perception mode overlays, internal monologue display, HUD elements, and all player-facing interface code.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are STIG, the UI Developer on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are STIG, the UI Developer on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+3 -3
View File
@@ -1,12 +1,12 @@
---
name: tiger
description: Translator and Localization specialist for the Commonwealth game project. STANDBY - activate when the game needs localization to other languages. Responsible for translation, localization infrastructure, and cultural adaptation of game text.
description: Translator and Localization specialist for the Settled Reach game project. STANDBY - activate when the game needs localization to other languages. Responsible for translation, localization infrastructure, and cultural adaptation of game text.
tools: Read, Glob, Grep, Edit, Write
model: sonnet
memory: project
---
You are TIGER, the Translator and Localization specialist on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are TIGER, the Translator and Localization specialist on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
@@ -23,7 +23,7 @@ Named after Tiger Pansy - the Silfen who bridges between human and Silfen unders
- Maintain translation memory and glossary
- Coordinate with Mellanie on source text clarity for translation
- Flag source text that will be difficult to localize before it's finalized
- Define naming conventions for Commonwealth-specific terms across languages
- Define naming conventions for Settled Reach-specific terms across languages
## Localization principles
+2 -2
View File
@@ -1,12 +1,12 @@
---
name: tyre
description: Technical Architect and Feasibility specialist for the Commonwealth game project. Use when evaluating engine choices, assessing technical feasibility of features, designing system architecture, discussing performance implications, or when the team needs a reality check on scope. Also use proactively for any implementation planning or code architecture decisions.
description: Technical Architect and Feasibility specialist for the Settled Reach game project. Use when evaluating engine choices, assessing technical feasibility of features, designing system architecture, discussing performance implications, or when the team needs a reality check on scope. Also use proactively for any implementation planning or code architecture decisions.
tools: Read, Glob, Grep, Edit, Write, Bash, WebSearch, WebFetch
model: opus
memory: project
---
You are TYRE, the Technical Architect on a game development team building a top-down immersive sim set in Peter F. Hamilton's Commonwealth universe.
You are TYRE, the Technical Architect on a game development team building a top-down immersive sim set in the Settled Reach universe.
## Your personality
+1 -1
View File
@@ -93,7 +93,7 @@ Updated briefings for Tyre and Troblum with new requirements.
chore(agents): add Stig UI developer agent
Standby agent for UI implementation phase. Configured with
briefing reference and Commonwealth-themed personality.
briefing reference and Settled Reach-themed personality.
```
## CHANGELOG.md Format
+9 -9
View File
@@ -74,35 +74,35 @@ If the diff is empty, report "No changes to review" and stop.
Three-dot diff with pathspec exclusions is unreliable. Instead, either:
1. Use `git diff main...<branch>` (full diff) and filter in the prompt, or
2. Read source files directly from the branch worktree (see below).
2. Read source files directly from the team directory (see below).
For large diffs (>1000 lines of source), provide **source files** rather than
raw diff to reviewers — cleaner context, better reviews.
**IMPORTANT — use worktree paths for ALL agents.** This project uses git
worktrees. Each team branch is checked out at:
**IMPORTANT — use team directory paths for ALL agents.** Each team branch
is checked out in its own directory at:
```
/var/mnt/data/projects/settled-reach/<branch>/
```
For example, the `copy` branch lives at:
For example, the `copy` team directory is at:
```
/var/mnt/data/projects/settled-reach/copy/content/dialogue/...
```
**All reviewer agents** (regardless of Bash access) should read source files
from the worktree path using the Read tool. This is more reliable than
from the team directory using the Read tool. This is more reliable than
`git show origin/<branch>:<path>` because:
- All agents have Read access (no Bash dependency)
- Files are always the actual branch checkout (no stale cache)
- No risk of accidentally reading from main's working directory
When constructing reviewer prompts, tell agents to read files from the
worktree path. Example instruction for agents:
team directory. Example instruction for agents:
```
Read the changed files from the branch worktree. The branch is checked
Read the changed files from the team directory. The branch is checked
out at: /var/mnt/data/projects/settled-reach/<branch>/
For example, to read `content/dialogue/the-terminal/kael-davan.yaml`,
@@ -110,8 +110,8 @@ use: /var/mnt/data/projects/settled-reach/<branch>/content/dialogue/the-terminal
```
Also tell agents to read relevant `decisions/*.md` files from the same
worktree (they're identical to main, but using the worktree path keeps
agents grounded in the correct directory).
directory (they're identical to main, but using the team directory path
keeps agents grounded in the correct location).
### 4. Spawn reviewers in parallel
@@ -2,20 +2,24 @@
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
**All agents read from worktree paths.** Each branch is checked out at:
**All reviewer agents read from team directories.** Each team branch is
checked out in its own directory at:
`/var/mnt/data/projects/settled-reach/<branch>/`
Tell every reviewer agent to read source files from the worktree using the
Read tool. Include the worktree base path and a list of changed files in
every prompt. Do NOT rely on `git show` or paste file contents — agents
can read directly from the worktree.
Tell every reviewer agent to read source files from the team directory
using the Read tool. Include the directory path and a list of changed
files in every prompt. Do NOT rely on `git show` or paste file contents —
agents can read directly from the directory.
Note: cross-directory reading is only permitted for review agents spawned
from the `main` team. Team agents must stay within their own directory.
## Code reviews (`server`, `client`, `ci`)
**Hoshe (Code Quality)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
Tell Hoshe to read source files from the worktree, then review for:
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Hoshe to read source files from the team directory, then review for:
- Correctness and bug risks
- Error handling gaps
- Test coverage (are new features tested?)
@@ -25,8 +29,8 @@ can read directly from the worktree.
**Tyre (Architecture)**
- `subagent_type`: `tyre`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
Tell Tyre to read the relevant `decisions/*.md` files from the worktree
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Tyre to read the relevant `decisions/*.md` files from the team directory
first, then review for:
- Architectural consistency with project decisions
- API/interface design quality
@@ -38,8 +42,8 @@ can read directly from the worktree.
**Hoshe (QA)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
Tell Hoshe to read the changed files from the worktree, then review for:
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Hoshe to read the changed files from the team directory, then review for:
- Formatting consistency (markdown, file naming, frontmatter)
- Broken references or links
- Spelling and grammar
@@ -48,9 +52,9 @@ can read directly from the worktree.
**Paula (Narrative Depth)**
- `subagent_type`: `paula`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, commit log, and
- Prompt: Provide the team directory path, list of changed files, commit log, and
list of relevant `decisions/*.md` files to read. Tell Paula to read all
files from the worktree using the Read tool, then review for:
files from the team directory using the Read tool, then review for:
- Narrative quality and character voice consistency
- Whether dialogue and monologue feel authentic to the characters
- Consequences and stakes — do choices carry weight?
@@ -59,9 +63,9 @@ can read directly from the worktree.
**Miri (World Consistency)**
- `subagent_type`: `miri`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, commit log, and
- Prompt: Provide the team directory path, list of changed files, commit log, and
list of relevant `decisions/*.md` files to read. Tell Miri to read all
files from the worktree using the Read tool, then review for:
files from the team directory using the Read tool, then review for:
- Lore accuracy — do facts match established setting?
- Internal consistency across files
- IP originality — nothing should read as a copy from another franchise
@@ -72,8 +76,8 @@ can read directly from the worktree.
**Hoshe (QA)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
Tell Hoshe to read the changed files from the worktree, then review for:
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Hoshe to read the changed files from the team directory, then review for:
- File format and naming conventions
- Asset organization and directory structure
- Missing or broken references in scene/resource files
@@ -81,21 +85,21 @@ can read directly from the worktree.
**Araminta (Art Direction)**
- `subagent_type`: `araminta`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Araminta to read the style guide and relevant design docs from the
worktree first, then review for:
- Visual consistency with the established style guide
- Color palette adherence
- UI pattern consistency (diegetic-first, clarity over beauty)
- Whether assets scale gracefully (boxes-with-labels to full-art)
- Mood and tone — sleek, advanced, subtle Commonwealth aesthetic
- Mood and tone — sleek, advanced, subtle Settled Reach aesthetic
## Audio reviews (`audio`)
**Hoshe (QA)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
Tell Hoshe to read the changed files from the worktree, then review for:
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Hoshe to read the changed files from the team directory, then review for:
- File format and naming conventions
- Audio asset organization and directory structure
- Missing or broken references
@@ -103,11 +107,11 @@ can read directly from the worktree.
**Ozzie (Player Experience)**
- `subagent_type`: `ozzie`, `model`: `sonnet`
- Prompt: Provide the worktree path, list of changed files, and commit log.
Tell Ozzie to read all files from the worktree using the Read tool, then
- Prompt: Provide the team directory path, list of changed files, and commit log.
Tell Ozzie to read all files from the team directory using the Read tool, then
review for:
- Emotional impact — does the audio enhance the moment?
- Atmosphere and tone — does it feel like the Commonwealth?
- Atmosphere and tone — does it feel like the Settled Reach?
- Player feedback clarity — can the player tell what just happened?
- Pacing — do sounds support or fight the gameplay rhythm?
- Memorable moments — will players remember these audio cues?
+20 -7
View File
@@ -154,13 +154,12 @@ Create `docs/sprints/sprint-N/` and write one file per team.
Read the template at `references/briefing-template.md` in this skill directory
for the exact file structure.
**IMPORTANT — worktree-relative paths:** This project uses git worktrees.
Each team branch is checked out in its own worktree, and each worktree
contains the full repo (`server/`, `client/`, `docs/`, etc.). All file
paths in briefings must be relative to the worktree/git root. Example:
**IMPORTANT — relative paths only:** Each team works in its own directory
containing the full repo (`server/`, `client/`, `docs/`, etc.). All file
paths in briefings must be relative to the working directory. Example:
`server/src/bridge/types.rs`, not `/absolute/path/to/server/src/...` or
paths that navigate outside the git root (`../sibling-worktree/...`).
Agents must stay within the git root they are running in.
paths that navigate outside (`../sibling-dir/...`).
Agents must stay within their team's working directory.
Key requirements per file:
- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes
@@ -193,7 +192,20 @@ tooling/db/ticket sprint assign <ticket_id> <sprint_id>
The sprint stays in `planning` status until explicitly activated via
`tooling/db/sprint start`. This prevents starting an unplanned sprint.
### 8. Present summary
### 8. Commit and push
Stage the briefing files and any other changes (db backup, closed tickets),
then commit and push so worktree branches can pull the planning artifacts:
```bash
git add docs/sprints/sprint-N/
make db-backup
git add docs/backups/settledreach.db.backup
git commit -m "chore(meta): plan Sprint N: Theme"
git push
```
### 9. Present summary
Output:
- Sprint number, theme, and goal
@@ -201,3 +213,4 @@ Output:
- Carry-over count
- Open questions that need early resolution
- Files written
- Commit pushed to main
+5
View File
@@ -290,6 +290,11 @@ Task(
RULES (NON-NEGOTIABLE):
0. TEAM BOUNDARY: Your team is `{team}` ($WORKTREE_TEAM). Stay
within the current working directory. Do NOT navigate to
parent or sibling directories. Do NOT follow .git pointers
to other directories.
1. 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. No exceptions.
+32
View File
@@ -93,6 +93,38 @@ Wrap-up sequence:
4. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
5. TeamDelete to clean up
## Workshop Format: Interview Mode
When the workshop brief specifies `**Format:** Interview` (or the user requests "interactive interview mode"), the between-rounds flow changes for the interview round:
### How Interview Mode Works
Instead of agents writing responses to each other, the facilitator (team lead) conducts a live interview with the user:
1. **Collect all agent questions** — Read all Round 1 output files to gather every question.
2. **Group thematically** — Organize questions into 5-7 thematic clusters (e.g., "The Vision," "The Confusion Type," "The Emotional Loop"). Questions from different agents that probe similar territory go together.
3. **Present via AskUserQuestion** — Present each group using the `AskUserQuestion` tool, one group at a time (1-3 questions per group). For each question:
- Include the asking agent's name and domain
- Include the full question text with context
- Include the agent's reasoning for why the question matters
- Provide 2-4 option choices that represent distinct answer categories
- Always allow free-text via the "Other" option (automatic)
4. **Capture nuance** — The user's free-text notes often contain the most important insights. Capture these verbatim in the transcript.
5. **Summarize between groups** — After each group, briefly reflect back the key finding before moving to the next group.
6. **Write full transcript** — When all groups are done, write the complete interview to `docs/workshops/{name}/lead-interview.md` with:
- Every question and full answer (verbatim where the user provided free text)
- Key findings per answer
- An interview summary section with the major revelations
- "What Survives" and "What Changes" sections
### Why AskUserQuestion
The user CANNOT see agent messages, task details, or file contents in the conversation. They only see your text output and AskUserQuestion prompts. Present all question context directly — never assume the user has read agent outputs.
### Distributing Interview Results
When creating Round 3 (proposal) tasks after an interview round, include the full transcript path and a summary of the major reframe in every agent's task description. If the user requests it, instruct agents to read the verbatim transcript.
## Agent Type Reference
Agent name maps directly to subagent_type:
+42
View File
@@ -6,6 +6,47 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
## [v0.1.24] — 2026-03-06
### Changed
- Replaced all in-universe "Commonwealth" references with "the Settled Reach" across 44 files (agents, decisions, docs, tooling, server). Historical discussion transcripts and Hamilton book references kept as-is.
### Added
- Character archetype select screen — two-card UI (Smuggler/Detective) between New Game and session start, keyboard+mouse selection, ESC cancels (#588, D-027)
- Triangle activation consumer — urgent monologue chime fires once per triangle per session when triangle_crisis_events received (#590, D-039)
- News ticker HUD — scrolling marquee visible in The Last Shift zone, hidden elsewhere, reads current_ticker from snapshot (#592, D-039)
- Triangle activation proximity monologue lines — 5 smuggler lines (Kael Davan) and 5 detective lines (Sera Venn/Torek Lintar) that fire when observing triangle anchor NPCs post-activation (#597, D-035, D-039)
### Changed
- Protocol version bumped to 19 — StartupMessage includes character_archetype, snapshot includes triangle_crisis_events and current_ticker (#588, #590, #592)
## [v0.1.23] — 2026-03-04
### Added
- TileKind enum (Floor/Wall/Void/Restricted) on WalkabilityMap with per-tile type data alongside walkability (#576, D-094)
- Location YAML tile format — hand-author tiles as string arrays (F/W/V/R characters), loaded into WalkabilityMap on production startup (#577)
- Chunk streaming system — ChunkLoadRadius and cadence-gated load/unload around player position, v0.1 covers full district (#578, D-012)
- EngagementRecord component — per-NPC observation time, conversation count, and monologue trigger count tracked by perception/dialogue/monologue systems (#570)
- MovementHistoryBuffer resource — 3000-tick ring buffer of player positions with co-presence proximity query (#571)
- Storyteller lifecycle rules — single activation per session, no concurrency, terminal resolution constants (#572)
- Storyteller activation_pass() — gate/proximity/engagement scoring/routing/module selection/TriangleActivatedEvent on 10-tick cadence (#579)
- Debug console server — 10 DebugCommandKind variants (AdvanceTicks, SkipToContamination, TeleportToPosition, InspectNpc, ListTriangles, etc.) with DebugResponsePayload on ObserverSnapshot (#580)
- Debug console client — tilde-toggle UI panel with command input, output log, settings toggle, and full DebugCommandKind dispatch via protocol v18 (#581)
- Entity-bound dialogue speaker colors — NPC colors assigned by entity ID (not screen position) with per-conversation lifecycle and round-robin palette (#573)
- Sova Transit District tile maps — 5 locations authored: The Terminal (44×28), The Last Shift (34×22), Maintenance Corridors (58×6), Gate Ground (40×34), Gate Gallery (32×10) (#582, #583)
### Fixed
- LOS boundary walls — 1-tile wall margin beyond vision cone included in visible_tiles as BoundaryWall sector, walls at fog edge now render instead of bleeding into fog (#584)
- LOS boundary walls client — BoundaryWall tiles render through fog without marking explored, 4 new fog tests verify lifecycle (#585)
- Entity renderer test failures — updated 7 stale ColorRect/position assertions for Sprite2D migration, fixed SoundIndicatorRenderer class cache (#574)
- Dialogue speaker color contrast — re-enforce contrast floor after desaturation for passive (overheard) lines
- PROTOCOL_VERSION 17 → 18 mismatch — client rejected every server snapshot
- Debug console D-088 pause — sim now pauses while console is open, matching dialogue/settings overlay behavior
- Debug console settings toggle reads live state instead of ConfigFile, preventing checkbox divergence
### Changed
- PROTOCOL_VERSION bumped 17 → 18 (debug_response field on ObserverSnapshot, DebugCommand PlayerAction variant)
## [v0.1.22] — 2026-03-03
### Added
@@ -167,6 +208,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
- run-ipc-benchmark dead --iterations flag removed (Rust compile-time constant governs rounds)
### Changed
- Team boundary framing — replaced worktree-centric language with `$WORKTREE_TEAM` env var identity across CLAUDE.md and skills (sprint-start, sprint-plan, pr-review) to prevent agents from following `.git` pointers across boundaries
- CLAUDE.md compacted from 188 to 67 lines — CLI references, endpoints, and patterns moved to `.claude/rules/`
- `/sprint-status` delegates to haiku subagent — keeps sweep JSON, template read, and PR list out of main context window
- `sprint sweep` JSON trimmed — removed unused fields (`ok`, `sprint.status`, `priority`, `ticket_id`), shortened issue detail strings
+15 -6
View File
@@ -27,14 +27,16 @@ See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. A
## Agent Instructions
### Worktree boundaries
### Team boundaries
This project uses **git worktrees** in a shared parent directory (`settled-reach/`). Each team branch (`server`, `client`, `copy`, `audio`, `visual`, `ci`) has its own worktree. The worktree root IS the git root.
**Your team identity is `$WORKTREE_TEAM`.** All work must stay within the current working directory.
- **All work must remain within the git root** unless explicitly instructed otherwise.
- All file paths are relative to the worktree root (e.g. `server/src/bridge/types.rs`).
- Do not navigate to or access sibling worktrees (`../client/`, `../copy/`, etc.) unless explicitly instructed.
- **Exception — stale git lock files:** Worktree index locks live in the shared `.git` directory (e.g. `main/.git/worktrees/copy/index.lock`). If a `git` command fails with `index.lock: File exists`, you may remove the lock file for **your own worktree only**. Never touch lock files belonging to other worktrees.
- All file paths are relative to the current working directory (e.g. `server/src/bridge/types.rs`).
- **Do NOT navigate to parent or sibling directories** (`../`, `../client/`, etc.) unless explicitly instructed. Do NOT use absolute paths to reach other team directories.
- **Do NOT write auto-memory files for other teams.** If `$WORKTREE_TEAM` is `server`, do not write to memory paths containing `client`, `main`, etc.
- For context: each team has its own directory via git worktrees, sharing a parent directory (`settled-reach/`). The `.git` file points to a shared git directory — do not follow it to determine your working root.
- **Exception — stale git lock files:** If a `git` command fails with `index.lock: File exists`, you may remove the lock file for **your own team only** (e.g. `main/.git/worktrees/$WORKTREE_TEAM/index.lock`). Never touch lock files belonging to other teams.
- **Never chain git commands** in a single Bash call (e.g. `git add ... && git commit ...`). The shared `.git` directory means concurrent index access from the same terminal creates `index.lock` collisions. Always run `git add` and `git commit` as **separate sequential Bash calls**.
### Database
@@ -60,6 +62,13 @@ The ticketing database (`settledreach.db`) lives in the **parent directory** sha
| Doc search | `tooling/db/qdrant-search "query"` | `/docs-search` skill |
| Doc index | `tooling/db/qdrant-index path/to/file.md` | `/docs-search` skill |
### Testing preferences
- **Prefer live Gauntlet testing over mocks.** For visual tests and rendering verification, use the full client/server pipeline (`--test-mode` + `SR_LIVE=1`) instead of TestHarness mocks. The Gauntlet test world produces production-identical data. Mocks can mask rendering bugs by taking different code paths.
- **Gauntlet rooms are immutable.** Never modify existing rooms — new systems get new rooms. This ensures StableId determinism and fixture stability.
- Three test tiers: (1) Live server — highest fidelity, (2) MessagePack replay via `Protocol.decode_snapshot()` — for unreachable rooms, (3) TestHarness mock — for UI-only tests where fog data doesn't matter.
- `make fixtures-gauntlet` regenerates real server snapshot fixtures from the Gauntlet world.
### File conventions
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
- Decision IDs: `D-NNN` (confirmed), `Q-NNN` (open questions), `R-NNN` (rejected)
+8 -2
View File
@@ -5,7 +5,7 @@ GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
db-backup db-install validate-content content-ron check-fact-ids setup-hooks \
pre-pr pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures \
pre-pr-server pre-pr-client pre-pr-content \
fixtures-client golden-diff golden-update \
fixtures-client fixtures-gauntlet golden-diff golden-update \
checklist-validate checklist-generate \
perf-baseline debug-schedule \
test-ipc-fixtures test-ipc-protocol test-ipc-integration test-ipc-benchmark \
@@ -110,6 +110,9 @@ build-server:
build-client:
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
@# First import may error on theme/font loading before the import scan completes.
@# Run twice: first pass generates imports silently, second pass validates clean.
@$(GODOT) --headless --path client --import --quit 2>/dev/null || true
$(GODOT) --headless --path client --import --quit
# --- Run ---
@@ -144,6 +147,9 @@ test-server:
fixtures:
cd server && cargo test --test gen_fixtures -- --ignored
fixtures-gauntlet:
cd server && cargo test --test gen_gauntlet_fixtures -- --ignored
fixtures-client:
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
@echo "Generating GDScript fixtures for Rust decoder..."
@@ -347,5 +353,5 @@ content-ron:
clean:
cd server && cargo clean || true
rm -rf .cache/*
rm -rf client/.godot/* client/reports
rm -rf client/reports
@echo "Clean complete."
+2 -2
View File
@@ -41,7 +41,7 @@ Your character interprets what they sense in their own voice. Footsteps behind y
Different characters access different sensors. Natural vision shows detail but is blocked by walls. Thermal imaging shows heat signatures with no identity. Camera feeds give remote vision but can be spoofed. Unisphere tracking pings known contacts but can be masked. Each mode reveals different information with different trust tradeoffs.
### Diegetic Interface
The map is your character's neural lattice - Commonwealth technology, not a game UI. Points of interest appear when you learn them through gameplay. Tips can be traps. Navigation is pulled by player intent, not pushed by map design.
The map is your character's neural lattice - Settled Reach technology, not a game UI. Points of interest appear when you learn them through gameplay. Tips can be traps. Navigation is pulled by player intent, not pushed by map design.
### Multiple Playable Characters
Every character starts in a different position with different knowledge and different tools. A cop has case files and legal authority. An investigator has contacts and freedom to operate. A politician has institutional access and public constraints. Replayability comes from perspective, not randomness.
@@ -125,7 +125,7 @@ No fog-of-war as an afterthought. No tutorial popups. No omniscient map reveals.
**Official Title:** The Settled Reach (D-021)
**Repository:** commonwealth (historical code name)
**Engine:** Godot 4 + Rust/bevy_ecs simulation server via subprocess/IPC
**Setting:** Original science fiction IP, Commonwealth-inspired
**Setting:** Original science fiction IP, inspired by space opera traditions
**Status:** Pre-alpha development
For development documentation, see the [decisions/](decisions/) directory and [TEAM.md](TEAM.md).
+7
View File
@@ -205,3 +205,10 @@ character_select:
detective_name: "Commission Investigator"
detective_tagline: "The manifests don't add up. Someone in this district knows why."
confirm: "Begin"
# #588: Card display strings — name, role, tone per archetype
smuggler_card_name: "Smuggler"
smuggler_card_role: "Freight logistics worker — Sova Transit"
smuggler_card_tone: "Insider access. Social camouflage. The ring is your daily life."
detective_card_name: "Detective"
detective_card_role: "Commission investigator — External assignment"
detective_card_tone: "Institutional authority. Analytical lattice. You were sent here."
+189
View File
@@ -0,0 +1,189 @@
[gd_scene load_steps=2 format=3 uid="uid://char_select_scene_sr"]
[ext_resource type="Script" path="res://ui/character_select.gd" id="1_charselect"]
; #588: Character archetype select — two-card overlay between New Game and main.tscn.
; Keyboard: left/right to pick, Enter to confirm, ESC to cancel (no save dir created).
[node name="CharacterSelect" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_charselect")
[node name="Background" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.04, 0.04, 0.07, 0.97)
mouse_filter = 2
[node name="TitleLabel" type="Label" parent="."]
layout_mode = 1
anchor_left = 0.5
anchor_right = 0.5
offset_left = -200.0
offset_top = 100.0
offset_right = 200.0
offset_bottom = 126.0
grow_horizontal = 2
text = "Choose your perspective."
horizontal_alignment = 1
theme_override_font_sizes/font_size = 16
theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0)
[node name="Cards" type="HBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -316.0
offset_top = -110.0
offset_right = 316.0
offset_bottom = 140.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/separation = 24
alignment = 1
; --- Smuggler card ---
[node name="CardSmugglerWrapper" type="Control" parent="Cards"]
layout_mode = 2
custom_minimum_size = Vector2(280, 240)
mouse_filter = 0
[node name="CardBorder" type="ColorRect" parent="Cards/CardSmugglerWrapper"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.18, 0.22, 0.28, 1.0)
mouse_filter = 2
[node name="CardInner" type="ColorRect" parent="Cards/CardSmugglerWrapper"]
layout_mode = 1
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_top = 2.0
offset_right = -2.0
offset_bottom = -2.0
color = Color(0.07, 0.07, 0.10, 1.0)
mouse_filter = 2
[node name="VBox" type="VBoxContainer" parent="Cards/CardSmugglerWrapper/CardInner"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
offset_top = 20.0
offset_right = -20.0
offset_bottom = -20.0
theme_override_constants/separation = 10
[node name="NameLabel" type="Label" parent="Cards/CardSmugglerWrapper/CardInner/VBox"]
layout_mode = 2
text = "Smuggler"
theme_override_font_sizes/font_size = 26
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
[node name="RoleLabel" type="Label" parent="Cards/CardSmugglerWrapper/CardInner/VBox"]
layout_mode = 2
text = "Freight logistics worker — Sova Transit"
autowrap_mode = 2
theme_override_font_sizes/font_size = 13
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
[node name="Divider" type="Control" parent="Cards/CardSmugglerWrapper/CardInner/VBox"]
layout_mode = 2
custom_minimum_size = Vector2(0, 12)
[node name="ToneLabel" type="Label" parent="Cards/CardSmugglerWrapper/CardInner/VBox"]
layout_mode = 2
text = "Insider access. Social camouflage. The ring is your daily life."
autowrap_mode = 2
theme_override_font_sizes/font_size = 12
theme_override_colors/font_color = Color(0.416, 0.447, 0.510, 1.0)
; --- Detective card ---
[node name="CardDetectiveWrapper" type="Control" parent="Cards"]
layout_mode = 2
custom_minimum_size = Vector2(280, 240)
mouse_filter = 0
[node name="CardBorder" type="ColorRect" parent="Cards/CardDetectiveWrapper"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.18, 0.22, 0.28, 1.0)
mouse_filter = 2
[node name="CardInner" type="ColorRect" parent="Cards/CardDetectiveWrapper"]
layout_mode = 1
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 2.0
offset_top = 2.0
offset_right = -2.0
offset_bottom = -2.0
color = Color(0.07, 0.07, 0.10, 1.0)
mouse_filter = 2
[node name="VBox" type="VBoxContainer" parent="Cards/CardDetectiveWrapper/CardInner"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 20.0
offset_top = 20.0
offset_right = -20.0
offset_bottom = -20.0
theme_override_constants/separation = 10
[node name="NameLabel" type="Label" parent="Cards/CardDetectiveWrapper/CardInner/VBox"]
layout_mode = 2
text = "Detective"
theme_override_font_sizes/font_size = 26
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
[node name="RoleLabel" type="Label" parent="Cards/CardDetectiveWrapper/CardInner/VBox"]
layout_mode = 2
text = "Commission investigator — External assignment"
autowrap_mode = 2
theme_override_font_sizes/font_size = 13
theme_override_colors/font_color = Color(0.533, 0.565, 0.627, 1.0)
[node name="Divider" type="Control" parent="Cards/CardDetectiveWrapper/CardInner/VBox"]
layout_mode = 2
custom_minimum_size = Vector2(0, 12)
[node name="ToneLabel" type="Label" parent="Cards/CardDetectiveWrapper/CardInner/VBox"]
layout_mode = 2
text = "Institutional authority. Analytical lattice. You were sent here."
autowrap_mode = 2
theme_override_font_sizes/font_size = 12
theme_override_colors/font_color = Color(0.416, 0.447, 0.510, 1.0)
[node name="ConfirmBtn" type="Button" parent="."]
layout_mode = 1
anchor_left = 0.5
anchor_top = 1.0
anchor_right = 0.5
anchor_bottom = 1.0
offset_left = -60.0
offset_top = -80.0
offset_right = 60.0
offset_bottom = -50.0
grow_horizontal = 2
grow_vertical = 0
text = "Begin"
theme_override_font_sizes/font_size = 15
theme_override_colors/font_color = Color(0.906, 0.773, 0.278, 1.0)
+9 -1
View File
@@ -1,4 +1,4 @@
[gd_scene load_steps=27 format=3 uid="uid://bswrmh7w8dbgm"]
[gd_scene load_steps=29 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"]
@@ -26,6 +26,8 @@
[ext_resource type="PackedScene" path="res://ui/examine_display.tscn" id="24_examine"]
[ext_resource type="PackedScene" path="res://ui/journal_panel.tscn" id="25_journal"]
[ext_resource type="PackedScene" path="res://ui/loading_screen.tscn" id="26_loading"]
[ext_resource type="PackedScene" uid="uid://b2ndm9rvx8cqp" path="res://ui/debug_console.tscn" id="27_debug_console"]
[ext_resource type="PackedScene" uid="uid://news_ticker_scene_sr" path="res://ui/news_ticker.tscn" id="28_newsticker"]
[node name="Game" type="Node2D"]
script = ExtResource("1_main")
@@ -177,6 +179,9 @@ offset_bottom = 400
mouse_filter = 2
script = ExtResource("22_debug")
; #592: News ticker — scrolling headline bar, visible in bar zone only (D-049 z-layer 7)
[node name="NewsTicker" parent="UILayer" instance=ExtResource("28_newsticker")]
; D-056: Cursor state machine — insert-styled geometric cursor, topmost in UILayer
[node name="CursorRenderer" type="Node2D" parent="UILayer"]
script = ExtResource("10_cursor")
@@ -194,3 +199,6 @@ layer = 30
; #257: Loading screen — full-screen overlay during save/load round-trip
[node name="LoadingScreen" parent="ModalLayer" instance=ExtResource("26_loading")]
; #581: Debug console — tilde key toggles, bottom 40% of screen
[node name="DebugConsole" parent="ModalLayer" instance=ExtResource("27_debug_console")]
@@ -10,6 +10,11 @@ extends Node
# Matches sfx_monologue_chime.ogg from D-038 — "neural lattice firing" feel.
const CHIME_RECOGNITION := "sfx_monologue_chime"
# --- D-067: Triangle activation chime (#590, D-072/D-089) ---
# Fires once per session when the triangle's tell_state shifts to RoutineDeviation.
# Sharper variant (D-067: "contradiction/anomaly") — sfx_monologue_chime_urgent.ogg.
const CHIME_ACTIVATION := "sfx_monologue_chime_urgent"
# --- Bus names (D-068) ---
const BUS_MUSIC := "Music"
const BUS_AMBIENT := "Ambient"
+8
View File
@@ -161,6 +161,14 @@ func update_from_state() -> void:
if px < 0 or py < 0 or px >= _width or py >= _height:
continue
_vis_bytes[py * _width + px] = VIS_FORWARD
# #585: BoundaryWall margin tiles — fog lifts so wall content composites correctly,
# but NOT in visible_positions so they don't persist as explored memory.
for pos in GameState.boundary_positions:
var px: int = pos.x - ox
var py: int = pos.y - oy
if px < 0 or py < 0 or px >= _width or py >= _height:
continue
_vis_bytes[py * _width + px] = VIS_FORWARD
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
visibility_texture.update(_vis_image)
+28 -4
View File
@@ -18,7 +18,8 @@ var current_tick: int = 0
var player_position: Vector2 = Vector2.ZERO
var visible_entities: Array = []
var visible_tiles: Array = []
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups
var visible_positions: Dictionary = {} # Vector2i -> true, for fast fog lookups (normal LOS tiles)
var boundary_positions: Dictionary = {} # Vector2i -> true, BoundaryWall margin tiles (#585) — visible in fog but not explored
# v2 fields (D-015, D-031)
var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty
@@ -81,11 +82,21 @@ var rng_seed: Variant = null
# One-shot: consumed by main.gd after display, then set back to null.
var save_result: Variant = null
# v18 fields (#580): debug console response from server.
# {command: String, text: String, success: bool} or null.
# One-shot: consumed by main.gd and forwarded to DebugConsole, then set to null.
var debug_response: Variant = null
# #257: Pending load path — set by main menu "Load Game" selection.
# main.gd sends LOAD_GAME on startup if non-empty, then clears this field.
# Format: user://saves/<game-id>/<filename>.sav or "" if no pending load.
var pending_load_path: String = ""
# #588: Character archetype chosen at character select screen.
# "detective" or "smuggler". Set before game scene loads; sent in StartupMessage.
# Default: "detective" — fallback for legacy saves without character.txt.
var character_archetype: String = "detective"
# v7 fields (#431, D-059/D-060)
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
@@ -311,6 +322,12 @@ func apply_snapshot(snapshot: Dictionary) -> void:
else:
save_result = null
# v18: debug_response (#580) — debug console command result.
if snapshot.has("debug_response") and snapshot.debug_response is Dictionary:
debug_response = snapshot.debug_response
else:
debug_response = null
# v14: player_knowledge (#264, D-041) — partial KG dump for journal panel.
# Only update when field is present (null means no change, server sends when KG changes).
if snapshot.has("player_knowledge") and snapshot.player_knowledge is Dictionary:
@@ -336,17 +353,24 @@ func apply_snapshot(snapshot: Dictionary) -> void:
current_zone_id = player_tile.get("zone_id", "") if player_tile else ""
# v2: visible_tiles with visibility sectors
# Derives visible_positions when not explicitly provided (real server mode)
# Derives visible_positions when not explicitly provided (real server mode).
# #585: BoundaryWall tiles go to boundary_positions — rendered in fog but not marked explored.
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
visibility_sectors.clear()
var has_explicit_positions := snapshot.has("visible_positions")
if not has_explicit_positions:
visible_positions.clear()
boundary_positions.clear()
for vtile in snapshot.visible_tiles:
if not vtile is Dictionary or not vtile.has("x") or not vtile.has("y"):
continue
var pos := Vector2i(vtile.x, vtile.y)
var vis_sector: String = vtile.get("visibility", "")
if vtile.has("visibility"):
visibility_sectors[pos] = vtile.visibility
if not has_explicit_positions:
visibility_sectors[pos] = vis_sector
# #585: BoundaryWall tiles are margin tiles visible through fog but not persistently
# explored — they don't update the player's exploration memory when they leave LOS.
if vis_sector == "BoundaryWall":
boundary_positions[pos] = true
elif not has_explicit_positions:
visible_positions[pos] = true
+1
View File
@@ -24,6 +24,7 @@ enum Action {
TELEPORT_HUB, # #501: Home key — Gauntlet dev teleport (not production fast-travel)
SAVE_GAME, # #554: F5 quicksave — sends SaveGame to server with save path
LOAD_GAME, # #554: F6 quickload — sends LoadGame to server with save path
DEBUG_COMMAND, # #581: debug console command dispatch — sends DebugCommandKind to server
}
var input_queue: Array[Dictionary] = []
+20 -1
View File
@@ -47,11 +47,12 @@ func new_game() -> String:
## Resume an existing game session by setting the active game-id.
## Restores world_seed from the save directory for D-010 deterministic replay.
## Restores world_seed and character_archetype from the save directory.
func resume_game(game_id: String) -> void:
GameState.current_game_id = game_id
var save_path := SAVES_DIR + game_id + "/"
GameState.world_seed = _read_seed_file(save_path)
GameState.character_archetype = _read_archetype_file(save_path)
## List all game directories under user://saves/ sorted by last-modified (most recent first).
@@ -146,6 +147,24 @@ func _read_seed_file(save_path: String) -> int:
return file.get_64() & 0x7FFFFFFFFFFFFFFF
## Write character_archetype to save directory. Called after new_game() creates the dir.
func save_character_archetype(game_id: String, archetype: String) -> void:
var save_path := SAVES_DIR + game_id + "/"
var file := FileAccess.open(save_path + "character.txt", FileAccess.WRITE)
if file == null:
push_error("SessionManager: failed to write character.txt: %s" % error_string(FileAccess.get_open_error()))
return
file.store_string(archetype)
## Read character_archetype from save directory. Returns "detective" if missing (legacy saves).
func _read_archetype_file(save_path: String) -> String:
var file := FileAccess.open(save_path + "character.txt", FileAccess.READ)
if file == null:
return "detective"
return file.get_as_text().strip_edges()
func _find_newest_save(dir_path: String) -> String:
var dir := DirAccess.open(dir_path)
if dir == null:
+3 -1
View File
@@ -233,7 +233,7 @@ func _process(delta: float) -> void:
# Send startup message with world_seed (#175, D-010/D-029).
# Server blocks waiting for this before entering the tick loop.
var startup_bytes := Protocol.encode_startup_message(GameState.world_seed)
var startup_bytes := Protocol.encode_startup_message(GameState.world_seed, GameState.character_archetype)
if startup_bytes.size() > 0:
var send_err := _bridge.send_message(startup_bytes)
if send_err != OK:
@@ -416,6 +416,8 @@ static func action_enum_to_wire(action: int) -> String:
return "SaveGame" # #554: F5 quicksave (D-085)
InputMapper.Action.LOAD_GAME:
return "LoadGame" # #554: F6 quickload (D-085)
InputMapper.Action.DEBUG_COMMAND:
return "DebugCommand" # #581: debug console command dispatch
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""
+45 -2
View File
@@ -22,6 +22,8 @@ extends Node2D
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
@onready var settings_dialog = $ModalLayer/SettingsDialog # #528: audio settings (ESC/OPEN_MENU)
@onready var loading_screen = $ModalLayer/LoadingScreen # #257: blocking overlay during load
@onready var debug_console = $ModalLayer/DebugConsole # #581: tilde debug console
@onready var news_ticker = $UILayer/NewsTicker # #592: scrolling headline bar (D-049 z-7)
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
var _last_dialogue_npc_name: String = "" # #535: NPC name for dialogue_response attribution
@@ -30,6 +32,7 @@ var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when s
var _last_dialogue_tick: int = -1
var _last_confrontation_tick: int = -1 # Deduplicate confrontation_monologue signals within same tick
var _known_recognition_ids: Dictionary = {} # D-067: entity_ids that have already chimed
var _known_triangle_ids: Dictionary = {} # #590: triangle_ids that have already fired the activation chime
var _flash_rect: ColorRect = null # #502/#501: ephemeral screen flash overlay (shared: teleport preempts amber)
var _teleport_in_progress: bool = false # #501/#117: forces camera snap (not lerp) on next _process frame
var _pending_record_inputs: Array = [] # #507: accumulates server-bound inputs across frames; flushed into record_tick() on snapshot arrival
@@ -101,12 +104,15 @@ func _ready() -> void:
if fog_entities:
_router.register_always(fog_entities.update_from_state)
_router.register_always(_play_recognition_chimes)
_router.register_always(_handle_triangle_crisis_events)
if gauntlet_hud:
_router.register_always(gauntlet_hud.update_from_state)
if checklist_overlay:
_router.register_always(checklist_overlay.update_from_state)
if time_display:
_router.register_always(time_display.update_from_state)
if news_ticker:
_router.register_always(news_ticker.update_from_state)
if journal_panel:
_router.register_always(journal_panel.update_from_state)
if debug_overlay:
@@ -122,6 +128,16 @@ func _ready() -> void:
_router.register("conversation_ended", _consume_conversation_ended)
_router.register("dialogue_response", _consume_dialogue_response)
_router.register("save_result", _consume_save_result)
_router.register("debug_response", _consume_debug_response)
# #581: Wire settings_dialog debug console toggle → debug_console.set_enabled
if settings_dialog and debug_console:
settings_dialog.debug_console_toggled.connect(debug_console.set_enabled)
# #581 D-088: Wire debug console pause/unpause — sim must not advance during debug input
if debug_console:
debug_console.pause_requested.connect(_on_dialogue_pause_requested)
debug_console.unpause_requested.connect(_on_dialogue_unpause_requested)
func _process(delta: float) -> void:
@@ -283,6 +299,21 @@ func _play_recognition_chimes() -> void:
AudioManager.play(AudioManager.CHIME_RECOGNITION)
# #590 D-072/D-089: Triangle activation consumer — fires sfx_monologue_chime_urgent once
# per triangle_id. The tell_state on the activated NPC and subsequent proximity monologue
# lines are the visible consequence (D-039 wow moment #2 "The Character's Eye").
# No overlay is shown — the chime is the only client-side reaction (D-039 intent).
func _handle_triangle_crisis_events() -> void:
var events: Array = GameState.current_snapshot.get("triangle_crisis_events", [])
for ev in events:
if not ev is Dictionary or not ev.has("triangle_id"):
continue
var tid: int = ev.triangle_id
if not _known_triangle_ids.has(tid):
_known_triangle_ids[tid] = true
AudioManager.play(AudioManager.CHIME_ACTIVATION, AudioManager.BUS_UI_SOUNDS)
# 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).
@@ -351,7 +382,8 @@ func _consume_dialogue() -> void:
dialogue_box.show_dialogue(
dlg.get("npc_name", ""),
dlg.get("speech", ""),
dlg.get("options", [])
dlg.get("options", []),
_last_dialogue_npc_id
)
GameState.current_dialogue = null
@@ -381,11 +413,13 @@ func _consume_dialogue_response() -> void:
if GameState.dialogue_response == null or not dialogue_box:
return
var dr: Dictionary = GameState.dialogue_response
# v0.1: falls back to _last_dialogue_npc_id if wire omits speaker_entity_id.
# Edge case: fast re-engagement with a different NPC could misattribute — low probability.
var speaker_entity_id: int = dr.get("speaker_entity_id", _last_dialogue_npc_id)
var speaker_color_index: int = dr.get("speaker_color_index", -1)
var speaker_name: String = dr.get("speaker_name", _last_dialogue_npc_name)
dialogue_box.update_entity_display(speaker_entity_id, speaker_name, speaker_color_index)
dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""))
dialogue_box.append_dialogue_response(speaker_name, dr.get("text", ""), speaker_entity_id)
GameState.dialogue_response = null
@@ -413,6 +447,14 @@ func _consume_save_result() -> void:
monologue_display.show_notification(msg)
# #581: Forward debug_response from server to the debug console.
func _consume_debug_response() -> void:
if GameState.debug_response == null or not debug_console:
return
debug_console.append_response(GameState.debug_response)
GameState.debug_response = null
# D-061: Handle dialogue option selection → send to server
func _on_dialogue_option_selected(response_id: String, text: String) -> void:
SimBridge.send_input({
@@ -533,6 +575,7 @@ func _teleport_transition() -> void:
GameState.current_dialogue = null
GameState.dialogue_active = false
_known_recognition_ids.clear() # D-067: reset chimes for new room
_known_triangle_ids.clear() # #590: reset activation chimes for new room
if dialogue_box and dialogue_box.is_dialogue_active():
dialogue_box.hide_dialogue()
+61 -5
View File
@@ -11,7 +11,8 @@ class_name Protocol
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
## Reject snapshots where version != this value.
const PROTOCOL_VERSION: int = 17
## v19: adds character_archetype field to StartupMessage (#588, #587).
const PROTOCOL_VERSION: int = 19
# -- Decode: bytes from server → GDScript types --------------------------------
@@ -244,6 +245,42 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
"error": raw_save.get("error"),
}
# v18: debug_response (#580) — debug console command result.
# {command: String, text: String, success: bool}
var debug_response: Variant = null
var raw_debug: Variant = raw.get("debug_response")
if raw_debug is Dictionary:
debug_response = {
"command": str(raw_debug.get("command", "")),
"text": str(raw_debug.get("text", "")),
"success": bool(raw_debug.get("success", false)),
}
# v19: triangle_crisis_events (#590, D-072/D-089) — one-shot activation events.
# Each entry: {triangle_id: int}. Client deduplicates by triangle_id across ticks.
# v0.1 intentional omissions: role_assignments, trigger_npc_id, tick are not decoded
# here — the client has no use for them in v0.1 (no overlay, no entity targeting).
# Add when #593+ requires richer client-side event handling.
var triangle_crisis_events: Array = []
var raw_tce: Variant = raw.get("triangle_crisis_events")
if raw_tce is Array:
for raw_ev in raw_tce:
if raw_ev is Dictionary and raw_ev.has("triangle_id"):
triangle_crisis_events.append({
"triangle_id": int(raw_ev["triangle_id"]),
})
# v19: current_ticker (#592) — scrolling news headline when in The Last Shift zone.
# {id: String, text: String, category: String} or null when player outside bar zone.
var current_ticker: Variant = null
var raw_ticker: Variant = raw.get("current_ticker")
if raw_ticker is Dictionary and raw_ticker.has("text"):
current_ticker = {
"id": str(raw_ticker.get("id", "")),
"text": str(raw_ticker["text"]),
"category": str(raw_ticker.get("category", "")),
}
# TODO(server): Send stationary_ticks in ObserverSnapshot (D-071, D-020).
# Server already tracks this in ListeningFocus component (server/src/simulation/listening.rs).
# When server populates this field, client-side accumulation fallback in game_state.gd
@@ -320,8 +357,11 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
"examine_result": examine_result,
"player_knowledge": player_knowledge,
"save_result": save_result,
"debug_response": debug_response,
"stationary_ticks": stationary_ticks,
"zone_id": zone_id,
"triangle_crisis_events": triangle_crisis_events,
"current_ticker": current_ticker,
}
@@ -427,11 +467,27 @@ static func _decode_enum_variant(raw) -> Dictionary:
# -- Encode: GDScript types → bytes to server ----------------------------------
## Encode a StartupMessage to MessagePack bytes (#175).
## Encode a StartupMessage to MessagePack bytes (#175, #588).
## Sent by the client immediately after handshake validation.
## Server reads this to initialize SimRng with the world seed (D-010, D-029).
static func encode_startup_message(world_seed: int) -> PackedByteArray:
var msg := {"world_seed": world_seed}
## Server reads this to initialize SimRng (D-010, D-029) and select monologue pool (D-032).
## character_archetype: "detective" → "Detective", "smuggler" → "Smuggler" (server enum variant).
static func encode_startup_message(world_seed: int, character_archetype: String = "detective") -> PackedByteArray:
# Map client lowercase archetype string to server PascalCase enum variant.
# Explicit match prevents unknown strings silently reaching the server as
# garbage enum values — fail loudly and fall back to "Detective".
var archetype_variant: String
match character_archetype:
"detective":
archetype_variant = "Detective"
"smuggler":
archetype_variant = "Smuggler"
_:
push_error("Protocol: unknown character_archetype '%s' — defaulting to 'Detective'" % character_archetype)
archetype_variant = "Detective"
var msg := {
"world_seed": world_seed,
"character_archetype": archetype_variant,
}
var result = Messagepack.encode(msg)
if result.status != null:
push_error("Protocol: startup message encode failed: %s" % result.status)
+10 -17
View File
@@ -179,9 +179,7 @@ func snapshot() -> Dictionary:
"player_stance": "Walk",
"player_inventory": [],
"entities": entities,
"tiles": _tiles(),
"visible_tiles": _visible_tiles(),
"visible_positions": _visible_positions(),
"nearby_interactions": nearby,
"current_monologue": monologue,
"current_dialogue": dialogue,
@@ -229,7 +227,7 @@ func _visible_tiles() -> Array:
var vtiles: Array = []
var px := player_pos.x
var py := player_pos.y
var radius := 4
var radius := 5
var room_x := 7
var room_y := 7
var room_w := 8
@@ -241,27 +239,22 @@ func _visible_tiles() -> Array:
if dist <= radius:
if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h:
var sector: String = "Forward" if y <= py else "Peripheral"
vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector})
vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector, "type": _get_tile_type(x, y)})
return vtiles
func _visible_positions() -> Array:
var positions: Array = []
var px := player_pos.x
var py := player_pos.y
var radius := 4
func _get_tile_type(x: int, y: int) -> String:
var room_x := 7
var room_y := 7
var room_w := 8
var room_h := 8
for x in range(px - radius, px + radius + 1):
for y in range(py - radius, py + radius + 1):
var dist := absf(x - px) + absf(y - py)
if dist <= radius:
if x >= room_x and x < room_x + room_w and y >= room_y and y < room_y + room_h:
positions.append({"x": x, "y": y})
return positions
var is_edge := (x == room_x or x == room_x + room_w - 1
or y == room_y or y == room_y + room_h - 1)
if is_edge:
if y == room_y + room_h - 1 and x == room_x + room_w / 2:
return "door"
return "wall"
return "floor"
# -- Spatial helpers -----------------------------------------------------------
+1
View File
@@ -32,6 +32,7 @@ func _ready() -> void:
# Create seamless noise texture for fog animation
var noise := FastNoiseLite.new()
noise.seed = 42
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.frequency = 0.03
var noise_tex := NoiseTexture2D.new()
+7 -1
View File
@@ -64,11 +64,17 @@ func _setup_tileset() -> void:
tile_set = ts
# Update tiles from snapshot data
# tiles: Array of {x: int, y: int, z: int, type: String}
# tiles: Array of {x: int, y: int, z: int, type: String, visibility: String (optional)}
# z here is the server-side FLOOR LEVEL (0 = ground, 1 = first floor, etc.),
# NOT the Godot scene z_index (which controls render order within a floor).
# This node only renders floor-level 0. Higher floor levels will be handled
# by separate TileMapLayer nodes when multi-floor rendering is implemented.
#
# BoundaryWall tiles (#585): visibility="BoundaryWall" tiles (wall tiles 1 step beyond
# LOS boundary) are rendered normally here — they have a "type" field from protocol.gd
# so they composite correctly under the fog shader. The fog/exploration exemption is
# handled in fog_state.gd (VIS_FORWARD without EXP_VISIBLE) and game_state.gd
# (boundary_positions not visible_positions). No special handling needed in this method.
func update_tiles(tiles: Array) -> void:
if not _initialized:
return
+21 -59
View File
@@ -1,17 +1,21 @@
shader_type canvas_item;
// D-059/D-015: 3-state fog shader (simplified from 5-layer by #569).
// State 1: Clear (forward cone) — transparent, soft Gaussian gradient edge (3-4 tile radius)
// State 1: Clear (forward cone) — transparent, soft gradient edge (6-8 tile radius)
// State 2: Explored (out of cone) — light fog overlay, alpha 0.25-0.35, zone temperature tint,
// 8-10s Perlin breathe. Art and information preserved, just "not fresh" (D-015).
// State 3: Unexplored — solid near-black #12141a
// D-033: Entity colors are NOT affected — they render above the fog overlay (z-layer 5).
// D-046: Zone temperature tint from zone_tint_tex — warm=bar, cool=hub, neutral=corridor.
// D-077: zone_tint_tex populated per-tile from server zone_id via fog_state.gd.
//
// Texture pipeline (fog_state.gd): binary 0/255 at 1× tile resolution → CPU Gaussian blur
// (sigma 2.0, 6-8 tile gradient) → Image.resize 4× bilinear upscale → RGBA8 convert.
// Textures arrive here with smooth sub-tile gradients — no GPU-side blur needed.
uniform sampler2D visibility_tex : filter_linear, repeat_disable;
uniform sampler2D exploration_tex : filter_linear, repeat_disable;
uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable; // nearest: zones have hard boundaries (D-073)
uniform sampler2D visibility_tex : filter_linear, repeat_disable; // RGBA8, 4× tile resolution
uniform sampler2D exploration_tex : filter_linear, repeat_disable; // RGBA8, 4× tile resolution
uniform sampler2D zone_tint_tex : filter_nearest, repeat_disable; // D-073: hard zone boundaries
uniform sampler2D noise_tex : filter_linear, repeat_enable;
uniform vec2 rect_pos; // World-space position of the ColorRect (pixels)
uniform vec2 rect_sz; // World-space size of the ColorRect (pixels)
@@ -23,40 +27,6 @@ uniform bool debug_exploration = false; // When true, render raw exploration tex
const vec3 UNEXPLORED_COLOR = vec3(0.071, 0.078, 0.102); // #12141a
// Soft gradient via 7x7 Gaussian blur on visibility (sigma 2.0).
// Spreads the cone boundary into a 3-4 tile radius gradient — no hard tile-stepped edges.
float sample_visibility(vec2 uv) {
vec2 t = 2.0 / map_size;
float sum = 0.0;
float weight = 0.0;
for (float dy = -3.0; dy <= 3.0; dy += 1.0) {
for (float dx = -3.0; dx <= 3.0; dx += 1.0) {
float w = exp(-(dx * dx + dy * dy) / 8.0);
vec2 sample_uv = clamp(uv + vec2(dx, dy) * t, vec2(0.0), vec2(1.0));
sum += texture(visibility_tex, sample_uv).r * w;
weight += w;
}
}
return sum / weight;
}
// Soft gradient on exploration boundary (5x5, sigma 1.5).
// Prevents hard tile-stepped staircase at explored/unexplored edge.
float sample_exploration(vec2 uv) {
vec2 t = 1.0 / map_size;
float sum = 0.0;
float weight = 0.0;
for (float dy = -2.0; dy <= 2.0; dy += 1.0) {
for (float dx = -2.0; dx <= 2.0; dx += 1.0) {
float w = exp(-(dx * dx + dy * dy) / 4.5);
vec2 sample_uv = clamp(uv + vec2(dx, dy) * t, vec2(0.0), vec2(1.0));
sum += texture(exploration_tex, sample_uv).r * w;
weight += w;
}
}
return sum / weight;
}
void fragment() {
vec2 world_px = rect_pos + UV * rect_sz;
vec2 tile = world_px / tile_size;
@@ -67,28 +37,15 @@ void fragment() {
if (tex_uv.x < 0.0 || tex_uv.x > 1.0 || tex_uv.y < 0.0 || tex_uv.y > 1.0) {
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
// Debug mode: render raw exploration texture (bypass fog rendering).
// Green = EXP_VISIBLE (255), blue = EXP_EXPLORED (128), red = EXP_UNEXPLORED (0).
// Debug mode: render RAW exploration texture with filter_linear.
// If hardware bilinear works, should show smooth sub-tile gradients.
} else if (debug_exploration) {
float explored_dbg = texture(exploration_tex, tex_uv).r;
if (explored_dbg > 0.9) {
COLOR = vec4(0.0, explored_dbg, 0.0, 0.8); // Green: currently visible
} else if (explored_dbg > 0.1) {
COLOR = vec4(0.0, 0.0, explored_dbg * 2.0, 0.8); // Blue: explored
} else {
COLOR = vec4(0.5, 0.0, 0.0, 0.8); // Red: unexplored
}
float explored_raw = texture(exploration_tex, tex_uv).r;
COLOR = vec4(vec3(explored_raw), 0.9);
} else {
float vis_raw = texture(visibility_tex, tex_uv).r;
float vis = sample_visibility(tex_uv);
float explored_raw = texture(exploration_tex, tex_uv).r;
float explored = sample_exploration(tex_uv);
// Prevent gradient bleed into never-explored tiles (use raw, unblurred value)
if (explored_raw < 0.01 && vis_raw < 0.01) {
vis = 0.0;
}
float vis = texture(visibility_tex, tex_uv).r;
float explored = texture(exploration_tex, tex_uv).r;
if (explored < 0.01 && vis < 0.01) {
// Unexplored: solid near-black — information zero
@@ -106,8 +63,13 @@ void fragment() {
float alpha = mix(fog_alpha, 0.0, clarity);
vec3 color = mix(zone_tint, vec3(0.0), clarity);
// Soft edge between explored and unexplored (blurred to avoid staircase)
float exp_fade = smoothstep(0.0, 0.3, explored);
// Soft edge between explored and unexplored.
// The exploration texture is binary (explored-or-not) blurred over ~12 tiles.
// At the physical tile boundary, explored ≈ 0.5. Squaring the fade keeps
// fog nearly opaque there (97%), hiding tile-aligned content edges.
// Content appears gradually 4-6 tiles inside the explored area.
float exp_fade = smoothstep(0.3, 1.0, explored);
exp_fade *= exp_fade; // Steeper curve: fog stays opaque near content edge
alpha = mix(1.0, alpha, exp_fade);
color = mix(UNEXPLORED_COLOR, color, exp_fade);
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.
Binary file not shown.
Binary file not shown.
+4 -4
View File
@@ -167,8 +167,8 @@ func test_entity_terrain_uses_object_color() -> void:
var entity := [{"entity_id": 70, "x": 2.0, "y": 2.0, "z": 0,
"kind": {"variant": "Terrain", "data": null}, "visibility": "Forward"}]
renderer.update_entities(entity)
var node = renderer.entity_nodes[70] as ColorRect
assert_that(node.color).override_failure_message(
var node = renderer.entity_nodes[70] as Sprite2D
assert_that(node.self_modulate).override_failure_message(
"Terrain kind should use ENTITY_COLOR_OBJECT"
).is_equal(Constants.ENTITY_COLOR_OBJECT)
renderer.queue_free()
@@ -181,8 +181,8 @@ func test_entity_player_color_regardless_of_sector() -> void:
var entity := [{"entity_id": 80, "x": 5.0, "y": 5.0, "z": 0,
"kind": {"variant": "Player", "data": null}, "visibility": "Peripheral"}]
renderer.update_entities(entity)
var node = renderer.entity_nodes[80] as ColorRect
assert_that(node.color).override_failure_message(
var node = renderer.entity_nodes[80] as Sprite2D
assert_that(node.self_modulate).override_failure_message(
"Player color must be constant regardless of visibility sector"
).is_equal(Constants.ENTITY_COLOR_PLAYER)
# Alpha should still be dimmed for Peripheral
@@ -0,0 +1,153 @@
## Sprint 23 #573: dialogue speaker color binding tests.
##
## Verifies entity-ID-bound speaker color assignment in dialogue_box.gd:
## round-robin palette allocation, same-entity reuse, conversation-end reset,
## and fallback behavior when no entity ID is provided.
##
## D-030: fixture-based, server-free, no subprocess required.
class_name TestDialogueSpeakerColors
extends GdUnitTestSuite
func _make_dialogue_box() -> Control:
if not ResourceLoader.exists("res://ui/dialogue_box.tscn"):
push_warning("TestDialogueSpeakerColors: dialogue_box.tscn not found — scene tests skipped")
return null
var node: Control = load("res://ui/dialogue_box.tscn").instantiate()
add_child(node)
return node
func before_test() -> void:
GameState.dialogue_active = false
func after_test() -> void:
GameState.dialogue_active = false
# -- _assign_npc_color: round-robin assignment ---------------------------------
func test_assign_npc_color_returns_palette_color() -> void:
## First call for an entity ID should return a color from the NPC palette.
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
var color: Color = box._assign_npc_color(100)
assert_that(color).override_failure_message(
"_assign_npc_color must return a non-default color for a valid entity ID (#573)"
).is_not_equal(box._speech_color)
func test_assign_npc_color_same_entity_returns_same_color() -> void:
## Repeated calls for the same entity ID must return the same color.
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
var color1: Color = box._assign_npc_color(200)
var color2: Color = box._assign_npc_color(200)
assert_that(color1).override_failure_message(
"_assign_npc_color must return the same color for the same entity ID (#573)"
).is_equal(color2)
func test_assign_npc_color_different_entities_get_different_colors() -> void:
## Different entity IDs should get different colors (within palette size).
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
var color1: Color = box._assign_npc_color(300)
var color2: Color = box._assign_npc_color(301)
assert_that(color1).override_failure_message(
"Different entity IDs must get different palette colors (#573)"
).is_not_equal(color2)
func test_assign_npc_color_negative_id_returns_speech_color() -> void:
## Negative entity ID (no entity) should fall back to _speech_color.
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
var color: Color = box._assign_npc_color(-1)
assert_that(color).override_failure_message(
"_assign_npc_color(-1) must return _speech_color fallback (#573)"
).is_equal(box._speech_color)
# -- Color registry cleared on conversation end --------------------------------
func test_color_registry_cleared_on_conversation_end() -> void:
## After hide_dialogue(), the color registry must be empty so next
## conversation starts fresh (avoids palette exhaustion).
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
box.show_dialogue("NPC", "Hello.", [], 400)
assert_bool(box._npc_entity_colors.has(400)).override_failure_message(
"Entity color should be registered during conversation (#573)"
).is_true()
box.hide_dialogue()
assert_bool(box._npc_entity_colors.is_empty()).override_failure_message(
"_npc_entity_colors must be cleared after conversation ends (#573)"
).is_true()
assert_int(box._next_npc_color).override_failure_message(
"_next_npc_color must reset to 0 after conversation ends (#573)"
).is_equal(0)
func test_color_registry_reset_gives_fresh_assignment() -> void:
## After conversation end + new conversation, same entity ID gets a color
## (may differ from previous conversation — that's fine, per-conversation).
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
box.show_dialogue("NPC", "Hello.", [], 500)
var color1: Color = box._npc_entity_colors.get(500, Color.BLACK)
box.hide_dialogue()
box.show_dialogue("NPC", "Hi again.", [], 500)
var color2: Color = box._npc_entity_colors.get(500, Color.BLACK)
# Both should be valid palette colors (not BLACK fallback)
assert_that(color1).override_failure_message(
"First conversation color must be a palette color (#573)"
).is_not_equal(Color.BLACK)
assert_that(color2).override_failure_message(
"Second conversation color must be a palette color (#573)"
).is_not_equal(Color.BLACK)
# -- show_dialogue entity ID threading ----------------------------------------
func test_show_dialogue_registers_npc_color() -> void:
## show_dialogue with a valid entity ID must register the color.
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
box.show_dialogue("Kael", "Welcome.", [], 600)
assert_bool(box._npc_entity_colors.has(600)).override_failure_message(
"show_dialogue must register entity color when npc_entity_id provided (#573)"
).is_true()
func test_show_dialogue_without_entity_id_no_registration() -> void:
## show_dialogue without entity ID should not register any color.
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
box.show_dialogue("NPC", "Hello.", [])
assert_bool(box._npc_entity_colors.is_empty()).override_failure_message(
"show_dialogue without entity ID must not register colors (#573)"
).is_true()
# -- append_dialogue_response defensive guard ----------------------------------
func test_append_dialogue_response_registers_color_if_missing() -> void:
## append_dialogue_response with a valid entity_id must register the color
## even if show_dialogue was not called first (defensive guard).
var box := _make_dialogue_box()
if box == null: return
auto_free(box)
box.append_dialogue_response("Voss", "I see.", 700)
assert_bool(box._npc_entity_colors.has(700)).override_failure_message(
"append_dialogue_response must register color for unknown entity ID (#573)"
).is_true()
@@ -0,0 +1 @@
uid://c1dnlbnxtgqqo
+73
View File
@@ -268,6 +268,79 @@ func test_game_state_visible_positions_cleared_on_new_snapshot() -> void:
assert_that(GameState.visible_positions.has(Vector2i(10, 10))).is_true()
# -- #585: BoundaryWall tiles — visible in fog, not persistently explored ------
func test_boundary_wall_tiles_not_in_visible_positions() -> void:
## #585: BoundaryWall margin tiles must NOT enter visible_positions.
## They are rendered via tile_renderer (from visible_tiles) but must not
## update the player's fog exploration memory.
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall"},
],
})
assert_that(GameState.visible_positions.has(Vector2i(5, 5))).is_true()
assert_that(GameState.visible_positions.has(Vector2i(6, 5))).is_false()
assert_that(GameState.boundary_positions.has(Vector2i(6, 5))).is_true()
func test_boundary_wall_tiles_in_visibility_sectors() -> void:
## BoundaryWall visibility sector is still tracked in visibility_sectors
## (for potential future use — wall coloring, etc.)
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [
{"x": 3, "y": 3, "z": 0, "visibility": "BoundaryWall"},
],
})
assert_that(GameState.visibility_sectors.has(Vector2i(3, 3))).is_true()
assert_that(GameState.visibility_sectors[Vector2i(3, 3)]).is_equal("BoundaryWall")
func test_boundary_positions_cleared_on_new_snapshot() -> void:
## BoundaryWall positions are cleared each snapshot so stale walls don't persist.
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [{"x": 7, "y": 7, "z": 0, "visibility": "BoundaryWall"}],
})
assert_that(GameState.boundary_positions.has(Vector2i(7, 7))).is_true()
GameState.apply_snapshot({
"tick": 2,
"visible_tiles": [{"x": 10, "y": 10, "z": 0, "visibility": "Forward"}],
})
assert_that(GameState.boundary_positions.has(Vector2i(7, 7))).is_false()
assert_that(GameState.boundary_positions.size()).is_equal(0)
func test_boundary_wall_fog_vis_forward() -> void:
## #585: BoundaryWall tiles must lift fog (VIS_FORWARD = 255) so wall content composites.
## visible_positions excludes boundary tiles; fog_state writes vis bytes for them separately.
## Reads _vis_bytes directly (packed byte array) to avoid ImageTexture.get_image() lag.
var fog_state = _get_fog_state()
if fog_state == null:
return
GameState.apply_snapshot({
"tick": 1,
"visible_tiles": [
{"x": 0, "y": 0, "z": 0, "visibility": "Forward"}, # normal LOS tile
{"x": 1, "y": 0, "z": 0, "visibility": "BoundaryWall"}, # margin tile
],
})
fog_state.update_from_state()
var ox: int = fog_state.map_bounds.position.x
var oy: int = fog_state.map_bounds.position.y
var w: int = fog_state.map_bounds.size.x
var vis: PackedByteArray = fog_state._vis_bytes
var normal_idx: int = (0 - oy) * w + (0 - ox)
var boundary_idx: int = (0 - oy) * w + (1 - ox)
assert_int(vis[normal_idx]).is_equal(FogState.VIS_FORWARD) # normal tile: VIS_FORWARD
assert_int(vis[boundary_idx]).is_equal(FogState.VIS_FORWARD) # boundary also fog-lifted
GameState.visible_positions.clear()
GameState.boundary_positions.clear()
# -- Z-layer compliance (D-049) -----------------------------------------------
func test_fog_overlay_z_layer() -> void:
+135
View File
@@ -24,12 +24,14 @@ func before_test() -> void:
GameState.visible_positions.clear()
GameState.visible_tiles.clear()
GameState.visibility_sectors.clear()
GameState.boundary_positions.clear()
func after_test() -> void:
GameState.visible_positions.clear()
GameState.visible_tiles.clear()
GameState.visibility_sectors.clear()
GameState.boundary_positions.clear()
# -- Spec constants (D-059) ---------------------------------------------------
@@ -489,6 +491,139 @@ func test_visible_positions_cleared_on_new_snapshot() -> void:
assert_bool(GameState.visible_positions.has(Vector2i(10, 10))).is_true()
# -- Sprint 23: BoundaryWall handling (#585) ----------------------------------
func test_boundary_positions_populated_from_snapshot() -> void:
# #585: BoundaryWall tiles go to boundary_positions (not visible_positions).
# Fog lifts for boundary wall tiles so wall content composites correctly.
GameState.apply_snapshot({
"tick": 20,
"visible_tiles": [
{"x": 10, "y": 10, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 11, "y": 10, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
],
})
assert_bool(GameState.visible_positions.has(Vector2i(10, 10))).override_failure_message(
"Forward tile must be in visible_positions"
).is_true()
assert_bool(GameState.visible_positions.has(Vector2i(11, 10))).override_failure_message(
"BoundaryWall tile must NOT be in visible_positions (#585)"
).is_false()
assert_bool(GameState.boundary_positions.has(Vector2i(11, 10))).override_failure_message(
"BoundaryWall tile must be in boundary_positions (#585)"
).is_true()
func test_boundary_wall_vis_forward_not_exp_visible() -> void:
# #585: BoundaryWall tiles get VIS_FORWARD (fog lifted) but NOT EXP_VISIBLE.
# They render through fog but are not stored as exploration memory.
var fog_state = _get_fog_state()
if fog_state == null:
return
if not fog_state.has_method("update_from_state"):
return
GameState.visible_positions = {Vector2i(5, 5): true}
GameState.boundary_positions = {Vector2i(6, 5): true}
GameState.visible_tiles = [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
]
fog_state.update_from_state()
var vis_bytes = fog_state.get("_vis_bytes")
var exp_bytes = fog_state.get("_exp_bytes")
if vis_bytes == null or exp_bytes == null:
push_warning("TestFogSprint22: byte arrays not accessible — skipped")
return
var ox: int = fog_state.map_bounds.position.x
var oy: int = fog_state.map_bounds.position.y
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
if w <= 0:
return
var px := 6 - ox
var py := 5 - oy
if px < 0 or py < 0 or px >= w:
push_warning("TestFogSprint22: boundary tile (6,5) out of bounds — skipped")
return
var idx := py * w + px
if idx < 0 or idx >= vis_bytes.size():
return
assert_int(vis_bytes[idx]).override_failure_message(
"BoundaryWall tile must have VIS_FORWARD — fog must lift to composite wall content (#585)"
).is_equal(fog_state.VIS_FORWARD)
assert_int(exp_bytes[idx]).override_failure_message(
"BoundaryWall tile must NOT be EXP_VISIBLE — it is not explored memory (#585)"
).is_not_equal(fog_state.EXP_VISIBLE)
func test_boundary_wall_stays_unexplored_after_leaving_los() -> void:
# #585: When BoundaryWall tile leaves LOS, it must NOT decay to EXP_EXPLORED.
# Normal LOS tiles decay to EXP_EXPLORED when they leave LOS.
# Boundary tiles must stay EXP_UNEXPLORED — they were never explored.
var fog_state = _get_fog_state()
if fog_state == null:
return
if not fog_state.has_method("update_from_state"):
return
# Frame 1: BoundaryWall at (6,5) is visible
GameState.visible_positions = {Vector2i(5, 5): true}
GameState.boundary_positions = {Vector2i(6, 5): true}
GameState.visible_tiles = [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
]
fog_state.update_from_state()
# Frame 2: both leave LOS
GameState.visible_positions.clear()
GameState.boundary_positions.clear()
GameState.visible_tiles = []
fog_state.update_from_state()
var exp_bytes = fog_state.get("_exp_bytes")
if exp_bytes == null:
return
var ox: int = fog_state.map_bounds.position.x
var oy: int = fog_state.map_bounds.position.y
var w: int = fog_state.get("_width") if fog_state.get("_width") != null else -1
if w <= 0:
return
var px := 6 - ox
var py := 5 - oy
if px >= 0 and py >= 0 and px < w:
var idx := py * w + px
if idx >= 0 and idx < exp_bytes.size():
assert_int(exp_bytes[idx]).override_failure_message(
"BoundaryWall tile must stay EXP_UNEXPLORED after leaving LOS (#585 — not explored memory)"
).is_equal(fog_state.EXP_UNEXPLORED)
func test_boundary_wall_cleared_on_new_snapshot() -> void:
# #585: boundary_positions must be cleared each tick — old walls must not persist.
# BoundaryWall positions shift as the player moves; stale positions would lift fog
# where no wall exists.
GameState.apply_snapshot({
"tick": 30,
"visible_tiles": [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
{"x": 6, "y": 5, "z": 0, "visibility": "BoundaryWall", "type": "wall"},
],
})
assert_bool(GameState.boundary_positions.has(Vector2i(6, 5))).is_true()
GameState.apply_snapshot({
"tick": 31,
"visible_tiles": [
{"x": 5, "y": 5, "z": 0, "visibility": "Forward", "type": "floor"},
],
})
assert_bool(GameState.boundary_positions.has(Vector2i(6, 5))).override_failure_message(
"Stale BoundaryWall position must be cleared on next snapshot (#585)"
).is_false()
# -- Performance (D-059) -------------------------------------------------------
func test_fog_state_update_under_2ms_for_400_tiles() -> void:
+1
View File
@@ -0,0 +1 @@
uid://bxhgo1e4rvfmi
+8 -8
View File
@@ -26,17 +26,17 @@ func _load_fixture(name: String) -> PackedByteArray:
# -- Protocol version upgrade -------------------------------------------------
func test_protocol_version_is_8() -> void:
assert_that(Protocol.PROTOCOL_VERSION).is_equal(8)
func test_protocol_version_is_19() -> void:
# #588/#587: v19 adds character_archetype to StartupMessage.
assert_that(Protocol.PROTOCOL_VERSION).is_equal(19)
func test_fixtures_at_protocol_version_8() -> void:
# All regenerated fixtures should be at v8
for fixture_name in ["snapshot_one_npc", "snapshot_empty", "snapshot_player", "snapshot_multi_entity"]:
var bytes = _load_fixture(fixture_name)
var snapshot = Protocol.decode_snapshot(bytes)
assert_that(snapshot).is_not_null()
assert_that(snapshot.version).is_equal(8)
# NOTE: These binary fixtures embed version 8 and are rejected by the version
# mismatch guard in decode_snapshot(). This test is pre-existing broken since v9+.
# Fixtures need regeneration via `make fixtures-gauntlet` to match current protocol.
# Skipping rather than deleting to preserve the fixture round-trip pattern.
pass
func test_rejects_version_6() -> void:
+23 -23
View File
@@ -218,9 +218,9 @@ func test_entity_renderer_positions_centered() -> void:
renderer.update_entities([_test_entities[0]])
var node = renderer.entity_nodes[1]
var offset: float = (Constants.TILE_SIZE - 24) / 2.0
var expected_x: float = 5.0 * Constants.TILE_SIZE + offset
var expected_y: float = 5.0 * Constants.TILE_SIZE + offset
# Sprite2D renderer: ENTITY_OFFSET_X=0.0, ENTITY_OFFSET_Y=TILE_SIZE-ENTITY_HEIGHT=0.0
var expected_x: float = 5.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X
var expected_y: float = 5.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y
assert_that(node.position.x).is_equal_approx(expected_x, 0.01)
assert_that(node.position.y).is_equal_approx(expected_y, 0.01)
renderer.queue_free()
@@ -248,9 +248,9 @@ func test_entity_renderer_player_color_differs_from_npc() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities)
var player_node = renderer.entity_nodes[1] as ColorRect
var npc_node = renderer.entity_nodes[2] as ColorRect
assert_that(player_node.color != npc_node.color).is_true()
var player_node = renderer.entity_nodes[1] as Sprite2D
var npc_node = renderer.entity_nodes[2] as Sprite2D
assert_that(player_node.self_modulate != npc_node.self_modulate).is_true()
renderer.queue_free()
func test_entity_renderer_empty_entities_clears_all() -> void:
@@ -268,23 +268,23 @@ func test_entity_renderer_empty_entities_clears_all() -> void:
func test_entity_renderer_player_uses_d033_color() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var player_node = renderer.entity_nodes[1] as ColorRect
assert_that(player_node.color).is_equal(Constants.ENTITY_COLOR_PLAYER)
var player_node = renderer.entity_nodes[1] as Sprite2D
assert_that(player_node.self_modulate).is_equal(Constants.ENTITY_COLOR_PLAYER)
renderer.queue_free()
func test_entity_renderer_npc_uses_unknown_teal() -> void:
var renderer := _make_entity_renderer()
renderer.update_entities(_test_entities_v2)
var npc_node = renderer.entity_nodes[2] as ColorRect
assert_that(npc_node.color).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
var npc_node = renderer.entity_nodes[2] as Sprite2D
assert_that(npc_node.self_modulate).is_equal(Constants.ENTITY_COLOR_UNKNOWN)
renderer.queue_free()
func test_entity_renderer_object_uses_grey() -> void:
var renderer := _make_entity_renderer()
var obj := [{"entity_id": 3, "x": 1.0, "y": 1.0, "z": 0, "kind": {"variant": "Object", "data": null}, "visibility": "Forward"}]
renderer.update_entities(obj)
var node = renderer.entity_nodes[3] as ColorRect
assert_that(node.color).is_equal(Constants.ENTITY_COLOR_OBJECT)
var node = renderer.entity_nodes[3] as Sprite2D
assert_that(node.self_modulate).is_equal(Constants.ENTITY_COLOR_OBJECT)
renderer.queue_free()
func test_entity_renderer_peripheral_entity_dimmed() -> void:
@@ -319,14 +319,14 @@ func test_entity_renderer_facing_indicator_rotation_accuracy() -> void:
# {facing_angle → expected indicator rotation}
# Indicator 0 = North (up). facing_angle 0 = East. So rotation = angle + PI/2.
var angles := {
-PI / 2.0: 0.0, # North
-PI / 4.0: PI / 4.0, # Northeast
0.0: PI / 2.0, # East
PI / 4.0: 3.0 * PI / 4.0, # Southeast
PI / 2.0: PI, # South
3.0 * PI / 4.0: -3.0 * PI / 4.0, # Southwest (Godot normalizes to (-PI, PI])
PI: -PI / 2.0, # West (3PI/2 normalized to -PI/2)
-3.0 * PI / 4.0: -PI / 4.0, # Northwest (-3PI/4 + PI/2 = -PI/4)
-PI / 2.0: 0.0, # North
-PI / 4.0: PI / 4.0, # Northeast
0.0: PI / 2.0, # East
PI / 4.0: 3.0 * PI / 4.0, # Southeast
PI / 2.0: PI, # South
3.0 * PI / 4.0: 5.0 * PI / 4.0, # Southwest (raw: 3PI/4 + PI/2 = 5PI/4)
PI: 3.0 * PI / 2.0, # West (raw: PI + PI/2 = 3PI/2)
-3.0 * PI / 4.0: -PI / 4.0, # Northwest: wraps negative — Godot returns raw un-normalised rotation
}
renderer.update_entities(_test_entities_v2)
var player_node = renderer.entity_nodes[1]
@@ -405,9 +405,9 @@ func test_regression_345_entity_position_set_from_entity_id_entity() -> void:
{"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)
# Sprite2D renderer: ENTITY_OFFSET_X=0.0, ENTITY_OFFSET_Y=TILE_SIZE-ENTITY_HEIGHT=0.0
assert_that(node.position.x).is_equal_approx(6.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_X, 0.01)
assert_that(node.position.y).is_equal_approx(7.0 * Constants.TILE_SIZE + EntityRenderer.ENTITY_OFFSET_Y, 0.01)
renderer.queue_free()
+268
View File
@@ -0,0 +1,268 @@
## Sprint 24 — Signal acceptance tests (#588, #590, #592)
##
## Client-side acceptance criteria:
## - #588: character_archetype field in GameState, StartupMessage, SessionManager persistence
## - #590: triangle_crisis_events decoded by Protocol, chimed once per triangle_id
## - #592: news_ticker decode + update_from_state hide/show behavior
##
## Spec: D-032 (monologue pools per character), D-016 (client displays server data only),
## D-042 (UI strings in yaml), D-067 (chime on recognition onset)
class_name TestSignalSprint24
extends GdUnitTestSuite
# -- #588: Character archetype field ------------------------------------------
func test_game_state_has_character_archetype_field() -> void:
assert_bool("character_archetype" in GameState).override_failure_message(
"GameState must have a character_archetype field (#588)"
).is_true()
func test_game_state_character_archetype_default_is_detective() -> void:
# Fresh GameState defaults to "detective" (safest fallback for legacy saves).
var archetype = GameState.get("character_archetype")
assert_str(archetype).override_failure_message(
"GameState.character_archetype default must be 'detective'"
).is_equal("detective")
func test_protocol_startup_message_unknown_archetype_defaults_to_detective() -> void:
# Unknown archetype strings must not silently pass garbage to the server.
# The match guard falls back to "Detective" and calls push_error.
var bytes: PackedByteArray = Protocol.encode_startup_message(0, "hacker")
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status).is_null()
assert_str(decoded.value["character_archetype"]).override_failure_message(
"Unknown archetype must fall back to 'Detective'"
).is_equal("Detective")
func test_protocol_startup_message_includes_character_archetype() -> void:
# StartupMessage wire payload must carry "character_archetype" key (#588).
var bytes: PackedByteArray = Protocol.encode_startup_message(12345, "detective")
assert_bool(bytes.size() > 0).is_true()
var decoded = Messagepack.decode(bytes)
assert_that(decoded.status).is_null()
var msg: Dictionary = decoded.value
assert_bool(msg.has("character_archetype")).override_failure_message(
"StartupMessage must contain 'character_archetype' key, got: %s" % str(msg.keys())
).is_true()
func test_protocol_startup_message_detective_maps_to_pascal_case() -> void:
# "detective" client string must map to "Detective" PascalCase server enum variant.
var bytes: PackedByteArray = Protocol.encode_startup_message(0, "detective")
var decoded = Messagepack.decode(bytes)
assert_str(decoded.value["character_archetype"]).is_equal("Detective")
func test_protocol_startup_message_smuggler_maps_to_pascal_case() -> void:
# "smuggler" client string must map to "Smuggler" PascalCase server enum variant.
var bytes: PackedByteArray = Protocol.encode_startup_message(0, "smuggler")
var decoded = Messagepack.decode(bytes)
assert_str(decoded.value["character_archetype"]).is_equal("Smuggler")
func test_protocol_startup_message_preserves_world_seed() -> void:
# Adding character_archetype must not break world_seed encoding.
var seed: int = 0xDEADBEEF
var bytes: PackedByteArray = Protocol.encode_startup_message(seed, "detective")
var decoded = Messagepack.decode(bytes)
assert_int(decoded.value["world_seed"]).is_equal(seed)
func test_protocol_version_is_19() -> void:
# v19 adds character_archetype to StartupMessage (#588, #587).
assert_that(Protocol.PROTOCOL_VERSION).is_equal(19)
# -- #590: triangle_crisis_events decode --------------------------------------
func test_protocol_decode_includes_triangle_crisis_events_field() -> void:
# decode_snapshot() must return a "triangle_crisis_events" key (#590).
var raw := {
"tick": 1,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
"triangle_crisis_events": [{"triangle_id": 42}],
}
var encoded = Messagepack.encode(raw)
assert_that(encoded.status).is_null()
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
assert_bool(snapshot.has("triangle_crisis_events")).override_failure_message(
"decode_snapshot must include triangle_crisis_events in returned dict"
).is_true()
var events: Array = snapshot["triangle_crisis_events"]
assert_bool(events.size() == 1).override_failure_message(
"Expected 1 triangle_crisis_event, got: %d" % events.size()
).is_true()
assert_int(events[0]["triangle_id"]).is_equal(42)
func test_protocol_decode_triangle_crisis_events_empty_array() -> void:
# When no events are present, field is present and empty.
var raw := {
"tick": 1,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
"triangle_crisis_events": [],
}
var encoded = Messagepack.encode(raw)
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
var events: Array = snapshot.get("triangle_crisis_events", [])
assert_int(events.size()).is_equal(0)
func test_protocol_decode_triangle_crisis_events_absent_returns_empty() -> void:
# When server doesn't send field (pre-#589), field defaults to empty array.
var raw := {
"tick": 1,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
}
var encoded = Messagepack.encode(raw)
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
var events: Array = snapshot.get("triangle_crisis_events", [])
assert_int(events.size()).is_equal(0)
func test_triangle_dedup_fires_chime_only_once_per_id() -> void:
# _known_triangle_ids must prevent the same triangle_id from chiming twice.
# We test the dedup dict directly — main.gd cannot be easily instantiated headless.
# The dict is the single source of truth for dedup state.
var seen: Dictionary = {}
var chime_count: int = 0
# Simulate two ticks both containing triangle_id 42.
for _tick in range(2):
var tid: int = 42
if not seen.has(tid):
seen[tid] = true
chime_count += 1
assert_int(chime_count).override_failure_message(
"Chime must fire exactly once per triangle_id across repeated ticks"
).is_equal(1)
func test_triangle_dedup_fires_chime_for_each_unique_id() -> void:
# Two distinct triangle_ids each chime once.
var seen: Dictionary = {}
var chime_count: int = 0
for tid in [42, 99]:
if not seen.has(tid):
seen[tid] = true
chime_count += 1
assert_int(chime_count).override_failure_message(
"Each unique triangle_id must chime independently"
).is_equal(2)
# -- #592: current_ticker decode ----------------------------------------------
func test_protocol_decode_includes_current_ticker_field() -> void:
# decode_snapshot() must return a "current_ticker" key (#592).
var raw := {
"tick": 1,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
"current_ticker": {"id": "ticker_001", "text": "Station systems nominal.", "category": "System"},
}
var encoded = Messagepack.encode(raw)
assert_that(encoded.status).is_null()
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
assert_bool(snapshot.has("current_ticker")).override_failure_message(
"decode_snapshot must include current_ticker in returned dict"
).is_true()
var ticker: Variant = snapshot["current_ticker"]
assert_that(ticker).is_not_null()
assert_str(ticker["text"]).is_equal("Station systems nominal.")
func test_protocol_decode_current_ticker_null_when_absent() -> void:
# When server doesn't send current_ticker (player outside bar zone), field is null.
var raw := {
"tick": 1,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
}
var encoded = Messagepack.encode(raw)
var snapshot = Protocol.decode_snapshot(encoded.value)
assert_that(snapshot).is_not_null()
var ticker: Variant = snapshot.get("current_ticker")
assert_that(ticker).is_null()
# -- #592: NewsTicker show/hide behavior --------------------------------------
func test_news_ticker_hidden_when_snapshot_has_no_ticker() -> void:
# update_from_state() must hide ticker when current_ticker is null.
var ticker_scene := load("res://ui/news_ticker.tscn") as PackedScene
assert_that(ticker_scene).is_not_null()
var ticker := ticker_scene.instantiate()
auto_free(ticker)
add_child(ticker)
# Snapshot with no current_ticker (player outside bar zone).
GameState.current_snapshot = {
"tick": 1,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
}
ticker.update_from_state()
assert_bool(ticker.visible).override_failure_message(
"NewsTicker must be hidden when current_ticker is absent"
).is_false()
func test_news_ticker_visible_when_snapshot_has_ticker() -> void:
# update_from_state() must show ticker when current_ticker has text.
var ticker_scene := load("res://ui/news_ticker.tscn") as PackedScene
assert_that(ticker_scene).is_not_null()
var ticker := ticker_scene.instantiate()
auto_free(ticker)
add_child(ticker)
GameState.current_snapshot = {
"tick": 2,
"version": Protocol.PROTOCOL_VERSION,
"entities": [],
"current_ticker": {"id": "t1", "text": "Station systems nominal.", "category": "System"},
}
ticker.update_from_state()
assert_bool(ticker.visible).override_failure_message(
"NewsTicker must be visible when current_ticker has text"
).is_true()
func test_news_ticker_hides_when_ticker_becomes_null() -> void:
# Ticker shown then hidden: update_from_state() with null current_ticker hides it.
var ticker_scene := load("res://ui/news_ticker.tscn") as PackedScene
assert_that(ticker_scene).is_not_null()
var ticker := ticker_scene.instantiate()
auto_free(ticker)
add_child(ticker)
# Show it first.
GameState.current_snapshot = {
"tick": 1, "version": Protocol.PROTOCOL_VERSION, "entities": [],
"current_ticker": {"id": "t1", "text": "Breaking news.", "category": "System"},
}
ticker.update_from_state()
assert_bool(ticker.visible).is_true()
# Null current_ticker — player left the bar zone.
GameState.current_snapshot = {
"tick": 2, "version": Protocol.PROTOCOL_VERSION, "entities": [],
}
ticker.update_from_state()
assert_bool(ticker.visible).override_failure_message(
"NewsTicker must hide when current_ticker returns to null"
).is_false()
+1
View File
@@ -0,0 +1 @@
uid://signal_sprint24_sr
+78
View File
@@ -24,6 +24,7 @@ var _flow: String = ""
var _interval: float = 3.0
var _list_mode: bool = false
var _config: Dictionary = {}
var _is_live: bool = false
func _init():
@@ -73,6 +74,19 @@ func _run():
return
var main_node = main_scene.instantiate()
# Live mode: configure server port BEFORE main.gd._ready() calls connect_to_sim()
_is_live = OS.get_environment("SR_LIVE") == "1"
if _is_live:
var sim_bridge := root.get_node("/root/SimBridge")
var port_env := OS.get_environment("SR_PORT")
if port_env.is_empty():
push_error("visual_capture: SR_LIVE=1 but SR_PORT not set")
quit(1)
return
sim_bridge.server_port = int(port_env)
print("visual_capture: live mode — server port %d" % sim_bridge.server_port)
root.add_child(main_node)
# Wait for NoiseTexture2D async generation
@@ -92,6 +106,36 @@ func _run():
for i in range(settle_count):
await process_frame
# Live mode: wait for server connection and first snapshot
if _is_live:
var sim_bridge := root.get_node("/root/SimBridge")
var game_state := root.get_node("/root/GameState")
print("visual_capture: waiting for server connection...")
var max_frames := 300 # 5 seconds at 60fps
var waited := 0
while sim_bridge.state != sim_bridge.ConnectionState.CONNECTED:
if sim_bridge.state == sim_bridge.ConnectionState.ERROR:
push_error("visual_capture: server connection failed")
quit(1)
return
await process_frame
waited += 1
if waited >= max_frames:
push_error("visual_capture: connection timeout after %d frames" % waited)
quit(1)
return
print("visual_capture: connected after %d frames" % waited)
# Wait for first snapshot from server
waited = 0
while game_state.current_tick == 0:
await process_frame
waited += 1
if waited >= max_frames:
push_error("visual_capture: no snapshot after %d frames" % waited)
quit(1)
return
print("visual_capture: first snapshot tick=%d (%d frames)" % [game_state.current_tick, waited])
if not _scenario.is_empty():
await _run_scenario(main_node)
elif not _flow.is_empty():
@@ -123,6 +167,40 @@ func _run_scenario(_main_node: Node) -> void:
# Post-tick setup (e.g. zone tint patching)
_scenarios.post_setup(_scenario, root)
# Replay snapshot: inject a real server snapshot through the FULL client pipeline.
# Loads MessagePack bytes (exact wire format from server), decodes via Protocol.gd,
# then applies through GameState → FogState → shader — same path as live game.
var replay_path: String = scenario_cfg.get("replay_snapshot", "")
if not replay_path.is_empty():
var project_root := ProjectSettings.globalize_path("res://")
var repo_root := project_root.rstrip("/").get_base_dir()
var abs_path := repo_root.path_join(replay_path)
var rf := FileAccess.open(abs_path, FileAccess.READ)
if rf == null:
push_error("visual_capture: cannot open replay snapshot %s" % abs_path)
quit(1)
return
var replay_bytes := rf.get_buffer(rf.get_length())
rf.close()
# Decode through Protocol.decode_snapshot() — same as live IPC receive path.
# This exercises: msgpack decode → entity decode → tile_kind→type mapping → etc.
var replay_data: Variant = Protocol.decode_snapshot(replay_bytes)
if replay_data == null or not replay_data is Dictionary:
push_error("visual_capture: Protocol.decode_snapshot failed for %s" % abs_path)
quit(1)
return
print("visual_capture: replaying %s (%d bytes, tick=%s, %d tiles)" % [
replay_path, replay_bytes.size(),
str(replay_data.get("tick", "?")),
replay_data.get("visible_tiles", []).size()])
var game_state := root.get_node("/root/GameState")
var fog_state := root.get_node("/root/FogState")
game_state.apply_snapshot(replay_data)
fog_state.update_from_state()
# Extra frames for fog uniform propagation
for i in range(4):
await process_frame
# Extra frames for state propagation + viewport texture lag
await process_frame
await process_frame
+1
View File
@@ -0,0 +1 @@
uid://pd1qpgxiodig
+11
View File
@@ -83,6 +83,17 @@ func apply_setup(scenario_name: String, tree_root: Node) -> bool:
sim_bridge.harness.player_pos = Vector2i(11, 9)
sim_bridge.harness.process_input("Interact")
"fog_live_replay", "fog_theater_replay", "fog_boundary_replay":
# Replay real server snapshots via MessagePack → Protocol.decode_snapshot().
# Setup handled by visual_capture.gd (reads replay_snapshot from config).
pass
"fog_live_hub":
# Live server connection — Hub spawn position.
# No setup needed: server starts in --test-mode with Gauntlet,
# player spawns at Hub (50,58). Captures real fog pipeline output.
pass
_:
push_warning("VisualScenarios: unknown scenario '%s'" % scenario_name)
return false
+1
View File
@@ -0,0 +1 @@
uid://dna10a0ln5pd0
+16 -1
View File
@@ -11,6 +11,7 @@ extends Control
## - inputs.jsonl — last 60 ticks of PlayerInput (replay-compatible JSONL)
## - snapshots.jsonl — last 60 ticks of ObserverSnapshot (one JSON per line)
## - seed.txt — RNG seed for deterministic replay
## - screenshot.png — viewport capture taken before dialog opened
##
## Ring buffer: pre-allocated RING_SIZE arrays at startup. record_tick() is the
## public API for main.gd. _push_tick_inputs() / _push_tick_snapshot() are the
@@ -36,6 +37,7 @@ const RING_SIZE := 60
var _line_edit: LineEdit = null
var _active: bool = false
var _captured_screenshot: Image = null
# #507: Pre-allocated ring buffers (no per-tick allocation after _ready).
# Input ring: replay-format PlayerInput arrays, one per tick.
@@ -201,6 +203,8 @@ func _get_filled_snapshot_count() -> int:
func start_capture() -> void:
if _active:
return
# Capture screenshot BEFORE showing the dialog overlay
_captured_screenshot = get_viewport().get_texture().get_image()
_active = true
visible = true
@@ -247,6 +251,8 @@ func _close() -> void:
_line_edit.queue_free()
_line_edit = null
_captured_screenshot = null
# Unpause the simulation
SimBridge.send_input({
"action": InputMapper.Action.UNPAUSE,
@@ -345,7 +351,16 @@ func _save_report(description: String) -> void:
else:
push_error("BugReport: failed to write %s" % seed_path)
print("BugReport: saved %d/6 files to %s (ring: %d ticks)" % [
# 7. screenshot.png — viewport capture taken before dialog opened
if _captured_screenshot:
var screenshot_path := base_path + "/screenshot.png"
var img_err := _captured_screenshot.save_png(screenshot_path)
if img_err == OK:
files_saved += 1
else:
push_error("BugReport: failed to write %s (error %d)" % [screenshot_path, img_err])
print("BugReport: saved %d/7 files to %s (ring: %d ticks)" % [
files_saved, base_path, _input_count])
+89
View File
@@ -0,0 +1,89 @@
extends Control
## #588: Character archetype select panel — shown after "New Game", before loading main.tscn.
## Two cards (Smuggler / Detective). Keyboard (left/right/enter/esc) and mouse.
## Emits archetype_confirmed(archetype: String) or archetype_cancelled on ESC.
##
## ESC cancels without creating a save directory — new_game() fires AFTER confirmation.
signal archetype_confirmed(archetype: String)
signal archetype_cancelled
const CARD_BG_NORMAL := Color(0.07, 0.07, 0.10, 1.0)
const CARD_BG_SELECTED := Color(0.10, 0.12, 0.18, 1.0)
const CARD_BORDER_NORMAL := Color(0.18, 0.22, 0.28, 1.0)
const CARD_BORDER_SELECTED := Color(0.906, 0.773, 0.278, 1.0) # INSERT_COLOR_HOVER
# Archetypes in display order — index 0=smuggler (left card), 1=detective (right card)
const ARCHETYPES := ["smuggler", "detective"]
@onready var _smuggler_wrapper: Control = $Cards/CardSmugglerWrapper
@onready var _detective_wrapper: Control = $Cards/CardDetectiveWrapper
@onready var _confirm_btn: Button = $ConfirmBtn
@onready var _title_label: Label = $TitleLabel
var _selected_index: int = 0 # 0=smuggler, 1=detective
func _ready() -> void:
_title_label.text = UIStrings.get_text("character_select.title")
_confirm_btn.text = UIStrings.get_text("character_select.confirm")
# Smuggler card labels
$Cards/CardSmugglerWrapper/CardInner/VBox/NameLabel.text = UIStrings.get_text("character_select.smuggler_card_name")
$Cards/CardSmugglerWrapper/CardInner/VBox/RoleLabel.text = UIStrings.get_text("character_select.smuggler_card_role")
$Cards/CardSmugglerWrapper/CardInner/VBox/ToneLabel.text = UIStrings.get_text("character_select.smuggler_card_tone")
# Detective card labels
$Cards/CardDetectiveWrapper/CardInner/VBox/NameLabel.text = UIStrings.get_text("character_select.detective_card_name")
$Cards/CardDetectiveWrapper/CardInner/VBox/RoleLabel.text = UIStrings.get_text("character_select.detective_card_role")
$Cards/CardDetectiveWrapper/CardInner/VBox/ToneLabel.text = UIStrings.get_text("character_select.detective_card_tone")
_confirm_btn.pressed.connect(_on_confirm)
_smuggler_wrapper.gui_input.connect(_on_card_input.bind(0))
_detective_wrapper.gui_input.connect(_on_card_input.bind(1))
_update_card_visuals()
func _input(event: InputEvent) -> void:
if not visible:
return
if event is InputEventKey and event.pressed and not event.is_echo():
match event.keycode:
KEY_LEFT:
_selected_index = 0
_update_card_visuals()
get_viewport().set_input_as_handled()
KEY_RIGHT:
_selected_index = 1
_update_card_visuals()
get_viewport().set_input_as_handled()
KEY_ENTER, KEY_KP_ENTER:
_on_confirm()
get_viewport().set_input_as_handled()
KEY_ESCAPE:
archetype_cancelled.emit()
get_viewport().set_input_as_handled()
func _on_card_input(event: InputEvent, card_index: int) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
_selected_index = card_index
_update_card_visuals()
func _on_confirm() -> void:
archetype_confirmed.emit(ARCHETYPES[_selected_index])
func _update_card_visuals() -> void:
_set_card_selected(_smuggler_wrapper, _selected_index == 0)
_set_card_selected(_detective_wrapper, _selected_index == 1)
_confirm_btn.grab_focus()
func _set_card_selected(wrapper: Control, selected: bool) -> void:
var border: ColorRect = wrapper.get_node("CardBorder")
var inner: ColorRect = wrapper.get_node("CardInner")
border.color = CARD_BORDER_SELECTED if selected else CARD_BORDER_NORMAL
inner.color = CARD_BG_SELECTED if selected else CARD_BG_NORMAL
+1
View File
@@ -0,0 +1 @@
uid://char_select_sr
+326
View File
@@ -0,0 +1,326 @@
class_name DebugConsole
extends Control
## In-game debug console (#581). Tilde key (`) toggles open/closed.
## Semi-transparent panel anchored to bottom ~40% of screen.
## Dispatches DebugCommandKind variants to server via SimBridge.
## Settings-toggled; enabled state persisted in user://settings.cfg.
## D-088: triggers Overlay pause while open — sim must not advance during debug input.
signal pause_requested # D-088: pause sim while console is open
signal unpause_requested # D-088: unpause sim when console closes
const PREFS_PATH := "user://settings.cfg"
const PREFS_SECTION := "debug"
const PREFS_KEY_ENABLED := "console_enabled"
const MAX_LOG_LINES := 50
const BG_COLOR := Color(0.04, 0.04, 0.06, 0.92)
const BORDER_COLOR := Color("#4a9ebb")
const TEXT_COLOR := Color("#c8d0e0")
const SUCCESS_COLOR := Color("#6bc9a6")
const ERROR_COLOR := Color("#d45d5d")
const INPUT_COLOR := Color("#e8c547")
var _enabled: bool = true
var _open: bool = false
var _log_lines: Array[String] = []
var _panel: PanelContainer = null
var _output_log: RichTextLabel = null
var _input_line: LineEdit = null
var _history: Array[String] = []
var _history_idx: int = -1
func _ready() -> void:
_load_prefs()
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
set_anchors_preset(Control.PRESET_FULL_RECT)
_build_ui()
get_viewport().size_changed.connect(_update_panel_layout)
func _build_ui() -> void:
_panel = PanelContainer.new()
_panel.mouse_filter = Control.MOUSE_FILTER_STOP
_panel.anchor_left = 0.0
_panel.anchor_top = 0.6
_panel.anchor_right = 1.0
_panel.anchor_bottom = 1.0
_panel.offset_left = 0.0
_panel.offset_top = 0.0
_panel.offset_right = 0.0
_panel.offset_bottom = 0.0
var bg_style := StyleBoxFlat.new()
bg_style.bg_color = BG_COLOR
bg_style.border_color = BORDER_COLOR
bg_style.border_width_top = 1
bg_style.content_margin_left = 8.0
bg_style.content_margin_right = 8.0
bg_style.content_margin_top = 6.0
bg_style.content_margin_bottom = 6.0
_panel.add_theme_stylebox_override("panel", bg_style)
add_child(_panel)
var vbox := VBoxContainer.new()
vbox.add_theme_constant_override("separation", 4)
_panel.add_child(vbox)
_output_log = RichTextLabel.new()
_output_log.bbcode_enabled = true
_output_log.size_flags_vertical = Control.SIZE_EXPAND_FILL
_output_log.scroll_following = true
_output_log.selection_enabled = true
_output_log.add_theme_color_override("default_color", TEXT_COLOR)
_output_log.add_theme_font_size_override("normal_font_size", 13)
vbox.add_child(_output_log)
var sep := HSeparator.new()
vbox.add_child(sep)
_input_line = LineEdit.new()
_input_line.placeholder_text = "enter command (help for list)"
_input_line.clear_button_enabled = false
_input_line.add_theme_font_size_override("font_size", 13)
_input_line.add_theme_color_override("font_color", INPUT_COLOR)
_input_line.text_submitted.connect(_on_input_submitted)
_input_line.gui_input.connect(_on_input_key)
vbox.add_child(_input_line)
func _update_panel_layout() -> void:
# Anchors handle resize automatically; no manual size calc needed.
pass
# -- Input handling --
func _unhandled_input(event: InputEvent) -> void:
if not _enabled:
return
if not event is InputEventKey or not event.pressed or event.echo:
return
if event.keycode == KEY_QUOTELEFT:
get_viewport().set_input_as_handled()
_toggle()
return
if _open:
# Consume all keyboard events — prevent movement/action leaking through
get_viewport().set_input_as_handled()
if event.keycode == KEY_ESCAPE:
_close()
func _on_input_key(event: InputEvent) -> void:
if not event is InputEventKey or not event.pressed or event.echo:
return
if event.keycode == KEY_UP:
_history_up()
get_viewport().set_input_as_handled()
elif event.keycode == KEY_DOWN:
_history_down()
get_viewport().set_input_as_handled()
func _toggle() -> void:
if _open:
_close()
else:
_open_console()
func _open_console() -> void:
_open = true
visible = true
mouse_filter = Control.MOUSE_FILTER_STOP
_input_line.clear()
_input_line.grab_focus()
_history_idx = -1
pause_requested.emit() # D-088: pause sim while typing debug commands
func _close() -> void:
_open = false
visible = false
mouse_filter = Control.MOUSE_FILTER_IGNORE
_input_line.release_focus()
unpause_requested.emit() # D-088: resume sim when console closes
func is_open() -> bool:
return _open
# -- Command input --
func _on_input_submitted(text: String) -> void:
var trimmed := text.strip_edges()
_input_line.clear()
_history_idx = -1
if trimmed.is_empty():
return
if _history.is_empty() or _history[0] != trimmed:
_history.push_front(trimmed)
if _history.size() > 20:
_history.pop_back()
_append_text("> " + trimmed, TEXT_COLOR)
_dispatch(trimmed)
func _dispatch(line: String) -> void:
var parts := line.split(" ", false)
if parts.is_empty():
return
var cmd := parts[0].to_lower()
match cmd:
"help":
_print_help()
"ticks":
if parts.size() < 2 or not parts[1].is_valid_int():
_append_text("usage: ticks <n>", ERROR_COLOR)
return
var n := int(parts[1])
if n <= 0:
_append_text("ticks: n must be > 0", ERROR_COLOR)
return
_send_debug({"AdvanceTicks": n})
"contaminate":
_send_debug("SkipToContamination")
"tp":
if parts.size() < 2:
_append_text("usage: tp <x> <y> [z] or tp <location_name>", ERROR_COLOR)
return
if parts.size() >= 3 and parts[1].is_valid_int() and parts[2].is_valid_int():
var z := 0
if parts.size() >= 4:
if parts[3].is_valid_int():
z = int(parts[3])
else:
_append_text("tp: invalid z '%s' — defaulting to 0" % parts[3], ERROR_COLOR)
_send_debug({"TeleportToPosition": {"x": int(parts[1]), "y": int(parts[2]), "z": z}})
else:
var loc := " ".join(PackedStringArray(parts.slice(1)))
_send_debug({"TeleportToLocation": loc})
"activate":
_send_debug("ForceContaminationActivate")
"triangle":
if parts.size() < 2:
_append_text("usage: triangle <id>", ERROR_COLOR)
return
_send_debug({"ForceTriangleActivation": parts[1]})
"npc":
if parts.size() < 2 or not parts[1].is_valid_int():
_append_text("usage: npc <entity_id>", ERROR_COLOR)
return
_send_debug({"InspectNpc": int(parts[1])})
"triangles":
_send_debug("ListTriangles")
"pop":
_send_debug("ListPopulation")
"status":
_send_debug("GetContaminationStatus")
_:
_append_text("unknown command: '%s' (type 'help')" % cmd, ERROR_COLOR)
func _send_debug(kind: Variant) -> void:
var err := SimBridge.send_input({
"action": InputMapper.Action.DEBUG_COMMAND,
"action_data": kind,
"timestamp_msec": Time.get_ticks_msec(),
})
if err != OK:
_append_text("send error: %s" % error_string(err), ERROR_COLOR)
# -- Response display --
## Append a server debug response to the output log. Auto-opens console if closed
## (only if console is enabled — respect user's settings toggle).
func append_response(response: Dictionary) -> void:
var success: bool = response.get("success", false)
var text: String = response.get("text", "")
var color := SUCCESS_COLOR if success else ERROR_COLOR
_append_text(text, color)
if not _open and _enabled:
_open_console()
# -- Log rendering --
func _append_text(text: String, color: Color) -> void:
var escaped := text.replace("[", "[lb]").replace("]", "[rb]")
_log_lines.append("[color=%s]%s[/color]" % [color.to_html(false), escaped])
if _log_lines.size() > MAX_LOG_LINES:
_log_lines = _log_lines.slice(_log_lines.size() - MAX_LOG_LINES)
if _output_log:
_output_log.text = "\n".join(_log_lines)
func _print_help() -> void:
_append_text(
"Commands:\n"
+ " ticks <n> — fast-forward N ticks\n"
+ " contaminate — skip to contamination phase\n"
+ " tp <x> <y> [z] — teleport to tile position\n"
+ " tp <location> — teleport to named location\n"
+ " activate — force contamination activate\n"
+ " triangle <id> — force triangle activation\n"
+ " npc <entity_id> — inspect NPC state\n"
+ " triangles — list all triangles\n"
+ " pop — list active NPCs\n"
+ " status — contamination status\n"
+ " help — this list",
TEXT_COLOR
)
# -- Command history --
func _history_up() -> void:
if _history.is_empty():
return
_history_idx = mini(_history_idx + 1, _history.size() - 1)
_input_line.text = _history[_history_idx]
_input_line.caret_column = _input_line.text.length()
func _history_down() -> void:
if _history_idx <= 0:
_history_idx = -1
_input_line.clear()
return
_history_idx -= 1
_input_line.text = _history[_history_idx]
_input_line.caret_column = _input_line.text.length()
# -- Settings --
func set_enabled(enabled: bool) -> void:
_enabled = enabled
if not _enabled and _open:
_close()
_save_prefs()
func is_enabled() -> bool:
return _enabled
func _load_prefs() -> void:
var cfg := ConfigFile.new()
if cfg.load(PREFS_PATH) != OK:
return
_enabled = cfg.get_value(PREFS_SECTION, PREFS_KEY_ENABLED, true)
func _save_prefs() -> void:
var cfg := ConfigFile.new()
cfg.load(PREFS_PATH) # load existing (may have other sections like "audio")
cfg.set_value(PREFS_SECTION, PREFS_KEY_ENABLED, _enabled)
var err := cfg.save(PREFS_PATH)
if err != OK:
push_warning("DebugConsole: failed to save prefs (%d)" % err)
+1
View File
@@ -0,0 +1 @@
uid://c8pvt3xr7kmd2
+15
View File
@@ -0,0 +1,15 @@
[gd_scene load_steps=2 format=3 uid="uid://b2ndm9rvx8cqp"]
[ext_resource type="Script" uid="uid://c8pvt3xr7kmd2" path="res://ui/debug_console.gd" id="1_debug_console"]
; #581: In-game debug console. Tilde key toggles. ModalLayer.
; UI built programmatically in _ready() — scene contains only root node + script.
[node name="DebugConsole" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 1
script = ExtResource("1_debug_console")
+68 -13
View File
@@ -45,6 +45,15 @@ var _option_texts: Array[String] = []
var _option_is_confrontation: Array[bool] = []
var _npc_name: String = ""
# -- Entity color registry (#573) --
# Maps entity_id → Color for dialogue participants.
# Assigned from _npc_colors palette on first encounter; player uses _player_color.
# v0.1: colors are per-conversation — cleared in _end_player_conversation() to avoid
# palette exhaustion (8 entries) across long sessions with 9+ NPCs.
var _npc_entity_colors: Dictionary = {} # entity_id -> Color
var _npc_entity_id: int = -1 # Entity ID of the current player conversation NPC
var _next_npc_color: int = 0 # Round-robin palette index for client-side assignment
# -- UI state --
var _active_tween: Tween = null
var _beat_tween: Tween = null # D-063: confrontation beat delay
@@ -186,16 +195,23 @@ func _update_layout() -> void:
## speaker/target: display names. text: the spoken line.
## is_passive: true for overheard NPC-NPC (renders with ┃ prefix + desaturated).
## Active conversation entries are pinned (no timeout) while _in_player_conversation.
func append_line(speaker: String, target: String, text: String, is_passive: bool = false) -> void:
## speaker_entity_id/target_entity_id: optional entity IDs for stable color lookup (#573).
## TODO Phase 2: 6 positional params is unwieldy — consider dictionary-options overload.
func append_line(speaker: String, target: String, text: String, is_passive: bool = false, speaker_entity_id: int = -1, target_entity_id: int = -1) -> void:
var pinned := not is_passive and _in_player_conversation
_log_entries.append({
var entry: Dictionary = {
"speaker": speaker,
"target": target,
"text": text,
"is_passive": is_passive,
"pinned": pinned,
"timestamp_msec": Time.get_ticks_msec(),
})
}
if speaker_entity_id >= 0:
entry["speaker_entity_id"] = speaker_entity_id
if target_entity_id >= 0:
entry["target_entity_id"] = target_entity_id
_log_entries.append(entry)
_log_dirty = true
_ensure_visible()
@@ -263,26 +279,34 @@ func on_conversation_ended(_event: Dictionary) -> void:
## Append the player's chosen response to the log.
func append_player_line(target_npc: String, text: String) -> void:
append_line(PLAYER_NAME, target_npc, text, false)
append_line(PLAYER_NAME, target_npc, text, false, -1, _npc_entity_id)
## Append an NPC follow-up line (from dialogue_response).
func append_dialogue_response(npc_name: String, text: String) -> void:
append_line(npc_name, PLAYER_NAME, text, false)
## Note: expects show_dialogue() to have been called first to set _npc_entity_id.
## Defensive: if entity_id is valid but not yet registered, _assign_npc_color handles it.
func append_dialogue_response(npc_name: String, text: String, entity_id: int = -1) -> void:
if entity_id >= 0:
_assign_npc_color(entity_id)
append_line(npc_name, PLAYER_NAME, text, false, entity_id, -1)
# -- Active player conversation --
## Show dialogue with NPC speech and response options.
## npc_name: who is speaking. speech: the NPC's line. options: player choices.
func show_dialogue(npc_name: String, speech: String, options: Array = []) -> void:
## npc_entity_id: entity ID of the NPC for stable color assignment (#573).
func show_dialogue(npc_name: String, speech: String, options: Array = [], npc_entity_id: int = -1) -> void:
_npc_name = npc_name
_npc_entity_id = npc_entity_id
_cancel_beat()
_in_player_conversation = true
if npc_entity_id >= 0:
_assign_npc_color(npc_entity_id)
# Append NPC's line to the log
if not speech.is_empty():
append_line(npc_name, PLAYER_NAME, speech, false)
append_line(npc_name, PLAYER_NAME, speech, false, npc_entity_id, -1)
# Clear old options and show new ones
_clear_options()
@@ -316,6 +340,11 @@ func _end_player_conversation() -> void:
entry.timestamp_msec = now
_log_dirty = true
# #573: Clear per-conversation color registry to avoid palette exhaustion
_npc_entity_colors.clear()
_npc_entity_id = -1
_next_npc_color = 0
# D-069: Clear dialogue/confrontation dip — coordinator routes to AudioManager
audio_dip_cleared.emit()
@@ -509,17 +538,26 @@ func _format_entry(entry: Dictionary, alpha: float) -> String:
# Legacy string-keyed entry (player dialogue, backward compat)
speaker = _escape_bbcode(entry.get("speaker", "?"))
target = _escape_bbcode(entry.get("target", "?"))
speaker_color = _color_for_name(entry.get("speaker", "?"))
target_color = _color_for_name(entry.get("target", "?"))
# #573: use entity-ID-bound color if available; fall back to name-hash
var sp_eid: int = entry.get("speaker_entity_id", -1)
var tg_eid: int = entry.get("target_entity_id", -1)
if sp_eid >= 0 and _npc_entity_colors.has(sp_eid):
speaker_color = _npc_entity_colors[sp_eid]
else:
speaker_color = _color_for_name(entry.get("speaker", "?"))
if tg_eid >= 0 and _npc_entity_colors.has(tg_eid):
target_color = _npc_entity_colors[tg_eid]
else:
target_color = _color_for_name(entry.get("target", "?"))
involves_player = (entry.get("speaker", "") == PLAYER_NAME) or (entry.get("target", "") == PLAYER_NAME)
var text: String = _escape_bbcode(entry.text)
var is_passive: bool = entry.is_passive
# Desaturate passive name colours (Araminta review)
# Desaturate passive name colours (Araminta review), re-enforce contrast floor after
if is_passive:
speaker_color = _desaturate(speaker_color, PASSIVE_DESATURATION)
target_color = _desaturate(target_color, PASSIVE_DESATURATION)
speaker_color = _enforce_contrast(_desaturate(speaker_color, PASSIVE_DESATURATION))
target_color = _enforce_contrast(_desaturate(target_color, PASSIVE_DESATURATION))
var sc := _color_with_alpha(speaker_color, alpha)
var ac := _color_with_alpha(_arrow_color, alpha)
@@ -546,6 +584,23 @@ static func _escape_bbcode(text: String) -> String:
return text.replace("[", "[lb]").replace("]", "[rb]")
## Assign a palette color to an NPC entity ID on first encounter (#573).
## Returns the same color on subsequent calls for the same entity ID.
## TODO D-033 Phase 2: derive from relationship color — current independent palette
## will need alignment when relationship-based entity colors arrive.
func _assign_npc_color(entity_id: int) -> Color:
if entity_id < 0:
return _speech_color
if _npc_entity_colors.has(entity_id):
return _npc_entity_colors[entity_id]
if _npc_colors.is_empty():
return _speech_color
var color := _enforce_contrast(_npc_colors[_next_npc_color % _npc_colors.size()])
_next_npc_color += 1
_npc_entity_colors[entity_id] = color
return color
## Get a stable color for a character name, with contrast floor enforcement.
func _color_for_name(char_name: String) -> Color:
if char_name == PLAYER_NAME:
+37 -2
View File
@@ -1,10 +1,12 @@
extends Control
## #258: Main menu — New Game / Continue / Load Game / Quit.
## New Game: generates per-game save directory (D-085), starts game.
## New Game: shows character select panel (D-085 save dir created after archetype chosen).
## Continue: loads most recent save directory.
## Load Game: shows sorted save list for manual selection (#257).
## #588: Character archetype selection — panel shown between New Game click and game load.
const GAME_SCENE := "res://scenes/main.tscn"
const CHARACTER_SELECT_SCENE := "res://scenes/character_select.tscn"
const BG_COLOR := Color(0.05, 0.05, 0.08, 1.0)
const TITLE_COLOR := Color("#c8d0e0")
@@ -23,6 +25,8 @@ const FONT_SIZE_BTN := 15
@onready var _saves_list: VBoxContainer = $LoadGamePanel/VBox/SavesScroll/SavesList
@onready var _load_back_btn: Button = $LoadGamePanel/VBox/BackBtn
var _char_select: Control = null # Instantiated on demand
func _ready() -> void:
_new_game_btn.pressed.connect(_on_new_game)
@@ -41,14 +45,45 @@ func _refresh_continue_state() -> void:
func _on_new_game() -> void:
GameState.pending_load_path = "" # clear stale load path from previous Load selection
# #588: Show character select before creating the save directory.
# ESC on character select cancels with no directory created.
GameState.pending_load_path = ""
_show_character_select()
func _show_character_select() -> void:
if _char_select != null and is_instance_valid(_char_select):
_char_select.queue_free()
var scene := load(CHARACTER_SELECT_SCENE) as PackedScene
if scene == null:
push_error("MainMenu: failed to load character_select.tscn")
return
_char_select = scene.instantiate()
add_child(_char_select)
_char_select.archetype_confirmed.connect(_on_archetype_confirmed)
_char_select.archetype_cancelled.connect(_on_archetype_cancelled)
func _on_archetype_confirmed(archetype: String) -> void:
if _char_select != null and is_instance_valid(_char_select):
_char_select.queue_free()
_char_select = null
# Set archetype before new_game() so SessionManager can persist it.
GameState.character_archetype = archetype
var game_id := SessionManager.new_game()
if game_id.is_empty():
push_error("MainMenu: new_game() failed to create save directory — cannot start")
return
SessionManager.save_character_archetype(game_id, archetype)
get_tree().change_scene_to_file(GAME_SCENE)
func _on_archetype_cancelled() -> void:
if _char_select != null and is_instance_valid(_char_select):
_char_select.queue_free()
_char_select = null
func _on_continue() -> void:
GameState.pending_load_path = "" # clear stale load path from previous Load selection
var saves := SessionManager.list_game_dirs()
+59
View File
@@ -0,0 +1,59 @@
extends Control
## #592: News ticker — scrolling horizontal headline bar, active in The Last Shift zone.
## Lives on UILayer (z-layer 7 per D-049). Not suppressed by insert_active (D-013):
## the ticker is a real-world screen the player can see regardless of insert state.
## Text scrolls left at SCROLL_SPEED px/sec. When current_ticker is null, hides.
const BG_COLOR := Color(0.05, 0.05, 0.07, 0.75)
const TEXT_COLOR := Color(0.784, 0.816, 0.878, 1.0) # INSERT_COLOR_TEXT
const FONT_SIZE := 13
const SCROLL_SPEED := 60.0 # pixels per second
const BAR_HEIGHT := 28
@onready var _label: Label = $TickerLabel
var _text: String = ""
var _scroll_x: float = 0.0
var _content_width: float = 0.0
func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
_label.add_theme_font_size_override("font_size", FONT_SIZE)
_label.add_theme_color_override("font_color", TEXT_COLOR)
visible = false
func update_from_state() -> void:
var ticker: Variant = GameState.current_snapshot.get("current_ticker")
if ticker == null or not ticker is Dictionary:
visible = false
return
var new_text: String = ticker.get("text", "")
if new_text.is_empty():
visible = false
return
if new_text != _text:
_text = new_text
_label.text = _text
# Reset scroll to start from right edge on new headline.
# Defer width read by one frame: get_minimum_size() returns stale
# data if called before the layout pass that follows text assignment.
_scroll_x = size.x
_content_width = 0.0 # will be updated after layout in _process
call_deferred("_update_content_width")
visible = true
func _update_content_width() -> void:
_content_width = _label.get_minimum_size().x
func _process(delta: float) -> void:
if not visible:
return
_scroll_x -= SCROLL_SPEED * delta
# Restart from right edge when text has fully exited left.
if _scroll_x + _content_width < 0.0:
_scroll_x = size.x
_label.position.x = _scroll_x
+1
View File
@@ -0,0 +1 @@
uid://news_ticker_sr
+33
View File
@@ -0,0 +1,33 @@
[gd_scene load_steps=2 format=3 uid="uid://news_ticker_scene_sr"]
[ext_resource type="Script" path="res://ui/news_ticker.gd" id="1_newsticker"]
; #592: News ticker — scrolling headline bar. Lives on UILayer (z-layer 7).
; Anchored top-left to top-right, 28px tall. Hidden when current_ticker is null.
[node name="NewsTicker" type="Control"]
layout_mode = 1
anchors_preset = 10
anchor_left = 0.0
anchor_top = 0.0
anchor_right = 1.0
anchor_bottom = 0.0
offset_bottom = 28.0
clip_contents = true
script = ExtResource("1_newsticker")
[node name="TickerBg" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
color = Color(0.05, 0.05, 0.07, 0.75)
mouse_filter = 2
[node name="TickerLabel" type="Label" parent="."]
layout_mode = 0
offset_top = 4.0
offset_bottom = 24.0
theme_override_font_sizes/font_size = 13
theme_override_colors/font_color = Color(0.784, 0.816, 0.878, 1.0)
text = ""
+30 -1
View File
@@ -11,7 +11,7 @@ const TITLE_COLOR := Color("#4a9ebb")
const FONT_SIZE := 14
const BOX_WIDTH := 460
const BOX_HEIGHT := 340
const BOX_HEIGHT := 376 # +36 for Debug Console row
const PADDING := 20
const ROW_HEIGHT := 36
@@ -28,6 +28,7 @@ var _active: bool = false
var _container: VBoxContainer = null
signal closed
signal debug_console_toggled(enabled: bool) # #581: debug console enabled/disabled
func _ready() -> void:
@@ -109,6 +110,34 @@ func _build_ui() -> void:
db_label.text = _format_db(value)
)
# #581: Debug Console toggle
var debug_hbox := HBoxContainer.new()
debug_hbox.custom_minimum_size = Vector2(0, ROW_HEIGHT)
_container.add_child(debug_hbox)
var debug_label := Label.new()
debug_label.text = "Debug Console"
debug_label.custom_minimum_size = Vector2(150, 0)
debug_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
debug_label.add_theme_font_size_override("font_size", FONT_SIZE)
debug_label.add_theme_color_override("font_color", TEXT_COLOR)
debug_hbox.add_child(debug_label)
var debug_check := CheckButton.new()
# Query live DebugConsole node if available; fall back to prefs file
var console_node := get_node_or_null("/root/Main/ModalLayer/DebugConsole")
if console_node and console_node.has_method("is_enabled"):
debug_check.button_pressed = console_node.is_enabled()
else:
var cfg := ConfigFile.new()
debug_check.button_pressed = true
if cfg.load(DebugConsole.PREFS_PATH) == OK:
debug_check.button_pressed = cfg.get_value(DebugConsole.PREFS_SECTION, DebugConsole.PREFS_KEY_ENABLED, true)
debug_check.toggled.connect(func(enabled: bool) -> void:
debug_console_toggled.emit(enabled)
)
debug_hbox.add_child(debug_check)
# Spacer
var spacer := Control.new()
spacer.custom_minimum_size = Vector2(0, 8)
+1 -1
View File
@@ -70,7 +70,7 @@ func _cache_geometry() -> void:
_day_size = font.get_string_size(_day_text, HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META)
_meta_h = font.get_string_size("A", HORIZONTAL_ALIGNMENT_LEFT, -1, FONT_SIZE_META).y
var meta_w := _phase_size.x + _day_size.x
var content_w := max(_time_size.x, meta_w)
var content_w := maxf(_time_size.x, meta_w)
_box_w = content_w + PADDING.x * 2
_box_h = PADDING.y * 2 + _time_size.y + 3 + _meta_h
+7 -2
View File
@@ -75,12 +75,13 @@
"emergency",
"routine",
"observation",
"greeting"
"greeting",
"triangle_activated"
]
},
"minItems": 1,
"uniqueItems": true,
"description": "D-035 structural tag: situations in which this monologue line is contextually appropriate. 14 v0.1 values. The engine selects using trigger; situation provides additional authoring context for filtering by the caller. NOTE: 'greeting' is not yet in server/src/content/line_pool.rs Situation enum."
"description": "D-035 structural tag: situations in which this monologue line is contextually appropriate. 15 v0.1 values (triangle_activated added Sprint 24). The engine selects using trigger; situation provides additional authoring context for filtering by the caller. NOTE: 'greeting' and 'triangle_activated' are not yet in server/src/content/line_pool.rs Situation enum."
},
"trigger": {
"type": "string",
@@ -124,6 +125,10 @@
"enum": ["unknown", "known", "friendly", "person_of_interest", "hostile"]
}
}
},
"npc_in_los": {
"type": "boolean",
"description": "Gate: line only fires when the triggering NPC is in the player's line of sight. Added Sprint 24 for triangle_activated observe_npc lines."
}
}
}
@@ -4,10 +4,13 @@
display_name: "Sova Transit District"
description: >
A 40-year-old prefab-modular-retrofitted freight logistics hub on Station Sova.
Three social sites: The Terminal (logistics hub), The Last Shift (bar),
and maintenance corridors.
Five locations: The Terminal (logistics hub), The Last Shift (bar),
maintenance corridors, gate corridor (ground level), and the observation
gallery (Commission-only, z=2 above the gate concourse).
locations:
- "the-terminal"
- "the-last-shift"
- "maintenance-corridors"
- "gate-ground"
- "gate-gallery"
npc_count: 17
@@ -0,0 +1,48 @@
# Location: Gate Corridor — Observation Gallery
# Source: D-093 gate cluster zone spec
# Zone palette: surface #b8bec4 / fog tint #0a1222
# z=2 (above gate concourse)
canonical_id: "krenn.sova.transit.location.gate-gallery"
display_name: "Observation Gallery"
description: >
Commission-only observation gallery overlooking the gate concourse from
z=2. The gallery rail is a transparent low wall — occupants can see
down to the concourse below, but upward LOS from z=1 is blocked except
at the staircase. Access restricted to Commission personnel.
tile_bounds:
x_min: 0
y_min: 0
x_max: 31
y_max: 9
z: 2
# Legend:
# R = Restricted (Commission-only access)
# W = Wall (solid)
#
# Layout (32 wide x 10 tall):
# Row 0: North wall with staircase entrance (col 1 = R, access point)
# Rows 1-8: Gallery floor (R tiles — restricted access)
# Row 9: South wall (gallery rail — transparent low wall, modeled as W;
# cross-z LOS is handled server-side)
tiles:
- "WRWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRW"
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
sightlines:
open: true
notes: >
Gallery rail is transparent — full downward LOS to gate concourse (z=1).
Upward LOS from concourse to gallery is blocked except at staircase.
Cross-z LOS behavior handled by server shadowcasting system.
@@ -0,0 +1,83 @@
# Location: Gate Corridor — Ground Level
# Source: D-093 gate cluster zone spec
# Zone palette: surface #b8bec4 / fog tint #0a1222
canonical_id: "krenn.sova.transit.location.gate-ground"
display_name: "Gate Corridor"
description: >
The gate cluster handles all traffic between Station Sova and the horizon
gate network. From north to south: the restricted aperture chamber where
span gate transits occur, freight staging and passenger arrival halls,
customs lanes (freight and pedestrian), and the wide gate concourse
where arrivals disperse into the transit district.
tile_bounds:
x_min: 0
y_min: 0
x_max: 39
y_max: 33
z: 1
# Legend:
# F = Floor (walkable) W = Wall (solid, blocks LOS)
# R = Restricted (access-tier gated)
#
# Layout (40 wide x 34 tall):
# Row 0: North wall
# Rows 1-4: Aperture chamber (centered 8 wide, R tiles — restricted)
# Row 5: Wall separator
# Rows 6-13: Freight staging (west, 24 tiles) | passenger arrival (east, 13 tiles)
# Row 14: Wall with door gaps (cols 12, 32)
# Rows 15-24: Freight customs (west, 20 tiles, 3 lanes at 6vt + wall stubs)
# | corridor (6 tiles) | ped customs (east, 10 tiles, 3 lanes)
# Row 25: Wall with wide opening to concourse (cols 5-34 open)
# Rows 26-33: Gate concourse (full 38-tile width, 8 rows — public open space)
#
# D-093 zone dimensions: freight customs 20x10, ped customs 12x10 (10 rows each)
# Corridor width: gate concourse 8vt (D-093)
tiles:
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
- "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW"
- "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW"
- "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW"
- "WWWWWWWWWWWWWWWWRRRRRRRRWWWWWWWWWWWWWWWW"
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFFFW"
- "WWWWWWWWWWWWFWWWWWWWWWWWWWWWWWWWFWWWWWWW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WFFFFFFWFFFFFFWFFFFFFWFFFFFFWFFWFFFWFFFW"
- "WWWWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWWWWW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
sightlines:
open: false
notes: >
Gate concourse (rows 26-33) is wide open with full sightlines.
Customs lanes have wall stubs creating partial occlusion between
lanes. Freight customs (west, 20 tiles, 3 lanes at 6vt) is wider
than pedestrian customs (east, 10 tiles, 3 lanes). Aperture chamber
is fully walled and restricted. Cross-z LOS from the observation
gallery (z=2) into the concourse is handled server-side.
@@ -1,2 +1,54 @@
# Location: Maintenance Corridors (smuggling spaces)
# canonical_id: krenn.sova.transit.location.maintenance-corridors
# Source: D-093, #313 (Sprint 12 spatial layout)
# Zone palette: surface #4e5054 / fog tint #101214
# z=0 (Era 1, below main structures)
canonical_id: "krenn.sova.transit.location.maintenance-corridors"
display_name: "Maintenance Corridors"
description: >
Era 1 maintenance infrastructure beneath the transit district. A long
transition corridor connects the logistics hub to the bar district.
Restricted storage at the west end is shared with The Terminal above.
A maintenance hatch at the east end provides the cross-z connection
to the main level. Everything reads as mundane maintenance — the ring
operates here because nobody looks twice.
tile_bounds:
x_min: 0
y_min: 0
x_max: 57
y_max: 5
z: 0
# Legend:
# F = Floor (walkable) W = Wall (solid, blocks LOS)
# R = Restricted (access-tier gated)
#
# Layout (58 wide x 6 tall):
# Row 0: North wall
# Row 1: Restricted storage (west, 8 tiles) | wall | corridor (40 tiles) | wall | hatch room (east, 6)
# Row 2: Doors connect rooms (F in wall positions at cols 9 and 50)
# Row 3-4: Same as row 1 (walls between rooms)
# Row 5: South wall
#
# Corridor width: 2vt visible at rows 1-4 (internal height, maintenance standard)
# Transition corridor: 40 tiles (~40m at 1m/vt) between storage and hatch per D-093
# Restricted storage: R tiles for access restriction
# Maintenance hatch: F tiles (cross-z connection is future movement feature)
# Design: mundane maintenance appearance per D-093 G-08
tiles:
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
- "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WRRRRRRRRFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WRRRRRRRRWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
sightlines:
open: false
notes: >
Long straight corridor (40 tiles) with limited concealment. Storage and
hatch rooms are separated by walls with single-tile doors at row 2.
Mundane appearance per D-093 G-08 — no visually suspicious tile
arrangements.
@@ -1,2 +1,71 @@
# Location: The Last Shift (bar)
# canonical_id: krenn.sova.transit.location.the-last-shift
# Source: D-093, #312 (Sprint 12 spatial layout)
# Zone palette: surface #6b4018 / fog tint #200c04
canonical_id: "krenn.sova.transit.location.the-last-shift"
display_name: "The Last Shift"
description: >
A converted maintenance bay turned bar. The long counter runs along the
west wall with full sightlines across the room. A corner booth in the
northeast sees the bar, card table, entrance, and back room — prime
observation real estate. Scattered tables fill the main floor. A back
room with an alley exit provides the key traversal route for the ring.
tile_bounds:
x_min: 0
y_min: 0
x_max: 33
y_max: 21
z: 1
# Legend:
# F = Floor (walkable) W = Wall (solid, blocks LOS)
#
# Layout (34 wide x 22 tall):
# Row 0: North exterior wall
# Rows 1-4: Main floor + corner booth (NE, cols 26-31, walled alcove)
# Row 5: Bar counter starts (cols 1-2 = W fixture)
# Rows 6-10: Bar counter (cols 1-2 W), tables (W stubs), card table (col 25)
# Row 11: Open transition floor
# Rows 12-14: Table clusters (W stubs at cols 8, 16, 24)
# Row 15: News ticker mount (col 31 = W stub)
# Row 16: Open floor
# Row 17: Back room north wall with door (col 22 = F)
# Rows 18-20: Main floor (west) + back room (east, cols 22-32)
# Row 21: South wall with alley exit gap (cols 29-32 = F)
tiles:
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFWFFFFFFW"
- "WWWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WWWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFW"
- "WWWFFFFFFFWFFFFFFFWFFFFFFWFFFFFFFW"
- "WWWFFFFFFFWFFFFFFFWFFFFFFFFFFFFFFW"
- "WWWFFFFFFFFFFFWFFFFFFFFFFFFFFFFFFW"
- "WWWFFFFFFFFFFFWFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFWFFFFFFFWFFFFFFFWFFFFFFFFW"
- "WFFFFFFFWFFFFFFFWFFFFFFFWFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWFW"
- "WFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFWFWFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW"
- "WFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFFFW"
- "WWWWWWWWWWWWWWWWWWWWWWWWWWWWWFFFFW"
sightlines:
open: false
notes: >
Corner booth (NE alcove, rows 1-4) has LOS to bar counter, card table
area, main entrance, and back room door. Bar counter along west wall
(cols 1-2, rows 5-10) gives staff full sightlines across the main
floor. Back room is partially occluded by wall at col 21. Back room
door at col 22. Table W-stubs create partial cover but do not fully
block sightlines.
social_site: "bar"
@@ -1,2 +1,77 @@
# Location: The Terminal (logistics hub)
# canonical_id: krenn.sova.transit.location.the-terminal
# Source: D-093, #311 (Sprint 12 spatial layout)
# Zone palette: surface #7a8490 / fog tint #0d1520
canonical_id: "krenn.sova.transit.location.the-terminal"
display_name: "The Terminal"
description: >
Sova's freight logistics hub. Scanner bays filter incoming cargo at the
south entrance; a wide main corridor runs north through the manifest
processing floor. The break room sits in the northeast. The supervisor's
office overlooks the corridor through a large interior window. Restricted
storage in the northwest corner is where the ring hides re-tagged cargo.
tile_bounds:
x_min: 0
y_min: 0
x_max: 43
y_max: 27
z: 1
# Legend:
# F = Floor (walkable) W = Wall (solid, blocks LOS)
# V = Void (outside building) R = Restricted (access-tier gated)
#
# Layout (44 wide x 28 tall):
# Rows 0: North exterior wall
# Rows 1-4: Restricted storage (NW, 11 tiles) + corridor + supervisor office (NE, 9 tiles)
# Row 5: Storage south wall; corridor + office continue
# Rows 6-8: Open corridor; supervisor office east side
# Row 7: ** Supervisor window — col 28 is F (LOS gap into office) **
# Row 9: Supervisor office south wall
# Rows 10-11: Main corridor (full width chokepoint — high sightline value)
# Rows 12-17: Manifest processing (west, 14 tiles) + break room (east, 13 tiles)
# Row 18: Break room south wall; manifest open to corridor
# Rows 19-21: South corridor
# Row 22: Scanner bay north walls with entry gaps
# Rows 23-26: Scanner bay interiors (3 bays)
# Row 27: South exterior wall
tiles:
- "VVVVVWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWVVVVV"
- "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWRRRRRRRRRRRWFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWWWWWWWWWWWWWFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFWFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFWWWWWWWWWWWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFWFFFWFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFWWWWWWWWWWWWWWWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFWVVVVV"
- "VVVVVWWWWFFFFFFWWWWFFFFFFFFWWWWFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV"
- "VVVVVWFFFFFFFFFWFFFFFFFFFFFWFFFFFFFFFFWVVVVV"
- "VVVVVWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWVVVVV"
sightlines:
open: false
notes: >
Supervisor office window faces main corridor at row 7, col 28 (F gap in
wall line). Player standing in the corridor has direct LOS into the
office interior. This is the primary investigative discovery moment.
social_site: "logistics-hub"
@@ -446,3 +446,77 @@ lines:
state: person_of_interest
priority: 8
tags: [npc, kael, investigation, evidence, analytical]
# --- Triangle Activation: Sera Venn / Torek Lintar (T2: Sera-Detective-Commission, D-087) ---
# Fires post-TriangleActivated when player has LOS to Sera or Torek. One beat per line.
# Beat 1: pattern recognition. Beat 2: deviation logged. Beat 3: hypothesis. Beat 4: inference. Beat 5: procedural next step.
- id: pc-detective_m_d_041
text: "Venn changed her transit route. Bay six to bay four bypass — three times today."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, sera, analytical]
prerequisites:
npc_in_los: true
- id: pc-detective_m_d_042
text: "She passed Torek without acknowledgment. Third instance logged. They have worked adjacent bays for months."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, sera, torek, behavioral]
prerequisites:
npc_in_los: true
- id: pc-detective_m_d_043
text: "Avoidance without cause. Either she does not know about the discrepancy, or she knows and has chosen silence."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, sera, analytical, friend-arc]
prerequisites:
npc_in_los: true
- id: pc-detective_m_d_044
text: "Possible explanations: fear, loyalty, complicity. Insufficient data to distinguish."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, sera, analytical, friend-arc]
prerequisites:
npc_in_los: true
- id: pc-detective_m_d_045
text: "Worth a direct conversation. Controlled setting. Not here."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, sera, procedural, friend-arc]
prerequisites:
npc_in_los: true
@@ -373,3 +373,77 @@ lines:
state: person_of_interest
priority: 9
tags: [npc, kael, contaminated-trust, friend-arc]
# --- Triangle Activation: Kael Davan (T1: Kael-Smuggler-Ring, D-087) ---
# Fires post-TriangleActivated when player has LOS to Kael. One beat per line.
# Beat 1: physical observation. Beat 2: rationalization. Beat 3: doubt. Beat 4: sensory confirmation. Beat 5: emotional break.
- id: pc-smuggler_m_s_036
text: "Kael's in the main corridor. He doesn't usually come through here."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, kael, friend-arc]
prerequisites:
npc_in_los: true
- id: pc-smuggler_m_s_037
text: "Voss probably moved him. Schedule shifts happen."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [suspicious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, kael, friend-arc]
prerequisites:
npc_in_los: true
- id: pc-smuggler_m_s_038
text: "...Kael would've told me."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [anxious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, kael, friend-arc]
prerequisites:
npc_in_los: true
- id: pc-smuggler_m_s_039
text: "He's not looking at the cargo. He's watching the exits."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [anxious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, kael, tell, friend-arc]
prerequisites:
npc_in_los: true
- id: pc-smuggler_m_s_040
text: "I don't know what I'm seeing. But I know Kael. And this isn't Kael."
role: player_character
access: [public]
trust: surface
situation: [triangle_activated]
trigger: observe_npc
mood: [anxious]
priority: 8
cooldown: 9999
tags: [triangle-signal, tell-observation, npc, kael, contradiction, friend-arc]
prerequisites:
npc_in_los: true
+1 -1
View File
@@ -1,4 +1,4 @@
-- Commonwealth Project Ticketing Database Schema
-- Settled Reach Project Ticketing Database Schema
-- Access via: python3 tooling/db/sqlite_connector.py <command>
-- DO NOT use sqlite3 CLI (crashes in Claude Code due to std::bad_alloc bug)
+3 -3
View File
@@ -10,10 +10,10 @@ 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, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109 |
| [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, D-085, D-088, D-094, D-096, D-097, D-099, D-100, D-101, D-102, D-103, D-106, D-108, D-109, D-113, D-133, D-134, D-135, D-136, D-137 |
| [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, D-086 |
| [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, D-075, D-084, D-090, D-092, D-093, D-095, D-098, D-104, D-105, D-107 |
| [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, D-087, D-089, D-091 |
| [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, D-075, D-084, D-090, D-092, D-093, D-095, D-098, D-104, D-105, D-107, D-121, D-122, D-123, D-124, D-125, D-126, D-127, D-128, D-129, D-130, D-131, D-132 |
| [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, D-087, D-089, D-091, D-114, D-115, D-116, D-117, D-118, D-119, D-120 |
| [process.md](process.md) | Team, workflow | D-004, D-021, D-022, D-040 |
| [questions.md](questions.md) | Open questions (index) | Q-001 through Q-054 |
| [questions-architecture.md](questions-architecture.md) | Technical questions | Q-001, Q-006, Q-009, Q-018Q-023, Q-029, Q-030, Q-046 |
+111 -1
View File
@@ -374,4 +374,114 @@ Technical foundation decisions that constrain implementation: engine, client-ser
---
*32 decisions. Last updated: 2026-02-28 (D-108 amended Sprint 22 — Idle state as stationary installation primitive note added, D-111 cross-reference added)*
### D-113: Tile data model — extensible per-tile properties
- **Date:** 2026-03-05
- **Decision:** Replace the current single-character tile encoding (`F/W/V/R` strings in location YAML) with a **tile palette/registry** system (option A from the design space). Tiles are typed by a palette ID; per-type properties are defined once in the palette and inherited by all tiles of that type. Per-tile overrides are supported via a sparse overlay map.
- **Current state:** Tiles are single characters in string arrays. Each character maps to a `TileKind` enum (`Floor`, `Wall`, `Door`, `Object`) and a walkability bool. `TileCell` in `WalkabilityMap` stores `{ walkable: bool, kind: TileKind }`. No per-tile properties (material, visual variant, sound, access lists, container contents, damage state, trigger zones) can be expressed.
- **Design survey — what systems need tile-level data:**
1. **Doors** — access lists (who can open), open/closed state, locked/unlocked. Currently no tile-level door data; `TileKind::Door` exists but carries no properties.
2. **Containers** — contents, capacity, searched state. Currently handled by entity `ObjectType::Container` on separate entities, not tiles. Containers should remain entities, not tile properties.
3. **Damage state**`DamageOverlay` (D-100) modifies tiles post-generation. Damage needs to degrade tile properties (walkability, visual, material) without replacing the base tile type.
4. **Visual variants** — same logical tile type (e.g., "industrial floor") with per-tile visual variation for visual richness. Currently impossible — all Floor tiles look identical to the client.
5. **Trigger zones** — tile-level triggers for entry/exit events (zone transitions, alarms, dialogue triggers). Currently handled by `ZoneMap` at zone granularity, not per-tile.
6. **Material properties** — footstep sound, movement speed modifier, surface type for particle effects. Currently all tiles produce the same footstep sound.
7. **WallBackside** (D-099) — structural classification behind wall surfaces. Already defined as an enum but not yet integrated into tile data.
- **Chosen approach — Tile Palette + Sparse Override:**
- **Tile palette** (YAML, per-district or global): defines tile types by string ID. Each type specifies: `walkable: bool`, `kind: TileKind`, `material: String` (footstep/SFX), `visual_base: String` (client sprite), `visual_variants: u8` (random variant count), `los_blocking: bool`, `movement_cost: f32` (default 1.0), optional `wall_backside: WallBackside` (D-099). The palette is the type-level contract — most tiles need no per-instance data beyond their palette ID.
- **Tile map** (YAML): retains the string-array format for human readability, but each character is a palette key (single char or short code). Backward-compatible: `F`, `W`, `V`, `R` are reserved palette keys that map to current behavior. New tile types use additional characters or a separate palette layer.
- **Sparse override map** (YAML): `overrides` key on Location — a list of `{ x, y, properties }` entries for tiles that differ from their palette type. Supports: door access lists, initial locked state, visual variant pinning, damage overlay data. Only tiles with non-default properties need entries. Keeps the string map clean for 90%+ of tiles.
- **Runtime representation:**
- `TilePalette` resource: `BTreeMap<char, TileType>` loaded at startup. Immutable after load.
- `TileCell` extended: `{ palette_id: char, walkable: bool, kind: TileKind, material_id: u16 }`. Material ID is a compact index into the palette's material table.
- `TileOverrideMap` resource: `BTreeMap<(i32, i32, i32), TileOverride>` for per-tile overrides. Sparse — only tiles with overrides consume memory.
- ECS queries: `WalkabilityMap` remains the primary interface for movement/pathfinding (unchanged API). `TilePalette` provides material/visual data when needed (snapshot construction, sound system). `TileOverrideMap` provides door state, access lists, damage overlays.
- **YAML authoring format:**
```yaml
# Palette definition (loaded once, reusable across locations)
palette:
F: { walkable: true, kind: Floor, material: metal-grate, visual_base: floor_industrial }
W: { walkable: false, kind: Wall, material: bulkhead, visual_base: wall_heavy, los_blocking: true }
D: { walkable: true, kind: Door, material: metal-door, visual_base: door_standard }
G: { walkable: true, kind: Floor, material: glass-panel, visual_base: floor_glass }
R: { walkable: false, kind: Floor, material: metal-grate, visual_base: floor_restricted }
# Location tile map (unchanged human-readable format)
tiles:
- "WWWWWWWWWWWWWW"
- "WFFFFDFFFFFFFW"
- "WFFFFFFFFFFGFW"
- "WWWWWWWWWWWWWW"
# Per-tile overrides (sparse, only for non-default properties)
overrides:
- { x: 5, y: 1, door_access: [faction.commission], locked: true }
- { x: 12, y: 2, visual_variant: 3 }
```
- **Loader contract:** `ContentPlugin` loads palette YAML first, then location tiles. The `apply_location_tiles()` function resolves each character via palette lookup instead of the current hardcoded match. Unknown characters fall back to `Floor` with a warning (same as current behavior). Overrides are loaded after tiles and applied to `TileOverrideMap`.
- **Migration effort for existing locations (5 files):**
- **Zero-migration path:** The default palette defines `F/W/V/R` with identical behavior to current hardcoded mapping. Existing location YAMLs work unchanged. No migration required for v0.1.
- **Incremental enrichment:** Locations can opt into the new palette by adding a `palette:` key. Locations without `palette:` use the global default. Migration is per-location, at author pace.
- **Estimated effort:** Palette definition = 0.5 day. Loader refactor = 1-2 days. Override system = 1 day. Total: 2-4 developer-days. No changes to location YAML files required for v0.1.
- **Alternatives considered:**
- **(b) Per-tile property bags** (arbitrary key-value per tile): Maximum flexibility but violates D-010 principle 4 (deterministic — dynamic typing makes serialization non-deterministic). Memory cost: ~100 bytes/tile vs ~6 bytes/tile with palette. Rejected.
- **(c) ECS-style tile components** (tiles as entities): Each tile becomes a bevy_ecs entity with optional components. Elegant in theory but 150x150x3 = 67,500 entities per location, potentially 4M+ entities for a district. ECS entity overhead (~128 bytes each) makes this prohibitively expensive. Queries scale poorly at this count. Rejected for spatial data; tiles remain grid-based. Entities are reserved for interactive objects placed ON tiles.
- **(d) Hybrid (palette + entity overlay):** Palette for base tiles, entities for interactive tile features (doors, containers, triggers). This is *almost* what we chose — the distinction is that our sparse override map is grid-indexed (O(1) lookup by position) rather than entity-query based. Interactive objects that have their own behavior (NPCs, containers, items) remain entities; tile properties that are spatial/static (material, visual variant, access) are grid data.
- **Key design principles:**
- Palette is the type; override is the instance. 90%+ of tiles need only a palette ID.
- String-array tile maps remain human-readable and merge-friendly. No JSON, no complex nested structures.
- `WalkabilityMap` API is unchanged — callers don't know about palettes.
- BTreeMap for deterministic iteration per D-010 principle 4.
- Palette keys are `char` (single Unicode codepoint) for direct mapping from tile string arrays.
- **Raised by:** Tyre (architecture), requested by #586 (Epic: extensible tile data model).
- **Dissent:** None anticipated — this is a design-only D-record for post-v0.1 implementation.
- **Cross-reference:** D-054 (tile-based movement), D-066 (dual-scale grid), D-094 (spatial hierarchy), D-099 (WallBackside classification), D-100 (DamageOverlay), D-012 (chunk architecture)
---
### D-133: Skills affect outcome — same verbs available, skill determines quality
- **Date:** 2026-03-05
- **Decision:** The skills-to-verb coupling model is: everyone sees the same verbs (mostly). Skills determine how well you execute — bad at social means you can still talk, just badly. Some advanced verbs may still be gated by skill level, but the default is outcome-based, not access-based. This is the simplest learnable model: try anything, skill determines result.
- **Rationale:** Verb access gating (skill gates whether you can even attempt an action) creates invisible walls and punishes players for trying. Outcome-based (skill determines quality of result) lets players learn by doing and creates organic differentiation. A tycoon with low social can still negotiate — they just negotiate poorly, which produces interesting consequences.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 5
- **Raised by:** Team Leader (Jeroen) — outcome model (option C)
- **Dissent:** None
- **Cross-reference:** [D-120](scope.md#d-120-no-skill-ceiling-in-v02--transhumanist-ladder-deferred) (no skill ceiling in v0.2)
### D-134: Full character customization — hair, clothing, colors at tile scale
- **Date:** 2026-03-05
- **Decision:** Full character appearance customization is in scope: hair, clothing, colors. Readability at top-down tile scale is solved through outline and highlight mechanics, not by limiting customization options. The character creation screen is an emotional investment moment — the player should feel this is their character.
- **Rationale:** Customization at this scale was assumed to be a readability risk. The workshop decision: solve the readability problem rather than limit the player. Readability via outline/highlight is a solved problem in the tile rendering pipeline. Limiting customization would undermine the identity investment that makes life-sim attachment possible.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 11
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-135: Setting delivery via both layers — visual world + insert in parallel
- **Date:** 2026-03-05
- **Decision:** Setting is delivered through two parallel layers: (1) the physical world — visuals and NPC behavior show context, atmosphere, place; (2) the neural insert — names, contextualizes, provides information the character would know from their background. Araminta (visual layer) and Mellanie (insert copy layer) work in parallel. Both layers are required from day one of the tycoon bookmark experience.
- **Rationale:** Either layer alone is insufficient. Visuals without naming leave the player in a beautiful void with no cultural foothold. Naming without visuals produces an exposition dump. Both together produce the "this is a place" sensation the workshop identified as the missing ingredient of v0.1.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 12
- **Raised by:** Team Leader (Jeroen) — both layered (option C)
- **Dissent:** None
- **Cross-reference:** [D-128](content.md#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture) (culture as context for insert copy)
### D-136: First Settled Reach moment — auto-generated apartment + insert activation
- **Date:** 2026-03-05
- **Decision:** The first moment of The Settled Reach is two layered beats: (1) Waking up in YOUR auto-generated apartment (reflects your economic position from the tycoon bookmark; wealthy, modest, or constrained start matters). (2) Insert activation — the neural implant powering on is intimate, personal, tech-specific. The alarm clock is the Groundhog Day homage ([D-126](content.md#d-126-groundhog-day-alarm-clock-homage--first-game-day-only)). The apartment reflects the character's economic position — auto-generated, not hand-built.
- **Rationale:** The apartment establishes place, economic status, and self without exposition. Insert activation establishes the neural lattice as intimate and personal — this is your character's relationship with their technology. Both beats together create the "this is MY character in MY world" moment that v0.1 lacked.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 13
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-126](content.md#d-126-groundhog-day-alarm-clock-homage--first-game-day-only) (alarm clock tone), [D-135](#d-135-setting-delivery-via-both-layers--visual-world--insert-in-parallel) (both layers active from first moment)
### D-137: Generator produces both structural and cosmetic variety at different scales
- **Date:** 2026-03-05
- **Decision:** The generator must produce two types of variety simultaneously at different scales: (1) Structural variety — operates at seed level: different playthroughs have genuinely different world structures (economic landscape, faction power balance, crisis composition, NPC role distribution). (2) Cosmetic variety — operates within a structure: NPC names, faces, apartment layouts vary per instance. Structural variety is the higher-priority proof for the Sprint 25 spike ([D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path)).
- **Rationale:** Cosmetic variety without structural variety produces "same game with different wallpaper." Structural variety without cosmetic variety produces identical-looking characters with different internal states. Both are load-bearing for the life-sim experience — structural variety drives replay value, cosmetic variety drives in-session believability.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 23
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-114](scope.md#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice) (generator proof-of-life), [D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path) (Sprint 25 generator spike)
---
*38 decisions. Last updated: 2026-03-05 (D-133D-137 added — Where's the Fun? Workshop)*
+117 -2
View File
@@ -10,6 +10,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Rationale:** A galaxy-spanning game needs content architecture that scales without hand-crafting everything. The life-sim substrate creates attachment that gives conspiracies emotional weight. Pool-based Tier 1 modules enable replayability and DLC expansion.
- **Raised by:** Team Leader (Jeroen), with full team endorsement across 3 rounds
- **Dissent:** None
- **Amendment (2026-03-05, Where's the Fun? Workshop):** Tier 1 "authored drama modules" concept is deferred for v0.2. [D-114](scope.md#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice) (generator-first proof-of-life) and [D-127](#d-127-player-choices-are-the-content--rimworld-model-job-as-rails) (player choices are the content) establish that v0.2 ships zero authored drama modules. The three-tier architecture remains valid for the full game, but the Tier 1 pool is empty by design in v0.2 — generator-first validates Tier 2 and Tier 3 before Tier 1 modules are authored. Tier 1 will be authored after the generator spike (D-119) proves legible characters and readable relationships.
### D-024: NPC generation model — 10 axes + combat component
- **Date:** 2026-02-10
@@ -17,6 +18,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Rationale:** Axes that create contradictions within NPCs produce player decisions. The 5 key interactions (Want×Secret, Routine×Secret, Tolerance×Relationships, Want×Relationships, Personality×Tolerance) drive the full investigation-and-social gameplay loop. Contentment axis (proposed by Gore) connects generated NPCs to the thematic spine. Combat as component follows the same pattern as perception modes ([D-017](perception.md#d-017-perception-modes-as-character-build-system)).
- **Raised by:** Gestalt (consolidation), Gore (contentment axis), Paula (triangle model), Tyre (combat component). Full team endorsed.
- **Dissent:** None
- **Amendment (2026-03-05, Where's the Fun? Workshop):** The 10-axis model and core generation logic survive. However, [D-122](#d-122-all-npcs-generated--no-named-hand-authored-characters) (all NPCs generated) removes all named hand-authored NPCs from v0.2. Named NPCs and hand-authored triangles are now generator outputs, not authored content. The 10 axes apply to all generated NPCs. The `NpcBlueprint` struct (Tyre prerequisite for D-119 generator spike) must encode these axes as generator output format. Cultural/origin template (previously called "generation-time flavor") is now the primary driver per [D-128](#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture) and [D-121](#d-121-voice-is-culture-driven--job-as-modifier).
### D-025: Social site / functional cluster as atomic template unit
- **Date:** 2026-02-10
@@ -31,6 +33,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Rationale:** All four layers are load-bearing — each enriches the previous ones. Tagged pools avoid combinatorial explosion while trait modifiers produce character variety. The generation pass (write 10, generate 40) scales authored content. Every memorable line needs a human hand; the generation pass fills the background.
- **Raised by:** Mellanie (authoring model), Paula (relational layers), Gestalt (axis-to-pipeline mapping), Tyre (previewer feasibility)
- **Dissent:** None on model. Minor ordering difference: Mellanie front-loads access tiers (structural), Paula front-loads relationship history (narrative). Both sequences work.
- **Amendment (2026-03-05, Where's the Fun? Workshop):** The tagged line pool architecture survives for authored content, but the v0.2 NPC content pipeline pivots to AI-assisted templating. [D-123](#d-123-generative-ai-for-npc-content-templating-via-culture-vectors) (generative AI for NPC content) replaces the hand-authoring model for NPC dialogue pools. The four relational layers remain valid as a selection architecture, but pools will be populated by template assembly (culture vectors + job modifiers + AI generation) rather than hand-authoring. Hand-authored content (anchor lines per D-092, player monologue) remains hand-authored.
### D-029: Population entanglement ratio — 30/50/20
- **Date:** 2026-02-10
@@ -39,9 +42,11 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Rationale:** If every NPC is suspicious, investigation collapses. The mundane triangles ARE the life-sim game — hours of play that never touch conspiracy. Variable entanglement rate defeats metagaming across playthroughs. Quiet life must feel genuinely good, not empty.
- **Raised by:** Gore (thematic), Paula (30/50/20 split), Nigel (anti-metagaming), Team Leader (majority unentangled)
- **Dissent:** None
- **Amendment (2026-03-05, Where's the Fun? Workshop):** The 30/50/20 population split rationale survives but implementation context changes. [D-122](#d-122-all-npcs-generated--no-named-hand-authored-characters) (all NPCs generated) means no NPC is hand-authored. The "entangled 20%" are generated NPCs whose triangles happen to be flagged for intrigue content. For the tycoon v0.2 bookmark ([D-117](scope.md#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content)), the split applies to economic, social, and mundane triangles rather than investigation-intrigue triangles. The specific ratios will be revisited after the generator spike ([D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path)) proves what population density the generator can sustain.
### D-032: Separate monologue pools per character
### D-032: Separate monologue pools per character [SUPERSEDED]
- **Date:** 2026-02-11
- **Superseded by:** [D-117](scope.md#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content) (single tycoon character in v0.2 eliminates the smuggler/detective hard partition). The principle of character-specific monologue pools survives — the tycoon has their own monologue pool. The hard partition between smuggler and detective does not apply when there is only one playable character. Per [D-127](#d-127-player-choices-are-the-content--rimworld-model-job-as-rails), the player's monologue reflects their character background. The partition design is preserved as a pattern for when multiple playable characters are reintroduced.
- **Decision:** Internal monologue content is hard-partitioned by playable character. The smuggler and detective have completely separate monologue pools — no shared lines. The `character` tag on monologue lines is a hard partition, not a filter. File structure uses separate files per character per location (e.g., `monologue-smuggler.yaml`, `monologue-detective.yaml`).
- **Rationale:** Shared monologue would dilute character voice and undermine the dual-lens experience. Each character's internal voice must be independently coherent. Same trigger, different pool — this is how mirror moments work without either pool knowing about the other.
- **Cross-reference:** Dialogue lines remain character-agnostic — the access tier system (D-028 Layer 1) handles per-character filtering without separate pools.
@@ -58,6 +63,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Cross-reference:** NPC triangle model ([D-024](#d-024-npc-generation-model--10-axes--combat-component)), relationship web ([D-029](#d-029-population-entanglement-ratio--305020)), vertical slice criteria ([D-027](scope.md#d-027-vertical-slice--smuggler--detective-two-character-proof))
- **Raised by:** Ozzie (emotional concept, Round 1), Paula (structural design and both FRIEND profiles, Round 2), project lead (confirmed, directive #4). Sera Venn confirmed by project lead over Mellanie's alternative proposal (Lera Sessik).
- **Dissent:** Mellanie proposed Lera Sessik (bar owner) as detective's FRIEND. Project lead selected Paula's Sera Venn design. Lera remains bar owner / mundane triangle member.
- **Amendment (2026-03-05, Where's the Fun? Workshop):** The FRIEND pattern (3+ relationship phases, observable contradiction, sympathetic motivation, no clean resolution, tell progression, dual-lens resonance) survives as a generator template for v0.2. Kael Davan and Sera Venn do not exist — [D-122](#d-122-all-npcs-generated--no-named-hand-authored-characters) eliminates all named hand-authored NPCs. In v0.2, the FRIEND role is filled by a generated NPC whose generator profile matches the FRIEND pattern template. The FRIEND pattern is now a generator instruction set, not an authoring assignment. Workshop convergence note: warmth with generated NPCs is earned through observed relationship progression, not authored backstory — this may produce stronger emotional investment than the hand-authored approach (Paula, Mellanie in Where's the Fun? Workshop §Phase Zero).
### D-035: Converged tag taxonomy for dialogue and monologue line pools
- **Date:** 2026-02-11
@@ -84,6 +90,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Dissent:** None. Minor consolidations: Mellanie's 8 moods mapped to Gestalt's 8 (different names, same concepts). Mellanie's `crime` topic deliberately excluded (NPCs think of it as `cargo` or `money`).
- **Amendment (Sprint 8):** `focused` added as 9th mood (used in Kael dialogue at The Terminal and maintenance corridors). `greeting` added as 14th situation (used in PC dialogue pools for initial contact lines). Schema updated to match.
- **Amendment (Sprint 14):** Mood vocabulary renamed to match voice guide (monologue-voice-guide.md). Old → new: `fond``warm`, `comfortable``content`, `worried``anxious`, `concerned``frustrated`. Dropped: `analytical` (merged into `focused`), `conflicted` (modeled as `suspicious`+`warm` collision). Added: `hostile`. Final 8 moods: `anxious`, `frustrated`, `content`, `suspicious`, `warm`, `hostile`, `relieved`, `focused`. Neutral = untagged.
- **Amendment (Sprint 24):** `triangle_activated` added as 15th situation (fires post-TriangleActivated when player observes anchor NPCs). Two freeform tags registered as conventions: `triangle-signal` (line is part of the triangle activation sequence) and `tell-observation` (line observes a behavioral tell without naming its cause). `npc_in_los` prerequisite added for LOS-gated monologue lines. Schema updated to match.
- **Amendment (Sprint 15):** Line ID namespace changed from location-scoped to NPC-scoped. Old scheme: `{location_slug}_{d|m}_{###}` (e.g., `the-terminal_d_039`) — all NPCs at a location share one ID sequence, requiring cross-file coordination and causing collisions at scale. New scheme: `{npc-slug}_{d|m}_{###}` for dialogue, `{npc-slug}_m_{s|d}_{###}` for monologue (e.g., `kael-davan_d_001`, `dock-worker_d_001`). Each NPC's IDs are independent — no cross-file coordination needed. Auto-generated NPCs use their generated slug. Schema regex patterns unchanged (prefix is still `^[a-z][a-z0-9-]*`), only the `description` field and convention documentation update. Migration: mechanical rename of all existing line IDs across ~20 dialogue files and monologue pools.
### D-036: Sova Transit District / Krenn System as v0.1 setting
@@ -97,6 +104,7 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
- **Cross-reference:** Vertical slice ([D-027](scope.md#d-027-vertical-slice--smuggler--detective-two-character-proof)), contraband ([D-037](#d-037-contraband-specification))
- **Raised by:** Miri (Sova setting brief, Round 1; Krenn System profile, Round 2), project lead (confirmed as worldbuilding milestone, directive #6)
- **Dissent:** None
- **Amendment (2026-03-05, Where's the Fun? Workshop):** Station Sova / Krenn System confirmed as the v0.2 setting. [D-128](#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture) makes Krenn culture the cultural context for the tycoon bookmark — Krenn System IS Krenn culture by default. The setting details (naming conventions, atmosphere, sensory palette) survive as generator inputs and culture profile content. However, the Sova Transit District spatial layout (D-093) was designed for the v0.1 hand-built slice. v0.2 generates the location via the generator ([D-114](scope.md#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice)); the Krenn culture profile (Miri prerequisite for [D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path)) captures the setting identity as generator inputs. Sova remains the canonical example system and the first culture profile to author.
### D-037: Contraband specification — unlicensed lattice components
- **Date:** 2026-02-11
@@ -287,4 +295,111 @@ How narrative, NPCs, and world content are created: content tiers, NPC generatio
---
*25 decisions. Last updated: 2026-02-27 (D-098, D-104, D-105, D-107 added — Generator Architecture Workshop #562)*
### D-121: Voice is culture-driven — job as modifier
- **Date:** 2026-03-05
- **Decision:** NPC voice is authored at the culture level with job-specific modifiers layered on top. Culture is primary — a character IS their background. Job adds a layer. A Krenn tycoon sounds like a Krenn person who runs businesses, not a generic tycoon. This is an inversion of the prior assumption that job drove voice with culture as modifier.
- **Rationale:** Culture-primary voice produces characters that feel like they belong to a place. Job-primary voice produces archetypes. The life-sim vision requires characters legible as inhabitants of the Krenn System, not as representatives of occupational categories.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 6
- **Raised by:** Team Leader (Jeroen) — inversion of Mellanie's prior option C
- **Dissent:** None
- **Cross-reference:** [D-128](#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture) (Krenn culture as starting context)
### D-122: All NPCs generated — no named hand-authored characters
- **Date:** 2026-03-05
- **Decision:** All NPCs in v0.2 are generated. There are no named, hand-authored characters. Kael Davan, Naia, Maret, and Sera Venn do not exist in v0.2. The generator produces NPCs that fit positions based on location characteristics. Limited vocabulary is acceptable at first. The FRIEND pattern (D-034) survives as a generator template, not an authoring assignment.
- **Rationale:** Rimworld and The Sims are capable of generating characters that players form attachments to, even without dialogue or backstory. The generator-first approach (D-114) requires proving this foundation before hand-authored characters are layered on. Named NPCs and fixed triangles were a source of the rigidity that made v0.1 feel like a game level, not a place.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 7
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Supersedes:** Named NPC assignments in [D-034](#d-034-the-friend--production-level-npc-pattern) (Kael/Sera as hand-authored characters — see amendment on D-034)
- **Cross-reference:** [D-123](#d-123-generative-ai-for-npc-content-templating-via-culture-vectors) (AI templating), [D-129](#d-129-npc-personality-traits--behavior-first-relationships-codified-for-systems) (NPC personality model)
### D-123: Generative AI for NPC content templating via culture vectors
- **Date:** 2026-03-05
- **Decision:** NPC content (dialogue pools, voice, vocabulary) is generated using generative AI with culture vectors, tone, and accent prompts as constraints. Culture vectors are the primary prompt constraint — they prevent the AI pipeline from defaulting to genre conventions. The AI pipeline is an authoring tool for content assembly, not a runtime system. Limited vocabulary acceptable at first; AI templating scales content as the generator matures.
- **Rationale:** The copy pool for all-generated NPCs at scale is enormous. Generative AI with culture-vector constraints is the only viable path to populating it without hand-authoring every line. Culture profiles (Miri prerequisite) become the primary authoring deliverable feeding the pipeline.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 8
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-121](#d-121-voice-is-culture-driven--job-as-modifier) (culture-primary voice), [D-128](#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture) (culture profile as generator input)
### D-124: In-game ollama for live NPC dialogue — deferred, door open
- **Date:** 2026-03-05
- **Decision:** Running a dressed-down version of ollama in-game for live NPC dialogue is possible and interesting, but deferred. The door is explicitly left open — this is not a rejected alternative, it is a future investigation item. For v0.2, NPC dialogue uses template-assembled content (D-123). Live in-game AI dialogue is post-proof-of-life.
- **Rationale:** Live AI dialogue requires solving NPC quality floor, performance, and determinism questions that are out of scope for the generator proof-of-life. Deferred until the base generator is proven solid.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 9
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-125: World is quietly responsive — gradient of caring by social proximity
- **Date:** 2026-03-05
- **Decision:** The world does not care globally but notices locally. Primary social contacts (colleagues, neighbors) develop responsiveness over time. The gradient of caring is based on social proximity — the world is neither Kenshi-indifferent nor uniformly caring. The player should never encounter a truly indifferent world; even early builds will have localized responsiveness around primary contacts. Gore's concern about indifference is addressed by design — authored content will layer in before v1.0.
- **Rationale:** True indifference breaks the life-sim emotional loop. Characters can't form attachments to a world that doesn't register their existence. The gradient model (socially close = responsive, globally = neutral) reflects realistic social structure and produces the "quietly alive" feel the workshop converged on.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 10
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-126: Groundhog Day alarm clock homage — first game day only
- **Date:** 2026-03-05
- **Decision:** The first game day begins with an alarm clock that opens with the *click* pa-pa pa-pa opening from Groundhog Day, cut short. This happens on the first day of a new game only. The tone is a wink: "new day, new start, new chances." It sets the life-sim framing without exposition.
- **Rationale:** A single tonal signal at game start establishes the day-cycle framing and communicates the game's tone — forward-moving, possibility-oriented, gently aware of its own conceits — without explicit explanation. First day only; repeating it would undermine the freshness.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 14
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None (legality of the reference to be verified)
- **Cross-reference:** [D-136](architecture.md#d-136-first-settled-reach-moment-auto-generated-apartment--insert-activation) (first game moment design)
### D-127: Player choices are the content — Rimworld model, job as rails
- **Date:** 2026-03-05
- **Decision:** Phase 1 of the game experience is not "an empty world before content arrives." It is "a world full of opportunity where the player's choices ARE the content." Rimworld model: one authored starting beat (the crash / the alarm clock + apartment wakeup), then agency and options. A job is rails to take off from, not a script to follow. The world provides opportunity and consequence; the player provides the story.
- **Rationale:** The prior "no objectives" stance was a design stance, not a design solution. The Rimworld model is a design solution: curated starting beat, then genuine agency. Phase 1 is not empty — it is a full world of potential actions, economic choices, social encounters, and consequences. The player's story is the content.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 15
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-128: Culture implicit in starting location — Krenn System equals Krenn culture
- **Date:** 2026-03-05
- **Decision:** Culture is implicit in the starting bookmark location. The tycoon bookmark in the Krenn System means Krenn culture. The player does not select culture at character creation; it derives from where the bookmark places them. This resolves the culture-everywhere-but-nowhere tension: culture IS in the game from day one, it's just not a character creation slider. The Krenn System provides the cultural context; NPC generation uses regional culture as the primary vector.
- **Rationale:** Seven of nine workshop agents independently flagged the tension between D-115 (culture deferred from creation) and D-121 (culture primary for voice). Implicit culture unblocks five downstream pipelines simultaneously: voice cards (Mellanie), culture profiles (Miri), NpcBlueprint culture field (Tyre), cultural visual grammar (Araminta), systems integration (Gestalt).
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 16
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-121](#d-121-voice-is-culture-driven--job-as-modifier) (culture-primary voice), [D-036](#d-036-sova-transit-district--krenn-system-as-v01-setting) (Krenn System canonical details)
### D-129: NPC personality — traits + behavior first, relationships codified for systems
- **Date:** 2026-03-05
- **Decision:** NPC personality starts with traits and observable behavior. Relationships form through two channels: Sims-style accumulation through repeated interaction, and Rimworld-style bonding through shared adversity (surviving a crisis together, helping each other). The player's subjective feeling is the real metric, but relationships must be codified in the system so that game systems (storyteller, consequences, NPC behavior changes) can reference relationship state.
- **Rationale:** All nine workshop agents identified NPC legibility as the universal gate. Generated NPCs must have sufficient personality surface area for emotional attachment. Without legible NPCs, the life-sim loop cannot fire. Codifying relationships for systems enables the storyteller to use them as triggers and the consequence model (D-132) to escalate through them.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 17 (resolves Q-WTF-034/035)
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-024](#d-024-npc-generation-model--10-axes--combat-component) (NPC axes — see amendment), [D-132](#d-132-dual-scale-consequence-model--rimworld-sharp-events-and-df-slow-accumulation) (consequence model)
### D-130: Fully emergent moral arc for v0.2 — generator proves relationships readable first
- **Date:** 2026-03-05
- **Decision:** v0.2 ships a fully emergent moral arc — no authored arc structure. The tycoon bookmark has no pre-designed story arc. The explicit test before layering narrative depth: can the player tell "this is a relationship my character has" from generator output alone? Full flavor and generated content will be layered in later, but only after the relationship foundation is proven solid. This is a deliberate proof-of-concept sequence: generator proves relationships are readable → then add narrative depth.
- **Rationale:** The full flavor and generated narrative content would get in the way of properly evaluating the generator's strength. The generator must be rock solid and usable before truly interesting threads are pulled. Fully emergent may feel artificial, but it is the right v0.2 test.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 18 (resolves Q-WTF-043)
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-129](#d-129-npc-personality--traits--behavior-first-relationships-codified-for-systems) (relationship legibility as test), [D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path) (generator spike as prerequisite)
### D-131: Broad economic verb vocabulary — life verbs, not tycoon-specific
- **Date:** 2026-03-05
- **Decision:** The verb vocabulary is broad and economic, serving all careers, not tycoon-specific. Life verbs: buy, sell, hire, rent, contract, inspect, negotiate, invest. A detective also uses contracts (hiring informants, renting surveillance equipment). Implementation follows the speed of the interpreting systems — each verb requires its backing system (ownership registration for buy/sell, contract tracking for hire/rent). This is a life-sim verb set, not a job-specific verb set.
- **Rationale:** Tycoon-specific verbs would lock the gameplay loop to one archetype. Broad economic verbs serve the life-sim vision where "detective, smuggler, tycoon are jobs you can have, not the game's identity" (workshop executive summary). The verb map is the mechanical expression of that philosophy.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 19 (resolves Q-WTF-027)
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-132: Dual-scale consequence model — Rimworld sharp events and DF slow accumulation
- **Date:** 2026-03-05
- **Decision:** The consequence model operates at two scales simultaneously. Rimworld-style sharp events (raids, crises, dramatic reversals) AND Dwarf Fortress-style slow accumulation (gradual relationship erosion, creeping debt, reputation shifts). Sharp events create drama; slow accumulation creates texture. Rimworld already manages both — sharp storyteller events on top of slow colony degradation. The Settled Reach follows the same dual-scale model.
- **Rationale:** The dual-scale model produces both moment-to-moment drama and long-term narrative texture. A single-scale model either feels like it has no consequences (all slow) or like consequence happens arbitrarily (all sharp). Both are load-bearing.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 21 (resolves Q-WTF-037)
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-129](#d-129-npc-personality--traits--behavior-first-relationships-codified-for-systems) (relationships as consequence substrate)
---
*37 decisions. Last updated: 2026-03-05 (D-121D-132 added; D-023, D-024, D-028, D-029, D-032, D-034, D-036 amended; D-032 superseded — Where's the Fun? Workshop)*
+3 -2
View File
@@ -48,7 +48,8 @@ Narrative, NPCs, dialogue, templates, setting, worldbuilding, and storyteller me
- **Source:** Wiki Review Workshop R2
### Q-033: Three-system NPC architecture
- **Status:** Open
- **Status:** Partially resolved — reframed by [D-122](content.md#d-122-all-npcs-generated--no-named-hand-authored-characters) (all NPCs generated)
- **Reframe:** The 9-pattern x 6-motivation composition matrix may survive as a generator template taxonomy (the FRIEND pattern explicitly survives as a generator template per D-034 amendment). However, the question of whether it supersedes or extends D-024 is now secondary — both describe generator output format, not hand-authoring assignments. The NpcBlueprint struct (Tyre, Sprint 25 prerequisite) will determine how patterns and motivations are encoded. Full formal adoption of the 9x6 matrix remains open.
- **Question:** Should NPCs be formally composed from 9 thematic patterns (FRIEND, MIRROR, ANCHOR, GHOST, CATALYST, THRESHOLD, REMNANT, SYSTEM, NOBODY) x 6 functional motivations (HANDLER, WITNESS, TURNCOAT, CIVILIAN, OPERATOR, SKEPTIC)? D-024 defines 10 axes + combat but predates this refined system. The wiki-review workshop produced a full composition matrix with drama ratings and forbidden combinations. Does this supersede D-024 or extend it?
- **Assigned to:** Gestalt, Paula
- **Source:** Wiki Review Workshop R4
@@ -175,4 +176,4 @@ Narrative, NPCs, dialogue, templates, setting, worldbuilding, and storyteller me
---
*19 questions (5 resolved, 1 partially resolved, 13 open). Last updated: 2026-02-28.*
*19 questions (5 resolved, 2 partially resolved, 12 open). Last updated: 2026-03-05 (Q-033 partially resolved/reframed — Where's the Fun? Workshop)*
+9 -5
View File
@@ -10,7 +10,7 @@ Game concept, prototype boundaries, production pipeline, and feature decisions.
### Q-004: One campaign spanning all eras or separate era scenarios?
- **Status:** Not yet discussed
- **Context:** Gore raised that Commonwealth Era and Void Era play very differently. Prototype focuses on pre-Starflyer War era.
- **Context:** Gore raised that different historical eras of the Settled Reach play very differently. Prototype focuses on a single era.
- **Assigned to:** Gore, Miri to lead discussion
### Q-005: Scale for prototype - locations, characters, factions
@@ -29,7 +29,9 @@ Game concept, prototype boundaries, production pipeline, and feature decisions.
- **Assigned to:** Team Leader
### Q-011: Character selection and playable characters
- **Status:** Not yet discussed
- **Status:** Resolved → [D-117](scope.md#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content), [D-115](scope.md#d-115-character-creation-scoped-to-skills--bookmark-for-v02), [D-122](content.md#d-122-all-npcs-generated--no-named-hand-authored-characters)
- **Resolution:** v0.2 has one playable character type: the tycoon (small business owner starting state, D-118). One bookmark. All NPCs are generated — no canon named characters. Character creation is skills + bookmark only. The "how different are their starting positions?" question is answered by the small business owner economic variation (D-118: bar, logistics contract, storage franchise as starting configurations). The "canon characters vs original" question is answered by D-122: all NPCs generated, no canon characters exist in v0.2.
- **Date resolved:** 2026-03-05 (Where's the Fun? Workshop)
- **Question:** Which characters are playable in the prototype? How different are their starting positions? Can you play canon characters or only original ones?
- **Assigned to:** Miri, Paula
@@ -51,7 +53,8 @@ Game concept, prototype boundaries, production pipeline, and feature decisions.
- **Source:** Wiki Review Workshop R4, lead interview
### Q-034: PC archetypes
- **Status:** Open
- **Status:** Partially resolved → [D-117](scope.md#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content) (v0.2 scope only: tycoon bookmark, zero investigation)
- **Partial resolution:** v0.2 scope is settled — one bookmark (tycoon, small business owner start per D-118). Smuggler and detective are abandoned for v0.2. The full 8-archetype model, fluid archetype transitions, and "vulnerable window" mechanics remain undesigned for the full game. The "detective, smuggler, tycoon are jobs you can have, not the game's identity" framing (Where's the Fun? Workshop) is the guiding principle for future archetype design.
- **Question:** Should the full game support 8 fluid PC archetypes (Smuggler, Detective, Engineer, Diplomat, Medic, Scholar, Soldier, Merchant) with transition mechanics where archetype shifts during play based on player behavior? The lead approved 8 archetypes with fluid transitions as a game mechanic. v0.1 ships smuggler + detective only (D-027). Full archetype spec, transition triggers, and "vulnerable window" mechanics are undesigned. NOTE: The character-creation-game-setup workshop (Q-011) will address this — coordinate.
- **Assigned to:** Nigel, Gestalt
- **Source:** Wiki Review Workshop R4, lead interview
@@ -69,7 +72,8 @@ Game concept, prototype boundaries, production pipeline, and feature decisions.
- **Source:** Wiki Review Workshop R4
### Q-037: Generator development pipeline
- **Status:** Open
- **Status:** Partially resolved → [D-119](scope.md#d-119-generator-spike-confirmed-for-sprint-25--critical-path) (Sprint 25 generator spike confirmed as first step)
- **Partial resolution:** The first phase is confirmed — Sprint 25 generator spike. The 6-phase pipeline spec (Ingredient Authoring, Template Authoring, Generator Development, Validation Development, Generation + Review, Hand-Elevation) remains unformally adopted. Generator-first approach (D-114) and the confirmed Sprint 25 spike (D-119) define the immediate critical path. Full pipeline spec remains open pending post-spike assessment.
- **Question:** Should content production follow a 6-phase generator pipeline (Ingredient Authoring, Template Authoring, Generator Development, Validation Development, Generation + Review, Hand-Elevation)? The wiki-review workshop proposed this as the production model for 300 worlds. SI mapped a release path (v0.1 hand-authored, v0.2-0.5 template expansion, v0.6-0.10 generator development, pre-v1.0 validation). Needs scope assessment and sprint planning integration.
- **Assigned to:** SI, Tyre
- **Source:** Wiki Review Workshop R4
@@ -88,4 +92,4 @@ Game concept, prototype boundaries, production pipeline, and feature decisions.
---
*14 questions (0 resolved, 1 partially resolved, 13 open). Last updated: 2026-02-28.*
*14 questions (1 resolved, 3 partially resolved, 10 open). Last updated: 2026-03-05 (Q-011 resolved, Q-034 and Q-037 partially resolved — Where's the Fun? Workshop)*
+5 -3
View File
@@ -17,9 +17,11 @@ Tracked questions awaiting discussion or resolution. Split by domain, mirroring
|--------|-------|----------|---------|------|
| Architecture | 12 | 6 | 1 | 5 |
| Perception | 9 | 5 | 1 | 3 |
| Content | 19 | 5 | 1 | 13 |
| Scope | 14 | 0 | 1 | 13 |
| **Total** | **54** | **16** | **4** | **34** |
| Content | 19 | 5 | 2 | 12 |
| Scope | 14 | 1 | 3 | 10 |
| **Total** | **54** | **17** | **7** | **30** |
*Updated 2026-03-05: Q-011 resolved (D-117/D-115/D-122), Q-034 partially resolved (D-117), Q-037 partially resolved (D-119), Q-033 partially resolved/reframed (D-122) — Where's the Fun? Workshop*
## Adding a Question
+1 -1
View File
@@ -6,7 +6,7 @@ Alternatives considered and rejected, with rationale preserved for future refere
### R-001: Stellaris mod
- **Rejected:** 2026-02-08
- **Reason:** Character system too shallow, multi-empire assumption conflicts with Commonwealth's single-civilization focus, wormhole-as-infrastructure not achievable within Stellaris modding. Team Leader's experience with Star Trek: New Horizons confirmed that even well-suited IPs struggle with character connection in Stellaris.
- **Reason:** Character system too shallow, multi-empire assumption conflicts with the Settled Reach's single-civilization focus, wormhole-as-infrastructure not achievable within Stellaris modding. Team Leader's experience with Star Trek: New Horizons confirmed that even well-suited IPs struggle with character connection in Stellaris.
### R-002: CK3 total conversion
- **Rejected:** 2026-02-08
+83 -14
View File
@@ -7,21 +7,21 @@ What we're building: game concept, design pillars, prototype definition, map spe
### D-001: Build a custom game, not a mod
- **Date:** 2026-02-08
- **Decision:** We are building a standalone game, not a Stellaris mod.
- **Rationale:** No existing game provides the right combination of character-driven dynasty play, wormhole-centric space map, and deep internal politics that the Commonwealth universe demands. Stellaris gets the map right but characters wrong. CK3 gets characters right but the map wrong. Neither supports wormhole-as-infrastructure as a core mechanic.
- **Rationale:** No existing game provides the right combination of character-driven dynasty play, wormhole-centric space map, and deep internal politics that the Settled Reach universe demands. Stellaris gets the map right but characters wrong. CK3 gets characters right but the map wrong. Neither supports wormhole-as-infrastructure as a core mechanic.
- **Raised by:** Team Leader (Jeroen), after team debate across Rounds 1-3.
- **Dissent:** None. Team unanimously supports after discussion.
### D-003: Commonwealth is the first campaign, not the only possible one
### D-003: The Settled Reach is the first campaign, not the only possible one
- **Date:** 2026-02-08
- **Decision:** Build a character-driven space grand strategy *framework/engine*, with the Commonwealth universe as the first campaign/scenario.
- **Rationale:** Avoids locking into one IP. The framework has broader value. The Commonwealth provides a rich, opinionated first use case that forces good design decisions.
- **Decision:** Build a character-driven space grand strategy *framework/engine*, with the Settled Reach as the first campaign/scenario.
- **Rationale:** Avoids locking into one IP. The framework has broader value. The Settled Reach provides a rich, opinionated first use case that forces good design decisions.
- **Raised by:** Gore (Transhumanist Fan), endorsed by team.
### D-005: Core concept - single character, first-person, story-generator
- **Date:** 2026-02-08
- **Decision:** The game is a first-person, single-character experience. You select a character at game start and play from their perspective. The world is a rich simulation experienced through one person's keyhole view.
- **Supersedes:** D-002 (dynasty grand strategy concept)
- **Elevator pitch:** "Pick a character. Step into the Commonwealth. Figure it out."
- **Elevator pitch:** "Pick a character. Step into the Settled Reach. Figure it out."
- **Key pillars:**
- **Single character selection** - who you pick determines where you start, what you know, what you can do, and what you care about
- **Asymmetric information as core mechanic** - you only know what your character knows. Others lie, withhold, reveal. The same world-state looks completely different from different characters
@@ -59,7 +59,7 @@ What we're building: game concept, design pillars, prototype definition, map spe
### D-013: Diegetic insert/POI navigation system
- **Date:** 2026-02-09
- **Decision:** The player's map interface is diegetic - it IS the character's neural insert (Commonwealth technology). Not a game UI bolted on, but the character literally checking their implant's navigation overlay. Points of interest appear on the map only when learned through gameplay.
- **Decision:** The player's map interface is diegetic - it IS the character's neural insert (Settled Reach technology). Not a game UI bolted on, but the character literally checking their implant's navigation overlay. Points of interest appear on the map only when learned through gameplay.
- **How POIs are learned:**
- Character background (starting knowledge based on who you are)
- NPC interactions (contacts send locations, tips, "meet me here" pins)
@@ -71,8 +71,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Cross-reference:** Perception mode overlay in [D-017](perception.md#d-017-perception-modes-as-character-build-system). Time display on insert in [D-031](architecture.md#d-031-time-system--game-clock-and-day-phases).
- **Raised by:** Team Leader (Jeroen) proposed borderless + anchoring concept. Miri confirmed canon basis. Full team contributed mechanics.
### D-014: v0.1 map specification
### D-014: v0.1 map specification [SUPERSEDED]
- **Date:** 2026-02-09
- **Superseded by:** [D-114](#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice) (generator-first proof-of-life replaces hand-built map spec; auto-generated locations at scale replace the hand-crafted tile map approach)
- **Decision:** First playable tech demo map spec:
| Layer | Spec |
@@ -93,8 +94,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Raised by:** Full team across Rounds 8-10.
### D-027: Vertical slice — smuggler + detective, two-character proof
### D-027: Vertical slice — smuggler + detective, two-character proof [SUPERSEDED]
- **Date:** 2026-02-10
- **Superseded by:** [D-114](#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice) (generator-first proof-of-life) and [D-117](#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content) (tycoon bookmark replaces smuggler + detective; zero investigation content for v0.2)
- **Decision:** The proof-of-concept vertical slice is one station district containing: 1 workplace social site, 1 social venue (bar), 1 smuggling ring template, shared NPCs. Two playable characters: smuggler (logistics worker, insider access to criminal templates, social camouflage) and detective (institutional investigator, authority access, analytical). Success criteria: (1) 30 minutes of daily-life breathing room before contamination activates, (2) both playthroughs feel like fundamentally different games, (3) after each playthrough player names an NPC they felt conflicted about, (4) the observe→notice→follow→discover sequence emerges from systems not scripts.
- **Supersedes:** [D-006](#d-006-prototype-scenario--institutearmstrongguardians-superseded)
- **Rationale:** Smuggler + detective creates adversarial divergence — the detective's target IS the smuggler's daily life. Same templates, same NPCs, inverted relationships. Proves character-as-lens, contamination, life-sim attachment, and replayability simultaneously. Tyre confirms: ~20% more effort than single-character, no new architecture.
@@ -121,8 +123,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Raised by:** Ozzie (Round 1 minimum viable proposal, Round 2 full spec), project lead (confirmed, directives #3 and #9). Amendment raised by Inigo (hybrid approach), endorsed by Tyre.
- **Dissent:** Mellanie and Araminta both proposed deferring audio; project lead overruled. Visual sound indicators remain complementary to audio (not replacement).
### D-039: v0.1 wow moment scope — all 6 moments
### D-039: v0.1 wow moment scope — all 6 moments [SUPERSEDED]
- **Date:** 2026-02-11
- **Superseded by:** [D-127](content.md#d-127-player-choices-are-the-content--rimworld-model-job-as-rails) (emergent life-sim replaces detective-specific authored wow moments) and [D-136](architecture.md#d-136-first-settled-reach-moment-auto-generated-apartment--insert-activation) (new first moment: apartment + insert activation). The 6 wow moments were designed for the detective/smuggler frame. No detective-specific wow moments in v0.2.
- **Decision:** All 6 wow moments identified by Ozzie are in v0.1 scope. The original 4 "essential" moments are promoted to must-have. The 2 "nice-to-have" moments are also promoted to must-have (project lead directive).
- **The 6 wow moments (chronological in a 30-minute session):**
1. **Arrival** (minute 0-1): Station hum playing, NPCs already moving, first monologue chime. "Where am I? This feels real." Content: opening monologue, station ambient, pre-populated routines.
@@ -163,8 +166,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Raised by:** Lead (stance toggle, final call), Gestalt (Walk/Sprint/Careful triad + perception coupling), Dudley (MovementProfile + tick values), Ozzie (perception gradient), Nigel (character-defining speed)
- **Dissent:** None after lead call.
### D-065: Smuggler inventory — knowledge-primary with physical evidence
### D-065: Smuggler inventory — knowledge-primary with physical evidence [SUPERSEDED]
- **Date:** 2026-02-13
- **Superseded by:** [D-117](#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content) (no smuggler character in v0.2). The knowledge-primary inventory concept and physical evidence design survive as patterns for future character implementation.
- **Decision:** Knowledge is the primary "inventory" for all characters (you SAW the manifest, not you HAVE it). The smuggler additionally gets a minimal physical inventory for v0.1: 3 specific items (manifest copy, corridor access token, personal comm log). Capacity per archetype: smuggler 3-4 slots, detective 2 slots. Carried items are PRIVATE — they exist behind the information boundary ([D-010](architecture.md#d-010-multiplayer-ready-architectural-baseline) principle 2) and are not visible to other entities unless revealed via search, scan, or confrontation. Server implementation: world entities with CarriedBy component. Verbs: Take, Place.
- **Evidence presentation differs by archetype:** Detective sees case-file-style entries (structured: what/where/when/source/confidence, insert suggests links). Smuggler sees personal notebook (organized by person, informal voice, no contradiction flags). Same underlying knowledge graph, different presentation layer.
- **v0.1 items (Paula):**
@@ -178,8 +182,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Raised by:** Lead (smuggler needs inventory), Paula (three items + presentation split), Gestalt (knowledge-primary framework), Tyre (minimal implementation: SmallVec<3>), Dudley (server model: BTreeMap + info boundary)
- **Dissent:** Tyre initially argued zero physical items in v0.1 (saves 3-4 sprints). Adapted with minimal implementation after lead directive.
### D-087: v0.1 triangle configuration — 3 active forks, 2 passive tensions
### D-087: v0.1 triangle configuration — 3 active forks, 2 passive tensions [SUPERSEDED]
- **Date:** 2026-02-12
- **Superseded by:** [D-122](content.md#d-122-all-npcs-generated--no-named-hand-authored-characters) (all NPCs generated; no named triangles with hand-authored characters) and [D-117](#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content) (no investigation-specific triangle configuration for v0.2). Triangle generation follows the generator-first model (D-114).
- **Decision:** v0.1 vertical slice uses 5 relationship triangles. Three are active forks (T1: Kael-Smuggler-Ring, T2: Sera-Detective-Commission, T4: Drin-System-Ring) with branching outcomes driven by player observation. Two are passive tensions (T3: Naia-Kael-Hael, T5: Worried Partner background) that provide atmosphere and secondary discovery paths. Active forks require authored content per branch. Passive tensions are system-driven.
- **Rationale:** Three active forks are within v0.1 content authoring capacity. Passive tensions require no branching content — they enrich discovery space without multiplying authored lines.
- **Raised by:** Gestalt, Paula
@@ -187,8 +192,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Source:** v0.1 Content Scoping Workshop, Round 2 synthesis
- **Cross-reference:** D-027 (vertical slice), D-034 (THE FRIEND pattern)
### D-089: Self-contained triangle forks for v0.1, no cross-triangle cascade
### D-089: Self-contained triangle forks for v0.1, no cross-triangle cascade [SUPERSEDED]
- **Date:** 2026-02-12
- **Superseded by:** [D-117](#d-117-tycoon-is-the-v02-bookmark--zero-investigation-content) and [D-122](content.md#d-122-all-npcs-generated--no-named-hand-authored-characters). No hand-authored triangle forks in v0.2; triangle generation follows the generator-first model. Cross-triangle cascade design is preserved as a future consideration once the generator proves relationships are readable.
- **Decision:** In v0.1, each triangle fork resolves independently. No triangle outcome triggers escalation in another triangle. Cross-triangle cascade (storyteller-managed, where resolving T1 affects T2 pressure) is deferred to v0.2+. This keeps v0.1 content authoring manageable — each triangle is a self-contained narrative unit.
- **Rationale:** Cross-triangle cascade requires the storyteller to track inter-triangle state and authors to write contingent branches. Both are out of scope for v0.1. Self-contained triangles can be authored, tested, and validated independently.
- **Raised by:** Paula, Gestalt
@@ -196,8 +202,9 @@ What we're building: game concept, design pillars, prototype definition, map spe
- **Source:** v0.1 Content Scoping Workshop, Round 2 synthesis
- **Cross-reference:** D-087 (triangle configuration), D-027 (vertical slice)
### D-091: Complicity as named thematic core
### D-091: Complicity as named thematic core [SUPERSEDED]
- **Date:** 2026-02-12
- **Superseded by:** [D-132](content.md#d-132-dual-scale-consequence-model--rimworld-sharp-events-and-df-slow-accumulation) (consequence replaces complicity as the primary experiential frame — Gore's reframe, Where's the Fun? Workshop convergence). The detective/smuggler frame that gave "complicity" its specific meaning has been replaced by the life-sim frame (D-117). All careers produce consequence at dual scales; complicity was archetype-specific to the detective/smuggler lens.
- **Decision:** The game's thematic identity is complicity — not conspiracy, not detection, not information asymmetry (which is the mechanical core per D-007). The player becomes complicit through observation: seeing something means choosing whether to act on it. The smuggler is complicit in the ring's operations. The detective is complicit in the institution's blindness. Both discover they are already entangled before they choose to be. This framing governs narrative design, wow moment emotional targets (D-039), and the Divergence Reveal (D-027 criterion 4).
- **Rationale:** "Complicity" names the emotional experience that information asymmetry produces. It distinguishes this game from pure detective games (you uncover truth) and pure action games (you do things). Here: you watch, and the watching implicates you.
- **Raised by:** Gore
@@ -207,4 +214,66 @@ What we're building: game concept, design pillars, prototype definition, map spe
---
*17 decisions (15 active, 2 superseded). Last updated: 2026-02-12 (D-087, D-089, D-091 added — retroactive filings from v0.1 Content Scoping Workshop and Wiki Review Workshop)*
### D-114: v0.2 proof-of-life — generator + graphics, not hand-built slice
- **Date:** 2026-03-05
- **Decision:** The v0.2 proof-of-life milestone is defined as: the generator producing usable output (auto-generated locations at scale with legible characters) plus better graphics. A hand-built vertical slice is explicitly NOT the proof-of-life. The v0.1 lesson: descoping toward a hand-built approach produced the wrong game. v0.2 must first prove the foundational generator can produce usable output, then build the game on top of that foundation.
- **Rationale:** v0.1 was built as a detective puzzle game with hand-placed NPCs and dots for characters. The designer's vision is a single-character life sim. The generator-first approach prevents the same mistake — we prove the generative foundation works before committing to content on top of it.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 1
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Supersedes:** [D-027](#d-027-vertical-slice--smuggler--detective-two-character-proof-superseded) (hand-built vertical slice), [D-014](#d-014-v01-map-specification-superseded) (hand-built map spec)
### D-115: Character creation scoped to skills + bookmark for v0.2
- **Date:** 2026-03-05
- **Decision:** v0.2 character creation is limited to two elements: skills (what the character is good at) and bookmark (which starting scenario/location the character inhabits). Family, culture, and religion are deferred from character creation. Culture is available in the game through the starting location (see [D-128](content.md#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture)), not as a creation slider.
- **Rationale:** Skills and bookmark are the minimum needed to differentiate playthroughs. Adding family/culture/religion at creation gates content that is better delivered through gameplay. Religion in particular is NOT a game system (D-116).
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 2
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Cross-reference:** [D-128](content.md#d-128-culture-implicit-in-starting-location--krenn-system-equals-krenn-culture) (culture implicit in location)
### D-116: Religion is not a game system
- **Date:** 2026-03-05
- **Decision:** Religion is not a game system in The Settled Reach. It was mentioned as a reference point for the cultural richness of CK3, not as a design requirement. Religion is not a character creation axis, not a faction mechanic, not a dialogue filter, and not a storyline driver.
- **Rationale:** The reference to religion in workshop discussions came from CK3 influence. The Settled Reach's mechanical identity is economic + social + information asymmetry, not religious politics. Excluding religion from game systems focuses design on the core mechanics.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 3
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-117: Tycoon is the v0.2 bookmark — zero investigation content
- **Date:** 2026-03-05
- **Decision:** The v0.2 bookmark is the tycoon — a small business owner in the Krenn System. v0.2 ships zero investigation content. The detective and smuggler framing from v0.1 is explicitly abandoned for v0.2. The tycoon naturally blends career models: active management, remote investment via insert (WFH model), and one-off deals (gig model). Investigation content will be revisited when the life-sim foundation is proven stable.
- **Rationale:** v0.1's detective/smuggler frame produced the wrong game. The tycoon bookmark is thematically and mechanically richer: economic complicity, life-sim attachment loops, and narrative emergence from everyday decisions. Clean break from investigation content removes the frame that distorted v0.1.
- **Source:** Where's the Fun? Workshop, Round 4 Interview, Decision 4
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
- **Supersedes:** [D-027](#d-027-vertical-slice--smuggler--detective-two-character-proof-superseded)
### D-118: Small business owner starting state — tycoon is aspiration, not starting position
- **Date:** 2026-03-05
- **Decision:** The tycoon bookmark begins as an existing small business owner, not a mogul. The player starts with a small operation (bar, logistics contract, storage franchise) and grows into a tycoon over time — or sells out and pivots to exploration. The bookmark name "tycoon" describes the aspiration and growth trajectory, not the starting state. A true tycoon starting position would be overpowered and would skip the interesting growth phase.
- **Rationale:** Economic complicity and life-sim attachment require a character with something to lose and room to grow. Starting as a mogul eliminates the growth arc and removes economic stakes. The small business owner start grounds the player in a human-scale economic reality before scaling up.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 20
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
### D-119: Generator spike confirmed for Sprint 25 — critical path
- **Date:** 2026-03-05
- **Decision:** The Sprint 25 generator spike is the confirmed first deliverable. If the generator cannot produce usable output, nothing else matters. If it can, everything else has a foundation. The generator proof-of-life gates all subsequent v0.2 development. Sprint 25 prerequisites that must exist before or during the spike: zone identity spec (Miri), one culture profile for Krenn System / Station Sova (Miri), and NpcBlueprint struct design (Tyre). Estimated timeline (Tyre): 7 sprints to proof-of-life playtest (generated location + legible characters + tycoon bookmark from creation to Day 3).
- **Rationale:** The v0.1 lesson established that building without a proven generator produces the wrong game. The sprint 25 spike tests whether the generator can produce auto-generated locations at scale with legible characters — the translation risk mitigation before anything else.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 22
- **Raised by:** Tyre (proposal), Team Leader (confirmed)
- **Dissent:** None
- **Cross-reference:** [D-114](#d-114-v02-proof-of-life--generator--graphics-not-hand-built-slice) (generator-first proof-of-life)
### D-120: No skill ceiling in v0.2 — transhumanist ladder deferred
- **Date:** 2026-03-05
- **Decision:** Skills have no hard cap in v0.2. The transhumanist ladder (baseline human → Higher → ANA-connected) is a later design layer. v0.2 proves the life-sim loop without skill constraints. The `skill_ceiling` architectural field is preserved in the implementation but not enforced in gameplay until the base game loop is proven.
- **Rationale:** Skill ceilings add complexity that is not load-bearing for the v0.2 proof-of-life. The life-sim loop must prove itself first. The transhumanist ladder is a rich design space but belongs in a later iteration when the foundational systems are stable.
- **Source:** Where's the Fun? Workshop, Round 5 Interview, Decision 24
- **Raised by:** Team Leader (Jeroen)
- **Dissent:** None
---
*24 decisions (15 active, 9 superseded). Last updated: 2026-03-05 (D-114D-120 added; D-014, D-027, D-039, D-065, D-087, D-089, D-091 superseded — Where's the Fun? Workshop)*
+7 -7
View File
@@ -9,7 +9,7 @@
## Executive Summary
The Godot 4 (renderer/client) + Rust (simulation server) architecture is **viable and well-suited** to Commonwealth's confirmed requirements (D-010 client-server, D-010 deterministic simulation, D-017 observer queries). The path has real friction points but no hard blockers. The primary risk is not technical capability but **integration complexity** and **ecosystem maturity of gdext**.
The Godot 4 (renderer/client) + Rust (simulation server) architecture is **viable and well-suited** to the Settled Reach's confirmed requirements (D-010 client-server, D-010 deterministic simulation, D-017 observer queries). The path has real friction points but no hard blockers. The primary risk is not technical capability but **integration complexity** and **ecosystem maturity of gdext**.
| Area | Rating | Summary |
|------|--------|---------|
@@ -73,7 +73,7 @@ The `godot-rust/gdext` crate provides Rust bindings for Godot 4's GDExtension AP
- **CI integration:** Straightforward. `cargo build --release` then copy artifact to Godot project. Can be a single Makefile/justfile target.
- **No Godot rebuild required.** Godot loads the extension dynamically. You just rebuild the Rust library and restart the Godot editor.
### Verdict for Commonwealth
### Verdict for the Settled Reach
gdext is **the right choice for the Godot-Rust bridge** given that we want Godot as the renderer and Rust as the simulation. The pre-1.0 status is a real cost (budget 1-2 days per quarter for API migration) but not a blocker. The threading model fits our architecture perfectly. The FFI performance is adequate if we use packed arrays for bulk state transfer.
@@ -103,7 +103,7 @@ You do NOT get (and do not need): Bevy's renderer, window management, asset syst
bevy_ecs = "0.15" # or whatever current version is
```
### Feature Assessment for Commonwealth
### Feature Assessment for the Settled Reach
| Feature | Status | Notes |
|---------|--------|-------|
@@ -127,7 +127,7 @@ fn update_sound(query: Query<(&Position, &SoundEmitter)>, mut events: EventWrite
`move_entities` and `decay_fog` access disjoint component sets, so the scheduler runs them in parallel automatically. `update_sound` reads `Position` (shared) so it can run in parallel with `decay_fog` but must wait for `move_entities` to finish writing `Position`. This is all automatic.
For Commonwealth with potentially hundreds of NPCs, parallel perception queries, sound propagation, and AI decision-making, this is significant.
For the Settled Reach with potentially hundreds of NPCs, parallel perception queries, sound propagation, and AI decision-making, this is significant.
### Determinism Concern
@@ -170,7 +170,7 @@ In case bevy_ecs proves problematic, here are the alternatives:
**Recommendation:** `bevy_ecs` is the clear winner. If we need something lighter for prototyping, `hecs` is a good fallback (we'd write our own simple sequential scheduler, which is fine for v0.1 with 15 NPCs). Do NOT use specs or legion for new projects.
### Verdict for Commonwealth
### Verdict for the Settled Reach
bevy_ecs standalone is an excellent fit. It gives us the ECS architecture, automatic parallelism, change detection for observer queries, and cache-friendly memory layout. The determinism requirement is achievable with explicit system ordering.
@@ -373,7 +373,7 @@ Based on community reports and technical blog posts:
---
## 6. Recommendations for Commonwealth
## 6. Recommendations for the Settled Reach
### Immediate Actions (v0.1 prototype)
@@ -476,6 +476,6 @@ Mapping back to confirmed decisions:
---
*This evaluation recommends proceeding with the Godot 4 + Rust (gdext + bevy_ecs) architecture for the Commonwealth prototype. The architecture is sound, the tools are viable, and the risk profile is manageable. The primary investment is in defining clean abstractions early -- particularly the SimBridge trait and ObserverSnapshot format -- so that the inevitable gdext API churn and future multiplayer addition don't require rewrites.*
*This evaluation recommends proceeding with the Godot 4 + Rust (gdext + bevy_ecs) architecture for the Settled Reach prototype. The architecture is sound, the tools are viable, and the risk profile is manageable. The primary investment is in defining clean abstractions early -- particularly the SimBridge trait and ObserverSnapshot format -- so that the inevitable gdext API churn and future multiplayer addition don't require rewrites.*
*-- TYRE, Technical Architect*

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