Compare commits

..
389 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.6 2df7cc2153 chore(meta): release v0.1.9
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 12:02:41 +01:00
jpmschweitzerandClaude Opus 4.6 3ca12e33a3 feat(skills): start-sprint marks actionable tickets in_progress
Step 7 now transitions all unblocked sprint tickets to in_progress
immediately rather than deferring to agents (who forget). Completes
the ticket lifecycle: start-sprint → in_progress, push-pr → review,
sprint stop → done.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:59:06 +01:00
jpmschweitzerandClaude Opus 4.6 5f3514b3c4 feat(skills): push-pr marks referenced tickets as review
Step 7 scans commit messages for #NNN ticket refs and transitions
in_progress tickets to review status. Only in_progress → review;
other statuses are left alone.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:57:39 +01:00
jpmschweitzerandClaude Opus 4.6 9728eb5d1d feat(db): auto-close picked-up tickets on sprint stop
sprint stop now marks in_progress/review tickets as done
automatically — agents often forget to update status after merging.
Backlog/ready tickets are left as carry-over candidates since they
were never started.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:56:46 +01:00
jpmschweitzerandClaude Opus 4.6 a91f042d2d feat(skills): add sprint close + version bump workflow to start-sprint
When invoked on main, /start-sprint now closes the active sprint,
bumps the version to v0.1.N, moves changelog entries under the new
version heading, tags, and pushes. Team branch behavior unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:55:07 +01:00
jpmschweitzerandClaude Opus 4.6 9cd6969db1 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:51:58 +01:00
jpmschweitzerandClaude Opus 4.6 d627c2bc0a chore(client): add missing .uid and .import files from Godot 4.6
Generated by headless --import after audio merge. These are needed
for stable UID references across editor sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:51:41 +01:00
jpmschweitzerandClaude Opus 4.6 843dbce1ba feat(db): add --description flag to ticket create CLI
ticket create previously had no way to set descriptions, causing
agents to work around with raw SQL or mangled titles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:51:33 +01:00
jpmschweitzerandClaude Opus 4.6 4d44863552 fix(client): make game depends on full build, use --import for client
build-client used --quit without --import, which failed to create
.godot/ and register class_name types after a clean. game target
only depended on build-server, skipping client entirely. clean now
preserves the .godot/ directory itself while clearing contents.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:51:27 +01:00
jpmschweitzerandClaude Opus 4.6 61b5ca7178 chore(db): backup after audio merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:21:00 +01:00
jpmschweitzer fdf82a89ff Merge remote-tracking branch 'origin/audio' 2026-02-18 11:20:16 +01:00
jpmschweitzerandClaude Opus 4.6 0adfb15bc9 chore(db): backup after client PR #34 merge
Tickets #495, #496 marked done.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:14:22 +01:00
jpmschweitzer 5a4156c967 Merge remote-tracking branch 'origin/client' 2026-02-18 11:12:42 +01:00
jpmschweitzerandClaude Opus 4.6 dd8718c762 fix(client): address PR #34 review — 8 items from Hoshe and Tyre
- Cast Variant to String via str() before passing to _on_room_change
- Clear _current_room_id on null room transition (fixes re-entry skip)
- Add push_error for failed dir creation and file writes in _save_report
- Fix docstring: tests/gauntlet-stats.json → user://dev/gauntlet-stats.json
- Namespace stats path to user://dev/ to avoid save data collision
- Replace print() with push_warning in _save_report (codebase consistency)
- Downgrade client-only wire guard from push_warning to silent return

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:06:56 +01:00
jpmschweitzerandClaude Opus 4.6 ddf7e482cd chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:02:57 +01:00
jpmschweitzerandClaude Opus 4.6 408f54f686 fix(ci): address PR #33 review — golden restore safety, pre-pr gate
- golden-diff restores committed file even on cargo test failure
- Wire checklist-validate into pre-pr-content gate
- Fix schema description: condition IDs are globally unique, not
  per-file; document room_id prefix naming convention
- Add schema file missing error handling in validate-checklist
- Add scope discriminator field (per_room/cross_room) to schema
- Check pyyaml and jsonschema packages in setup-tooling

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:02:24 +01:00
jpmschweitzerandClaude Opus 4.6 68848377c7 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:01:41 +01:00
jpmschweitzerandClaude Opus 4.6 20427b9d8b feat(audio): revise Sprint 7 UI audio assets for #453
Re-generate monologue chimes (were oversized placeholders) and
fog_recognition (was silent). Fix sample rate headers on remaining
assets (192kHz→44.1kHz). All 8 UI assets now at 44.1kHz stereo,
LUFS -16 normalized, Vorbis q6, durations within D-038 spec.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 11:01:19 +01:00
jpmschweitzerandClaude Opus 4.6 9e027c5e9d chore(meta): update changelog for sprint 9 client work
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:53:42 +01:00
jpmschweitzerandClaude Opus 4.6 a292674342 feat(client): add 24 gauntlet + bug report tests (#495, #496)
Replace stub F12 tests with BugReportDialog integration tests (dialog
exists, activates on action, pause/unpause, wire guard, text render
with entities/monologue/dialogue, empty snapshot edge case). Add 16
GauntletHUD tests (format_time, visibility toggle, timer lifecycle,
room change reset, personal bests record/overwrite/preserve, null room,
timer paused when hidden, finalize, session attempts, snapshot
roundtrip). Anti-tedium assertions now falsifiable against real
GameState.room_id and gauntlet_mode properties.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:53:26 +01:00
jpmschweitzerandClaude Opus 4.6 d7755698b2 feat(client): add WRONG button F12 bug report capture (#495)
F12 pauses simulation, shows modal LineEdit prompt, saves three files
to user://bug-reports/gauntlet-t{tick}-{timestamp}/: snapshot.json
(full ObserverSnapshot), render.txt (simplified client-side text
render), description.txt (tester notes + tick/room/stance metadata).
Esc cancels without saving. Double-activation guard prevents stacking.

BUG_REPORT action added to InputMapper with wire guard in SimBridge
(client-only, never sent to server). Dialog on ModalLayer (CL 30).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:53:15 +01:00
jpmschweitzerandClaude Opus 4.6 bc073c891f feat(client): add gauntlet room timer + personal bests (#496)
GauntletHUD in UILayer shows TIMER: MM:SS (PB: MM:SS). Timer starts on
room entry, resets on room change, records personal bests to
user://gauntlet-stats.json. Session summary printed on disconnect.
Hidden in non-gauntlet mode (anti-tedium guard).

Adds room_id and gauntlet_mode fields to GameState, parsed from
ObserverSnapshot. Main.gd wires update_from_state() and finalize().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:53:05 +01:00
jpmschweitzerandClaude Opus 4.6 c4f78d8926 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:44:50 +01:00
jpmschweitzerandClaude Opus 4.6 d8d005169a feat(ci): add golden file targets and checklist schema (#486, #497)
Golden targets: make golden-diff shows color diff if simulation output
changed, make golden-update regenerates and stages for review.

Checklist schema: JSON Schema for 7 condition types evaluable from
ObserverSnapshot. Per-room YAML checklists for 3 Gauntlet rooms plus
cross-room checks. Validation script + make checklist-validate/generate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:44:33 +01:00
jpmschweitzerandClaude Opus 4.6 318a1e8b07 chore(db): snapshot database after Sprint 9 PR merges
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:24:25 +01:00
jpmschweitzer bb94a6581c Merge remote-tracking branch 'origin/ci' 2026-02-18 10:24:13 +01:00
jpmschweitzer ffe3fdaf3e Merge remote-tracking branch 'origin/client' 2026-02-18 10:23:58 +01:00
jpmschweitzer 445f34f22e Merge remote-tracking branch 'origin/server' 2026-02-18 10:23:33 +01:00
jpmschweitzerandClaude Opus 4.6 48802f6b48 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:13:17 +01:00
jpmschweitzerandClaude Opus 4.6 f1e5d76ad8 chore(skills): clarify verdict rules in review-pr skill
Add explicit mapping: critical/warning → REQUEST_CHANGES,
suggestion-only or clean → APPROVE.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 10:13:06 +01:00
jpmschweitzerandClaude Opus 4.6 4f20a996aa fix(client): address PR #30 review — 9 items from Hoshe and Tyre
- Add SimBridge._last_snapshot = null to P3 before_test() (contamination risk)
- Clarify mono.is_visible is custom property, not CanvasItem builtin
- Camera smoothing test uses approximate equality instead of exact
- Propagate inner _encode_message errors in array/dict encoding
- Add explanatory comment on int_64 encoder branch
- Document unfalsifiable gauntlet guard as intentional future-proof
- Remove loose D-053 citation from LERP_SPEED pin
- Assert FogOverlay is sibling of FogGroup under World
- Add CANVAS_MODAL=30 (ModalLayer) assertion in P3-Z04

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:46:08 +01:00
jpmschweitzerandClaude Opus 4.6 f8db996eb9 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:45:31 +01:00
jpmschweitzerandClaude Opus 4.6 aadfe64cfb chore(meta): update changelog for PR #32 review fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:45:30 +01:00
jpmschweitzerandClaude Opus 4.6 09eb6d7582 refactor(simulation): address PR #32 review — 14 items from Hoshe + Tyre
Hoshe (code quality):
- Remove dead RoomMember component from reset.rs
- Remove execute_reset (dual API trap); plan_reset is sole production path
- .unwrap() → .expect() on reset_plate in setup_gauntlet boot path
- Add 10s read timeout to TCP runtime test (prevents hangs)
- Register player in EntityRegistry in runtime boot test
- Document room_at z-range and corridor overlap assumptions
- Derive entity count from EXPECTED_ENTITY_COUNT constant (was hardcoded 24)
- Add reset plate (49-51) verification to stable_id_ranges_match_spec
- Add debounce exact boundary test (tick 9 rejected, tick 10 accepted)

Tyre (architecture):
- Gate test_world rooms/constants/setup behind "gauntlet" feature (default-on);
  reset module stays always-compiled (production dependency via input system)
- Document setup_gauntlet scheduler bypass for future tracking
- Extract runtime TCP test to content_runtime.rs (separate failure modes)

507 tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:45:17 +01:00
jpmschweitzerandClaude Opus 4.6 3ccac5432b fix(ci): address PR #31 review — harden cross-encoder fixture pipeline
- Fail on encode errors instead of silently writing empty .msgpack files
- Fail test on missing/empty fixture dir instead of silent skip
- Add all missing action variants (MoveSouth, MoveEast, MoveWest,
  Unpause, ToggleStanceDown, WalkAway) to GDScript fixture generator
- Add GDScript fixture staleness check to make pre-pr
- Validate repo root detection before writing outside client/
- Add file.flush() before close in headless mode
- Document fixture failure recovery in DEVOPS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 09:45:13 +01:00
jpmschweitzerandClaude Opus 4.6 e66352e0ea feat(simulation): sprint 9 gauntlet — test infrastructure and first 3 rooms
Add Gauntlet test world with 3 rooms (Inventory Warehouse, Occlusion
Corridor, Pause Chamber) + Central Hub, room constants module, room
reset trigger mechanism, Layer 3 subprocess integration test, golden
file comparison engine and test suite, and content runtime validation.

Tickets: #482, #484, #485, #487, #488, #489, #490

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:25:57 +01:00
jpmschweitzerandClaude Opus 4.6 24289a6ded chore(meta): update changelog for whatsinagame kit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:22:48 +01:00
jpmschweitzerandClaude Opus 4.6 908691bf0b chore(skills): add domain-action naming convention to create-skill
Documents the {domain}-{action} naming pattern for new skills
so future skills follow the convention established in the kit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:22:35 +01:00
jpmschweitzerandClaude Opus 4.6 7158ae9cf5 feat(meta): add whatsinagame multi-agent team starter kit
Claude-native starter kit that bootstraps multi-agent team
infrastructure for any project. Clone once, install as a global
skill, run /kit-install in any project directory.

Includes:
- 3-tier profile system (minimal/standard/full: 3-12 agents)
- 16 agent archetype templates with personality spectrum
- 18 skill templates using domain-action naming convention
- Stakeholder persona panel for workshops and PR reviews
- SQLite ticketing DB with CLI tools (config-based DB paths)
- Decision tracking, sprint lifecycle, workshop orchestration
- Multi-git-host support (GitHub, Gitea, GitLab)
- /kit-update skill for syncing with source repo evolution
- Naming theme support for agent identity/flavor
- Smoke tests for all three profile tiers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:22:30 +01:00
jpmschweitzerandClaude Opus 4.6 da63aa580e feat(ci): add make fixtures-client target for GDScript->Rust cross-encoder validation (#475)
Closes the bidirectional protocol compatibility loop (D-030 Layer 1):
- GDScript fixture generator (20 fixtures: inputs, boundary ticks, batch)
- Rust decoder test verifying all GDScript-encoded fixtures deserialize
- Makefile target with generation + verification in one step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:20:24 +01:00
jpmschweitzerandClaude Opus 4.6 1254fa1b28 chore(meta): update changelog for sprint 9 client work
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:15:05 +01:00
jpmschweitzerandClaude Opus 4.6 4648ce62bb feat(client): add 5 anti-tedium regression tests (#494)
Regression guards for Sprint 9 QA features:
- F12 bug report (2): no-crash without handler, no queued input action
  (stubs for when #495 WRONG button lands)
- Gauntlet UI hidden (3): no gauntlet nodes visible in default mode,
  no room_id/gauntlet_mode in normal snapshots, stays hidden across
  multiple ticks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:14:46 +01:00
jpmschweitzerandClaude Opus 4.6 f82f97afcb feat(client): add 12 P3 tests — z-layer, entity lerp, Tyre additions (#493)
gdUnit4 tests covering:
- Z-layer ordering (4): floor below ysort, entities in ysort at z:0,
  fog exact z_index values (Z_FOG=900, Z_FOG_ENTITIES=950), UI layers
- Entity lerp (3): snap on first appear, moves toward target,
  converges within 0.3s at LERP_SPEED=12.0
- Tyre additions (5): recognition progress, facing rotation, lerp
  weight scaling with delta, blob removal, LERP_SPEED constant guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:14:42 +01:00
jpmschweitzerandClaude Opus 4.6 5820609594 feat(client): add 16 P2 tests — camera, entity alpha/color, UI (#492)
gdUnit4 tests covering:
- Camera (5): zoom default, smoothing convergence, viewport tracking,
  follow movement, no-panning lock per D-014
- Entity alpha/color (4): null visibility default, peripheral/forward
  transition, terrain object color, player color constant
- UI (7): monologue visible/hidden, interaction verbs, inventory grid,
  dialogue overlay, pause indicator, fog blob count, fog z_index

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:14:37 +01:00
jpmschweitzerandClaude Opus 4.6 1435a77dcc fix(client): fix MessagePack int_64 encoder dead code branch (#516)
The int_64 branch condition `-(1 << 63) <= value` overflowed in
GDScript's signed 64-bit arithmetic, making the branch unreachable.
Negative values beyond int_32 range were incorrectly encoded as
uint_64 (0xcf) instead of int_64 (0xd3). Replaced with `value < 0`.

Updated boundary tests BV-N15 and BV-N16 to expect correct int_64
header byte.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 02:14:29 +01:00
jpmschweitzer 58072bc119 Merge remote-tracking branch 'origin/client'
# Conflicts:
#	CHANGELOG.md
2026-02-18 01:57:07 +01:00
jpmschweitzer ca84753182 Merge remote-tracking branch 'origin/server'
# Conflicts:
#	CHANGELOG.md
2026-02-18 01:56:48 +01:00
jpmschweitzerandClaude Opus 4.6 44d6960758 chore(meta): update changelog for protocol v8 sync
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:41:44 +01:00
jpmschweitzerandClaude Opus 4.6 674c7147de feat(client): sync protocol to v8 — dialogue_response + Interact encoding
Bump PROTOCOL_VERSION from 7 to 8 to match server. Three changes:

1. Add dialogue_response field decoding (DialogueResponseEvent with
   line_id, text, speaker_entity_id) from server #305/D-028.

2. Fix Interact encoding: server changed PlayerAction::Interact from
   unit variant to struct variant with {target_entity_id, verb}.
   Extract _encode_action() helper to handle this consistently.

3. Update all test assertions that checked version=7 or expected
   Interact as a bare string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:41:27 +01:00
jpmschweitzerandClaude Opus 4.6 dcb76d1221 fix(client): fix monologue duplication test using wrong poll path
test_monologue_not_duplicated_after_consumption was failing because
poll_snapshot() in test mode returns _test_snapshot() without
consuming _last_snapshot. The carry-forward logic then incorrectly
preserved the monologue. Fix: directly clear _last_snapshot to
simulate the live-mode consumption path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:41:18 +01:00
jpmschweitzerandClaude Opus 4.6 dde68104fc chore(config): add test-client Cargo.lock and gitignore target/
Follows existing tooling crate pattern (content-converter, line-previewer).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:40:35 +01:00
jpmschweitzerandClaude Opus 4.6 932a708cde chore(meta): update changelog for #480 and #481
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:39:56 +01:00
jpmschweitzerandClaude Opus 4.6 d5a1f6dadb refactor(simulation): apply rustfmt formatting
Formatting-only changes across server source and test files.
No logic changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:39:44 +01:00
jpmschweitzerandClaude Opus 4.6 1da81a21ea feat(simulation): add test client binary scaffolding (#480)
Separate workspace crate at tooling/test-client/ importing bridge
types from server crate. CLI: --connect, --replay, --text, --json,
--quiet, --golden, --ticks. Exit codes: 0=success, 1=golden mismatch,
2=connection error. Golden file comparison with recursive JSON diff.
JSONL replay loader for tick-scheduled input sending.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:39:37 +01:00
jpmschweitzerandClaude Opus 4.6 f83815fd9a feat(simulation): add snapshot text renderer library (#481)
format_snapshot_text(&ObserverSnapshot) -> String pub-exported from
server crate. Entity labels as kind:entity_id sorted by distance,
room name stubbed as (unknown) until Gauntlet constants land.
10 unit tests. Consumed by tooling/test-client for --text mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:39:28 +01:00
jpmschweitzerandClaude Opus 4.6 f7712fb4b8 chore(meta): update changelog for Sprint 9 planning
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:32:38 +01:00
jpmschweitzerandClaude Opus 4.6 e3890117d0 chore(db): snapshot database with Sprint 9 ticket assignments
23 tickets assigned to Sprint 9 (Gauntlet) in planning status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:32:31 +01:00
jpmschweitzerandClaude Opus 4.6 85a3e87d36 docs(sprints): add Sprint 9 Gauntlet briefing files
Sprint 9 theme: Gauntlet — test infrastructure backbone with test
client binary, Gauntlet rooms 1-3, golden file suite, and client QA
harness. 23 tickets across server (9), client (6), CI (6), audio (2).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:32:28 +01:00
jpmschweitzerandClaude Opus 4.6 0ed719f8b9 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:26:39 +01:00
jpmschweitzerandClaude Opus 4.6 053380103a feat(audio): add weapon aim lock and stance change audio assets for #440
Delivers final 2/6 interaction UI audio assets, completing ticket #440:
- sfx_weapon_aim_lock.ogg (0.5s, clinical targeting lock tone)
- sfx_stance_change.ogg (0.35s, soft mechanical stance toggle click)

Both insert-tech aesthetic, UI Sounds bus, non-positional. Generated
via Stable Audio Open, trimmed, LUFS-normalized, converted to ogg.
Updated docs/assets/audio/ui.md with UI-007 and UI-008 entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 01:26:27 +01:00
jpmschweitzerandClaude Opus 4.6 4af2197c59 fix(ci): address PR review — filter comments, trim whitespace, warn on missing scripts
Address all review comments from Hoshe and Tyre on PR #27:
- Remove 2>/dev/null from pre-pr-fixtures (critical: swallowed errors)
- Remove dead _file_type function
- Check 4: error on districts with no locations declared
- Check 5: print advisory message when skipping
- Check 8: cross-file line ID uniqueness (not just per-file)
- Check 9: document D-034 asymmetric relationships in docstring
- Document regex fallback rationale in _scan_knowledge
- Add D-035 decision trace to schema descriptions
- Use concrete protocol version in DEVOPS.md example
- Amend D-035 with focused (9th mood) and greeting (14th situation)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 00:23:21 +01:00
jpmschweitzerandClaude Opus 4.6 06f47747c7 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:44:15 +01:00
jpmschweitzerandClaude Opus 4.6 16cccda411 fix(schema): add missing dialogue mood and situation values
Adds 'focused' to mood enum and 'greeting' to situation enum in
dialogue-pool.schema.json. Both values are used by Sprint 7-8
dialogue content but were not added to the schema at the time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:43:55 +01:00
jpmschweitzerandClaude Opus 4.6 65d3b5e50c feat(ci): add content cross-reference validation (9 checks)
Extends tooling/validate-content with Pass 2 cross-reference
validation via ContentIndex class. Nine checks:

1. canonical_id uniqueness (ERROR)
2. relationship target resolution (ERROR)
3. location slug resolution (ERROR)
4. dialogue location resolution (ERROR)
5. fact_id resolution (ERROR, advisory when catalogs empty)
6. triangle membership resolution (ERROR)
7. npc_count accuracy (WARNING)
8. dialogue line_id uniqueness (ERROR)
9. bidirectional relationship consistency (WARNING)

Pass 2 only runs if Pass 1 (schema) passes. Absorbs check-fact-ids
functionality. Spec from hoshe-round3.md Section 2.

Ticket: #464

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:43:47 +01:00
jpmschweitzerandClaude Opus 4.6 f06c8be313 feat(ci): add make pre-pr target and fixture staleness check
Implements make pre-pr chain: lint -> build -> test -> content
validation -> fixture staleness. Branch-specific variants:
pre-pr-server, pre-pr-client, pre-pr-content.

Fixture staleness is a blocker (exit 1) — stale fixtures cause
false positive client tests. Spec from hoshe-round3.md Section 5.

Tickets: #460, #465

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 22:43:40 +01:00
jpmschweitzerandClaude Opus 4.6 c0f6afa082 chore(db): snapshot database after Sprint 8 PR merges
7 client tickets closed (#435-437, #470, #476-478) after
PR #25 and PR #26 reviews completed and merged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:29:14 +01:00
jpmschweitzer a5c30f0101 Merge remote-tracking branch 'origin/server' 2026-02-17 18:21:53 +01:00
jpmschweitzerandClaude Opus 4.6 7af85f126f chore(meta): update changelog for PR #26 review fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:13:07 +01:00
jpmschweitzerandClaude Opus 4.6 12d1fd505e fix(simulation): address PR #26 review comments (13 items)
Warnings fixed:
- Add WalkAway to all_player_action_variants_roundtrip test
- Warn and skip on unresolvable speaker_entity_id (was silent 0)
- Change MonologueState.shown_ids from Vec to HashSet (O(1) lookup)
- Add cross-plugin ordering: trigger_recognition_monologue after
  detect_anomalies (latent determinism bug)
- Add TODO for unreachable Secret trust tier

Suggestions addressed:
- Server-side range check for Talk verb in handle_talk (CLOSE_RANGE)
- Emit IncompleteInteraction before overwriting ActiveDialogue
- Add different_seed_produces_different_replay determinism test
- Replace panic with assert for unknown fixture naming convention
- Fix duplicate "Observe" label: ExamineNpc now uses "Examine NPC"
- Change DialogueCooldownTracker.used from Vec to BTreeMap (D-041)
- Add .after(process_talk_interaction) to process_walk_away ordering
- Collapse dead conditional in main.rs (both branches identical)

468 tests pass, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 18:12:45 +01:00
jpmschweitzerandClaude Opus 4.6 a5ab8238e1 chore(meta): update changelog for Sprint 8 server
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:42:16 +01:00
jpmschweitzerandClaude Opus 4.6 9c60325c2d chore(engine): update and add msgpack fixtures for protocol v8
Update existing snapshot fixtures for protocol v8 (dialogue_response
field). Add 14 raw boundary value fixtures and 5 snapshot boundary
tick fixtures for encoding edge case coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:41:40 +01:00
jpmschweitzerandClaude Opus 4.6 b1fdeabb7c test(simulation): sprint 8 test suite — pause guards, registry, boundary, determinism
Add 50+ tests: pause guard suite (movement, unpause, roundtrip, stance,
interact, batch, tick_rate), EntityRegistry lifecycle (stale mapping,
re-register, unknown unregister), boundary value encode/roundtrip (41
values), encoding asymmetry (GDScript signed→Rust unsigned), malformed
batch rejection, determinism gauntlet (20-tick replay), per-fix
determinism unit tests, and recognition monologue integration tests.
Fix pause guard to block all actions except Pause/Unpause while paused.
Fixes #461-463, #466-469, #471-473, #479.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:41:33 +01:00
jpmschweitzerandClaude Opus 4.6 35f55cfa46 feat(simulation): dialogue pipeline, ContentSlug, and walk-away KG recording
Implement full dialogue selection pipeline (D-028): 4-layer filtering
engine with access tier, situation derivation, trust tier, and weighted
topic+mood scoring via SimRng. Add ContentSlug component for stable
content identity across save/load. Add walk-away KG recording with
IncompleteInteraction events per D-064 three-phase consequences. Bump
protocol to v8 with DialogueResponseEvent. Fixes #305, #427, #452.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:41:23 +01:00
jpmschweitzerandClaude Opus 4.6 695d2ac843 feat(perception): anomaly detection and recognition monologue during delay
Add AnomalyMarker component and detect_anomalies() system that flags
entities with KG relationship PersonOfInterest or Contradicted state
for urgent cognitive delay (0.3s vs 0.6s normal). Add
trigger_recognition_monologue() that fires monologue at delay START
(when grey blob appears), not at completion — the monologue IS the
recognition process per D-060. Includes v0.1 fallback recognition
lines and cooldown tracking. Fixes #450, #451.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:41:14 +01:00
jpmschweitzerandClaude Opus 4.6 ed7498cb24 feat(simulation): add --test-mode, --port, --seed CLI flags
Add CLI argument parsing for test infrastructure: --test-mode enables
deterministic seed (42) and warn-level tracing to stderr, --port allows
OS-assigned ports (--port 0), --seed overrides RNG seed. Prints
LISTENING:{port} to stdout after bind for test harness discovery.
Extracts setup_proof_room() for reuse. Fixes #459.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:41:05 +01:00
jpmschweitzerandClaude Opus 4.6 af8e20ab9a fix(simulation): determinism fixes — BTreeSet ordering, entity sort, mover sort
Replace HashSet with BTreeSet for visible_ids, sort visible_tiles by
coordinates, sort visible entities in snapshot by entity_id, and sort
movers by Entity bits in validate_movement. Required by D-010 principle 4
(deterministic simulation). Fixes #456, #457, #458.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:40:57 +01:00
jpmschweitzerandClaude Opus 4.6 2ab41a0f04 fix(client): address PR #25 review — 5 critical bugs, 4 warnings, 3 suggestions
Critical fixes:
- hide_dialogue() sent PAUSE instead of UNPAUSE, permanently freezing
  simulation after every dialogue (both reviewers)
- Confrontation monologue hardcoded in GDScript constant, violating
  D-042/D-020 — moved to ui-strings.yaml as dialogue.confrontation_beat
- Walk-away WASD didn't call set_input_as_handled(), letting movement
  event propagate and potentially stepping on the same frame
- is_dialogue_active() returned _is_showing only — interaction list
  could flash during 300ms fade gap. Now includes dialogue_active state
- Removed dead _last_dialogue_id / get_dialogue_id() state (never read)

Warnings addressed:
- Audio registry now scans res://audio/ recursively (subdirs registered)
- add_bus_effect guarded against duplicate calls in tests
- int64 encoder dead code tagged KNOWN-DEFECT, filed as ticket #516
- D-073 zone crossfade stub comment clarifies Sprint 9+ deferral
- listening_focus dip documents caller tick-gate responsibility (D-069/D-071)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 17:33:56 +01:00
jpmschweitzerandClaude Opus 4.6 4ccb73d364 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:35:47 +01:00
jpmschweitzerandClaude Opus 4.6 afc433be34 test(client): add boundary, P0 regression, P1 fog/entity, protocol v7 tests
MessagePack boundary tests: 41 values (25 pos + 16 neg) with encode-
only verification, roundtrip, and Rust-style unsigned decode (#470).
P0 regressions: monologue carry-forward (Bug #5), camera stability
during pause (Bug #2) (#477). P1 tests: fog shader state (4), entity
lifecycle (2), pending recognition blob (1) using FogState named
constants (#478). Protocol v7: structured dialogue options with
response_id, priority, confrontation flag, malformed skip (#435).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:35:19 +01:00
jpmschweitzerandClaude Opus 4.6 be763908b2 refactor(client): replace fog byte magic numbers with named constants
Define VIS_HIDDEN/PERIPHERAL/FORWARD and EXP_UNEXPLORED/EXPLORED/
VISIBLE in FogState. Replaces 7 magic number usages in _resize() and
update_from_state(). Provides stable assertion targets for fog tests.
Implements #476.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:35:05 +01:00
jpmschweitzerandClaude Opus 4.6 bfc699c15d feat(ui): implement dialogue pipeline — selection, walk-away, confrontation
Protocol: decode current_dialogue with structured options {text,
response_id, priority, confrontation} and npc_entity_id (#435).
Dialogue box: priority sort, max 3 visible, RichTextLabel for BBCode
italic confrontation options (D-063), 1.5s monologue beat with audio
dip before confrontation send. Walk-away: WASD triggers WalkAway
input, 300ms fade, dialogue_active flag gates movement (D-064).
Implements #435, #437, #436.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:34:55 +01:00
jpmschweitzerandClaude Opus 4.6 9e6b859cdf feat(audio): add AudioManager autoload with 5-bus architecture
D-068 bus layout (Music, Ambient, WorldSFX, PlayerActions, UISounds),
directory-scan asset registry, spatial/non-spatial playback, D-069
audio dip profiles (dialogue, confrontation, listening_focus) with
low-pass filter sweep, and D-073 zone crossfade stub. No-op fallback
when audio assets absent. Implements #255.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 16:34:42 +01:00
jpmschweitzerandClaude Opus 4.6 600cfe506c chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:00:37 +01:00
jpmschweitzerandClaude Opus 4.6 a87c95a6eb docs(workshops): complete QA test architecture workshop
3-round workshop with 7 agents (Tyre, Dudley, Stig, Hoshe,
Justine, Gestalt, Ozzie) plus Qatux documenting. Produced:

- 59-item prioritized test backlog (60 tickets under epic #455)
- Gauntlet test world spec: 7 rooms + hub, 48 entities
- Test client binary spec (tooling/test-client/)
- Determinism fixes (3 patches, ~22 lines)
- Server --test-mode + --port 0 design
- Content cross-reference validation (9 checks)
- make pre-pr pipeline (6-step)
- 38 client tests prioritized
- Anti-tedium features (reset plate, hub teleport, WRONG button)
- Human tester walkthrough
- CI pipeline design (deferred but documented)

Sprint 8 scope: ~17.75 team-days across 26 tickets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:00:15 +01:00
jpmschweitzerandClaude Opus 4.6 7043fd7e4f chore(skills): require file output in workshop skill
Agents were sending outputs as messages instead of writing files,
which prevented Qatux (documenter) from reading other agents' work.
Now each round task instructs agents to write to
docs/workshops/{name}/{agent}-roundN.md, and between-round steps
verify files exist before proceeding.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 13:59:55 +01:00
jpmschweitzerandClaude Opus 4.6 6bf075ed8e chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:27:28 +01:00
jpmschweitzerandClaude Opus 4.6 21edbfec19 docs(workshops): add test architecture workshop brief
4-track workshop targeting the class of bugs from Sprint 6-7: blocking
I/O, serialization boundaries, state desync, off-by-one encoding.
Participants: Tyre, Hoshe, Dudley, Stig. Covers integration test
architecture, serialization boundary tests, client test infrastructure,
and test gap prioritization. Includes catalogue of 6 real bugs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:41 +01:00
jpmschweitzerandClaude Opus 4.6 855dc72b58 docs(scope): update D-053 with client throttle and input model
Document client-side movement throttle rates per stance, hold-to-move
input model with composite diagonals, and crouch ticks-per-move value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:34 +01:00
jpmschweitzerandClaude Opus 4.6 3a02fd0d2a feat(client): add camera anchor test suite
10 gdUnit4 tests verifying camera behavior: SimBridge test mode
connection, snapshot pipeline, player position extraction, camera
anchor in _ready(), smoothing disable/re-enable cycle, camera tracking
across frames, and player movement following.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:28 +01:00
jpmschweitzerandClaude Opus 4.6 8bf7f6e610 fix(client): entity lerp, fog shader safety, type fixes
Entity renderer: add framerate-independent position lerping so entities
slide between tiles instead of snapping. Tuned for Sprint snappiness
and Walk/Careful fluidity.

Fog shader: set ColorRect to transparent fallback so a shader load
failure doesn't paint solid white over the world. Track camera position
(not player position) so fog stays synced during smooth camera pan.
Restructure GLSL to avoid early return (some GPU drivers miscompile it).

Minor type fixes: typed Array[Vector2] in cursor_renderer tick drawing,
untyped Array in inventory_grid to avoid Godot typed-array cast issues.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:23 +01:00
jpmschweitzerandClaude Opus 4.6 bc875d607f fix(client): correct messagepack signed int boundary encoding
Off-by-one in all four signed int branches: upper bound used <= instead
of < (e.g., value <= 128 matched int8 instead of falling through to
uint8). Tick 128 was encoded as int8 -128, causing server deserialization
failure. Also remove stray debug prints from nil/false decode paths.

Fixes bug #4 (MessagePack encodes tick 128 as -128).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:13 +01:00
jpmschweitzerandClaude Opus 4.6 ee3ac75578 fix(client): add pause toggle, hold-to-move, snapshot event carry-forward
Three client input/bridge fixes:

1. Pause toggle: add UNPAUSE action and toggle logic based on
   GameState.game_time.tick_rate. Wire UNPAUSE in sim_bridge.gd.

2. Hold-to-move: replace press-event movement with polled _process()
   direction sampling. Composite diagonals via simultaneous keys
   (W+D → northeast). Client-side throttle per stance (D-053):
   Sprint=200ms, Walk=400ms, Careful=600ms, Crouch=800ms.

3. Snapshot carry-forward: when a newer snapshot overwrites an
   unconsumed one, carry forward current_monologue and current_dialogue
   so one-shot events aren't silently dropped.

Fixes bugs #5 (monologue lost on overwrite) and #6 (overwrite spam).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:26:07 +01:00
jpmschweitzerandClaude Opus 4.6 2dd1e9545d fix(client): anchor camera to player before first frame renders
Camera2D's internal smoothed_camera_pos starts at (0,0) and lerps
toward global_position, causing a visible white-screen drift on startup.
Disable position_smoothing in _ready(), snap camera to player position
from the first snapshot, then re-enable smoothing with reset_smoothing()
after the first anchored frame. Add tick guards to monologue/dialogue
consumption to prevent re-triggering when client FPS > sim tick rate.

Fixes bug #2 (camera doesn't center on player at startup).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:25:58 +01:00
jpmschweitzerandClaude Opus 4.6 2b54697278 fix(simulation): discard movement input while paused
process_player_input processed movement commands regardless of tick
rate, causing the camera to drift from the player when the game was
paused and the player held direction keys. Add PlayerAction::is_movement()
helper and a paused guard that skips movement actions while TickRate is
Paused. Pause/Unpause commands still process normally.

Fixes bug #3 (player moves while game is paused).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:25:51 +01:00
jpmschweitzerandClaude Opus 4.6 d1aba3d554 fix(simulation): use non-blocking TCP to prevent game loop stall
read_framed() called read_exact() which blocked the entire bevy Update
schedule waiting for client input — no systems ran until a keystroke
arrived. Switch TcpStream to non-blocking mode so receive_inputs()
returns Ok(vec![]) on WouldBlock instead of blocking. Toggle to
blocking for snapshot writes (reliable delivery). Add 50ms frame
throttle (~20 ticks/sec) since the non-blocking loop would otherwise
spin. Downgrade input receive logging to trace, add error logging for
missing bridge resource and failed observer queries.

Fixes bug #1 (server never sends snapshots) and #2 (camera doesn't
center until first keystroke).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 23:25:45 +01:00
jpmschweitzerandClaude Opus 4.6 686c89170a chore(db): snapshot database after Sprint 7 completion
Sprint 7: Talk completed (13/13 done).
Sprint 8: React activated (10 tickets, 2 carry-overs).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:34:46 +01:00
jpmschweitzerandClaude Opus 4.6 be168d59ef refactor(client): rename test_protocol_v6.gd to test_protocol_bridge.gd
Version numbers in test filenames go stale every sprint. The file tests
bridge protocol features (stance, inventory, fixtures) not a specific
protocol version.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:33:53 +01:00
jpmschweitzerandClaude Opus 4.6 6d6b59c17b fix(client): address PR #24 review — 5 critical bugs, 3 warnings, 8 suggestions
Critical:
- Protocol test assertions updated v6→v7 (test_protocol_v6.gd)
- Dialogue signal connections wired (option_selected→DialogueResponse,
  dialogue_dismissed→DialogueEnd sent to server via SimBridge)
- Consume-once race fixed: _consume_dialogue() checks is_dialogue_active()
  before re-showing; dialogue_id tracking prevents re-trigger during fade
- Dialogue box responsive: _update_layout() clamps width to MAX_WIDTH_PX
  (832px) or 65% viewport, height to 20% viewport (MAX_HEIGHT_RATIO)
- Auto-pause added: SimBridge.send_input(PAUSE) on dialogue open/close

Warnings:
- Test coverage: 16 new tests in test_protocol_v7.gd (pending_recognitions
  decode, current_dialogue, GameState, SimBridge mock data, insert colors)
- queue_redraw() optimization: early return when no entities and no pings
- Fixed 200px height → responsive 20% viewport via _update_layout()

Suggestions:
- WASD detection refactored to _WALK_AWAY_ACTIONS array loop
- Button colors reference Constants.INSERT_COLOR_TEXT/HOVER/ACTIVE
- Named constants: COLOR_TRANSITION_START, SILHOUETTE_APPEAR_THRESHOLD,
  SILHOUETTE_SIZE with explanatory comments
- Bounds check: MAX_PENDING_RECOGNITIONS=64 with truncation warning
- Mock dialogue sustained across ticks (not 1-tick flash)
- COLOR_PING coupling with cursor documented as intentional
- Consume helpers extracted: _consume_monologue(), _consume_dialogue()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:30:15 +01:00
jpmschweitzer 90a80e73df Merge remote-tracking branch 'origin/main' into client 2026-02-16 01:26:36 +01:00
jpmschweitzerandClaude Opus 4.6 7873b77a5c chore(meta): add Sprint 8 briefings and DB backup
Sprint 8: React — briefings for server, client, audio, and joint teams.
Database snapshot after Sprint 8 ticket assignments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:26:04 +01:00
jpmschweitzer 7edc513632 Merge remote-tracking branch 'origin/audio'
# Conflicts:
#	CHANGELOG.md
2026-02-16 01:25:40 +01:00
jpmschweitzerandClaude Opus 4.6 91c90829b7 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:23:16 +01:00
jpmschweitzerandClaude Opus 4.6 6035d2049a feat(audio): add 6 interaction UI audio assets for #440
Generated via Stable Audio Open with sonic family prompt prefixes:
- cursor_hover.ogg (insert-tech, digital click)
- weapon_aim.ogg (insert-tech, mechanical latch sketch)
- implant_open.ogg (insert-tech, neural lattice rising tone)
- fog_recognition.ogg (organic, cognitive realization chime)
- sfx_monologue_chime.ogg (insert-tech, placeholder per D-071)
- sfx_monologue_chime_urgent.ogg (insert-tech, placeholder per D-071)

All assets need duration trimming in next audio sprint (#453).
Updated docs/assets/audio/ui.md status to draft.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:22:56 +01:00
jpmschweitzerandClaude Opus 4.6 725dc4684a chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:19:40 +01:00
jpmschweitzerandClaude Opus 4.6 2659de0c28 feat(client): add fog entity visualization and wire Sprint 7 UI (#431, #434)
Fog entity cognitive delay rendering (D-059/D-060): sonar-style sound
pings (3 concentric rings, 1.5s fade), unrecognized grey blobs with
breathing pulse, D-033 color transition at 50% recognition progress,
±0.5 tile position drift. FogEntities node at z:950 between fog
shader and InsertOverlay.

Protocol v7 bump to match server PR #23 (pending_recognitions field).
Wires dialogue box and fog entities into game loop with mock test data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:19:24 +01:00
jpmschweitzerandClaude Opus 4.6 2d56131221 feat(ui): add dialogue box — bottom screen, max 20% height, walk-away (#434)
D-061 compliant UI skeleton: NPC speech top, max 3 response options
below, left-aligned. Insert-styled colors (white-blue, amber hover,
green pressed). WASD walk-away fades over 300ms, no close button.
InsertOverlay (CanvasLayer 10, z-layer 6). Mock data in next commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:19:15 +01:00
jpmschweitzerandClaude Opus 4.6 88fc344ed9 docs(sprints): add protocol v7 bump requirement to client briefing
Server PR #23 bumped PROTOCOL_VERSION to 7 (pending_recognitions field).
Client must match before #431 fog entity visualization or joint testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:08:29 +01:00
jpmschweitzer 307124cee6 Merge remote-tracking branch 'origin/server'
# Conflicts:
#	CHANGELOG.md
2026-02-16 01:08:17 +01:00
jpmschweitzerandClaude Opus 4.6 d0d545f44d feat(audio): add dip spec and synthesis tooling for #440
dialogue-ambient-dip.md: full Godot implementation spec for dialogue
(-7dB ambient) and confrontation (-11dB ambient + LP 800Hz, -5dB world
SFX) audio dips with tween code and edge case handling. synth_ui_sounds
.py: FM/noise/impulse synthesis for insert-tech UI sounds (cursor hover,
weapon aim, monologue chimes).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:04:44 +01:00
jpmschweitzerandClaude Opus 4.6 d4fdbf426e fix(server): address PR #23 review — 4 critical bugs, 3 warnings, 11 suggestions
Critical fixes:
- Add PendingRecognitionWire serialization roundtrip test
- Check Option return from delay.cancel() before logging
- InputQueue capacity limit (1000) with drop-oldest and warning
- TODO in observation.rs references ticket #450

Warning fixes:
- Hot-reload guards against invalid/empty content root
- Location header mismatch warning in line pool indexing
- walk_yaml() depth limit (100) against symlink loops
- Consecutive reload failure counter (warns after 5+)

Test additions:
- Negative prerequisite filtering test for monologue lines
- Integration test for pending_recognitions in observer snapshot
- Eavesdrop threshold ordering assertion (Careful < default)

Documentation:
- Playtesting expectation comments on delay constants
- is_pending() scalability note for future NPC cognitive delay
- ID format regex validation in line pool spec
- BTreeMap vs sort() ordering clarification in loader
- Multiplayer TODO in relationships.rs references D-010
- ContentSlug ticket #452 filed for entity slug resolution

394 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:03:51 +01:00
jpmschweitzerandClaude Opus 4.6 c5f0694a1b feat(skills): add gen-audio skill, rename asset-gen to gen-image
gen-audio: prompt assembly system (sonic palette + category templates +
asset descriptions), SAO generation workflow, post-processing pipeline,
manual synthesis guide for sub-200ms insert-tech sounds. Rename
asset-gen to gen-image for consistent gen-* naming. Add audio connector
permissions to settings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:03:45 +01:00
jpmschweitzerandClaude Opus 4.6 094939a33d feat(db): add Stable Audio Open connector and post-processing wrappers
audio-generate: Gradio API wrapper with timeout handling (600s default),
sequential-only generation, SSE stream polling. audio-health: API
health check. audio-post: ffmpeg wrapper for convert/normalize/trim/
pipeline operations. All follow existing connector pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:03:30 +01:00
jpmschweitzerandClaude Opus 4.6 34d0e7211c docs(assets): add asset pipeline documentation system
Category-based asset index for audio, visual, and video pipelines.
Audio pipeline fully populated: palette (sonic identity bible), ambient
loops, SFX, UI sounds with generation specs and mix parameters.
Visual and video are stubs pending their respective workshops.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:03:18 +01:00
jpmschweitzerandClaude Opus 4.6 ac86c8a72b docs(decisions): record audio discussion outcomes D-067 through D-074
Recognition chime timing (D-067), 5-bus audio architecture (D-068),
audio dip profiles (D-069), confrontation as cognitive vulnerability
(D-070), monologue chime placeholder strategy (D-071), universal
conversation murmur (D-072), zone crossfade (D-073), hybrid audio
generation (D-074). Amends D-038 scope, resolves Q-014.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 01:03:02 +01:00
jpmschweitzerandClaude Opus 4.6 8a334b47f3 chore(meta): update changelog
Sprint 7 server entries: cognitive delay, ListeningFocus, YAML loader,
line pool spec, interaction memory schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:42:13 +01:00
jpmschweitzerandClaude Opus 4.6 087ff67bfb docs(architecture): add interaction memory KG schema design (#442)
Design doc at docs/architecture/interaction-memory-schema.md extending
FactKnowledge with interaction tracking. FactMetadata enum on existing
BTreeMap, 5-state InteractionState (Active/Completed/WalkedAway/
Confrontation/ConflictAbandoned), monologue prerequisite extension.

Includes entity slug resolution approach, NpcTolerance reconciliation,
and fact decay exemption documentation.

Ref: D-041, D-064, D-063

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:41:40 +01:00
jpmschweitzerandClaude Opus 4.6 91d6b1b1cb feat(simulation): add YAML content loader with hot-reload (#326)
LinePool system parses dialogue.yaml and monologue.yaml into indexed
BTreeMap structures. Supports 4-layer query filtering (access >
situation > trust > topic+mood) per D-028. Hot-reload via timestamp
polling every 20 ticks (dev-only). Graceful failure preserves
previous content on reload error.

30+ tests covering enum parsing, index building, query filtering,
monologue fallback, and content watching.

Ref: D-028, D-032, D-035, D-041

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:41:29 +01:00
jpmschweitzerandClaude Opus 4.6 ea0a3f0b35 docs(architecture): add line pool format specification (#308)
Formal spec at docs/architecture/line-pool-format.md defining YAML
structure for dialogue and monologue content files. Covers tag enums,
4-layer filtering pipeline, prerequisite-to-KG mapping, ID format,
validation rules, and Rust loader interface.

Fixes monologue schema: adds required constraint on relationship
prerequisite target/state fields.

Ref: D-028, D-032, D-035, D-041

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:41:18 +01:00
jpmschweitzerandClaude Opus 4.6 4d8fe996fc feat(perception): add cognitive delay system for fog recognition (#423)
New CognitiveDelay component buffers perception events before emitting
KnowledgeEvents. Normal delay: 6 ticks (0.6s), urgent: 3 ticks (0.3s).
Drain system runs after emit_observation_events, before process_knowledge_events.

Adds pending_recognitions to ObserverSnapshot (protocol v7) for client
fog entity visualization. Includes cancellation when entity leaves LOS.

Monologue fires during delay (not after) per D-060 — cross-system
wiring deferred to follow-up ticket #451.

Ref: D-060 (cognitive delay), D-031 (10 tps), D-059 (fog layers)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:41:08 +01:00
jpmschweitzerandClaude Opus 4.6 3a2cfc37e0 feat(simulation): add ListeningFocus eavesdrop positioning (#426)
New ListeningFocus component tracks stationary_ticks for eavesdrop
mechanic. Increments when position unchanged, resets on movement.
Sprint stance blocks accumulation, Careful reduces threshold from
30 to 20 ticks. Registered in SimulationPlugin after validate_movement.

17 tests covering all stances, thresholds, and edge cases.

Ref: D-053 (stance system), D-018 (sound model)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:40:54 +01:00
jpmschweitzerandClaude Opus 4.6 d0663c4193 refactor(server): apply rustfmt and clippy suggestions
Formatting pass across simulation, perception, knowledge, NPC, and
test modules. Includes two clippy fixes in monologue.rs (.values()
instead of for (_, v) pattern).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:40:43 +01:00
jpmschweitzerandClaude Opus 4.6 b218338223 chore(db): snapshot database after copy merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:36:42 +01:00
jpmschweitzerandClaude Opus 4.6 d1764cbd70 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:26:25 +01:00
jpmschweitzerandClaude Opus 4.6 462f42113f docs(docs): add archetype evidence presentation spec (#443)
Design specification for how evidence is presented differently per
archetype — detective case file (institutional, insert-assisted) vs
smuggler notebook (personal, unassisted). Covers item definitions,
knowledge graph presentation, contradiction markers, THE FRIEND
arc walkthroughs, systems interaction map, and authoring guidelines.

Sprint 7, ticket #443. Authored by Mellanie (copy), Paula (narrative),
Gestalt (systems).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 00:26:13 +01:00
jpmschweitzer 9b022197a8 Merge remote-tracking branch 'origin/main' into copy 2026-02-15 23:53:00 +01:00
jpmschweitzerandClaude Opus 4.6 8ec875385a docs(sprints): add Sprint 7: Talk briefings
Briefings for server, client, copy, audio, and joint teams.
Sprint goal: complete interaction UI layer and activate basic
dialogue system with NPC conversations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:51:57 +01:00
jpmschweitzer 8399a75d98 Merge remote-tracking branch 'origin/main' into copy 2026-02-15 23:46:00 +01:00
jpmschweitzerandClaude Opus 4.6 d52179c58b docs(meta): add scratchpad for design ideas and notes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:35:20 +01:00
jpmschweitzerandClaude Opus 4.6 ebf348e838 chore(db): snapshot database after Sprint 6 completion
All 18 tickets marked done, sprint closed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:32:01 +01:00
jpmschweitzerandClaude Opus 4.6 9e8ccb13bf fix(client): address PR #22 review — 3 critical bugs, 2 warnings, 8 suggestions
Critical fixes:
- fog_state: guard _compute_bounds() against all-invalid tiles (negative Rect2i crash)
- fog_shader: read Camera2D zoom dynamically instead of hardcoded Vector2(2,2)
- world_radial: set custom_minimum_size in _ready() from spoke geometry

Warnings:
- fog.gdshader: tighten PERIPHERAL_LOW 0.15→0.55 to match D-059 peripheral band
- Extract color_for_entity_kind() to Constants.gd, decouple CursorRenderer from EntityRenderer

Documentation: shallow copy assumption, gradual decay TODO, tween guard rationale,
fade timing rationale, ToggleInsert TODO, monologue consume-once, hover offset safety,
v6 fixture gap TODO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:23:11 +01:00
jpmschweitzerandClaude Opus 4.6 1b0953db25 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:06:32 +01:00
jpmschweitzerandClaude Opus 4.6 f08710fa61 docs(architecture): z-layer gap analysis, fog shader spec, flying taxi analysis
Three architecture documents from Sprint 6 design work:
- z-layer-gap-analysis.md: full three-scope rendering pipeline with
  reserved z-ranges, airborne treatment, cross-floor VFX, liquid depth
- fog-shader-spec.md: 5-layer fog shader architecture (D-059)
- flying-taxi-analysis.md: validates z-layer architecture supports
  flight scenarios with LOD tiers and performance budget

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:06:12 +01:00
jpmschweitzerandClaude Opus 4.6 1fcf08d21f feat(client): Sprint 6 Touch — z-layer pipeline, cursor, fog, interactions, inventory, stance, radial
Three-scope z-layer rendering pipeline (D-049): world z:0-900 inside
CanvasGroup, insert overlay CanvasLayer 10, UI CanvasLayer 20, modal
CanvasLayer 30. Y-sort contract enforced (entities z_index=0). Reserved
ranges for VFX, airborne, lower floors documented in constants.gd.

Sprint 6 client tickets:
- #429: Cursor state machine — 4 states, 150ms transitions (D-056)
- #430: Fog shader rebuild — 5-layer fragment shader, animated noise (D-059)
- #432: Entity interaction list — vertical multi-verb, insert-styled (D-057)
- #433: World radial menu — 2 spokes, drag-release + click-click (D-058)
- #438: Inventory UI — 3x3 grid, 40x40px, 1-9 hotkeys (D-065)
- #439: Stance indicator — color-coded HUD, C/X keybinds (D-053)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:06:03 +01:00
jpmschweitzerandClaude Opus 4.6 24cce41379 feat(client): protocol v6 bridge — stance, inventory, input mapping
Upgrade client protocol bridge from v5 to v6 to match server.
Adds player_stance (4 variants) and player_inventory decode to
ObserverSnapshot. Adds TOGGLE_STANCE_UP/DOWN to InputMapper.
Includes 25 gdUnit4 tests for v6 decode + server serialization
test gap fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:05:47 +01:00
jpmschweitzerandClaude Opus 4.6 f3550577a9 chore(db): snapshot database after server branch merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:10:13 +01:00
jpmschweitzer 2b98e54103 Merge remote-tracking branch 'origin/server' 2026-02-14 16:09:18 +01:00
jpmschweitzerandClaude Opus 4.6 aadb155968 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:01:08 +01:00
jpmschweitzerandClaude Opus 4.6 3d63a4f9f6 test(simulation): QA coverage for Phase 2 verbs, sprint suppression, occupancy, inventory
Add comprehensive test suites across sprint 6 systems:
- Phase 2 observer filter: Confront injection, archetype label relabeling,
  contradiction marking, POI priority ordering (12 tests)
- Sprint anomaly: detection during sprint, queue semantics, delay timing,
  D-055 visible-but-suppressed integration (7 tests)
- Wire format: Confront and CharacterArchetype msgpack roundtrip (4 tests)
- TilePresence: 4-layer coexistence on same tile (1 test)
- Inventory: Take/Place full roundtrip, capacity enforcement (3 tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:00:32 +01:00
jpmschweitzerandClaude Opus 4.6 df81b24cd1 feat(simulation): sprint anomaly double-take monologue (#428, D-055)
When sprinting past a Contradicted entity, queue a delayed retroactive
monologue that fires after ~1.5s: "Wait — something wasn't right back
there." SprintAnomalyQueue component with first-in-wins semantics.
Detection in observer pipeline, processing in monologue system.
Wire MovementProfile, Stance, and SprintAnomalyQueue into player spawn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:00:20 +01:00
jpmschweitzerandClaude Opus 4.6 1d76100f5d feat(simulation): movement profile per archetype and sprint interaction suppression (#418, #419)
Add MovementProfile component with per-archetype default stances (D-053).
Sprint stance now explicitly clears the interaction buffer — no verbs
computed or sent during sprint (D-055). Anomaly monologue pipeline is
unaffected (handled by separate system).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:00:09 +01:00
jpmschweitzerandClaude Opus 4.6 2fc8bf9545 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:44:53 +01:00
jpmschweitzerandClaude Opus 4.6 6ef7029707 feat(simulation): smuggler inventory — CarriedBy, Take/Place verbs (#424)
Implement physical inventory system per D-065:

- CarriedBy(StableId) component marks items as carried by an entity
- ItemName and InventorySlot components for display and slot assignment
- Take verb: removes TilePosition, adds CarriedBy + InventorySlot
- Place verb: removes CarriedBy + InventorySlot, adds TilePosition
- Observer snapshot populates player_inventory from carried items query
- Info boundary: carried items lack TilePosition, naturally excluded
  from all spatial queries (visibility, interactions) for other entities
- 9-slot capacity (3x3 grid per OQ-24), auto-assigns first free slot
- 8 new tests covering take, place, sequential slots, full rejection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:44:37 +01:00
jpmschweitzerandClaude Opus 4.6 32422ac4bc chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:39:28 +01:00
jpmschweitzerandClaude Opus 4.6 98f4cedc03 feat(simulation): Sprint 6 Touch — stance, tile presence, verbs, protocol v6
Implements the core Sprint 6: Touch systems across 5 tickets:

- #449 ObserverSnapshot v6: add player_stance (MovementStance) and
  player_inventory (Vec<InventoryItem>) wire fields with serde defaults
  for backward compatibility. Bump PROTOCOL_VERSION 5→6.

- #417 Stance system: Sprint/Walk/Careful/Crouch movement stance with
  tick-based speed (1/2/3/4 ticks per move), monologue rate multipliers,
  and PlayerMoveCooldown component. ToggleStanceUp/Down player actions.

- #420 TilePresence: posture-layer collision system allowing same-tile
  occupancy for different layers (Standing/Prone/Seated/Fixture).
  Layer-based collision in validate_movement.

- #421 ObjectType component: Readable/Container/Terminal/Door/Pickup/
  Furniture types with Phase 1 verb sets computed from type + proximity.

- #422 Phase 2 verb filter: KG-gated observer-side verb processing —
  POI priority flips, Confront injection at KnowsDetails+, contradiction
  marking, archetype-specific label relabeling (Smuggler/Detective).

217 unit tests + 17 integration tests passing. All MessagePack fixtures
regenerated for v6 wire format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:38:59 +01:00
jpmschweitzerandClaude Opus 4.6 eb69f1f9ac chore(config): add git rev-parse --show-toplevel permission
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:29:07 +01:00
jpmschweitzerandClaude Opus 4.6 651b1d34a6 chore(db): update database backup after server merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:17:23 +01:00
jpmschweitzerandClaude Opus 4.6 1126df4198 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:15:58 +01:00
jpmschweitzerandClaude Opus 4.6 52c4872650 chore(config): add worktree boundary rules to CLAUDE.md and plan-sprint skill
Agents were navigating outside the git root into sibling worktrees.
Added explicit boundary rules: all work stays within the git root
unless directly instructed otherwise. Updated plan-sprint to enforce
worktree-relative paths in generated briefings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 15:15:45 +01:00
jpmschweitzerandClaude Opus 4.6 538b59a7ff chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:01:33 +01:00
jpmschweitzerandClaude Opus 4.6 15b1955827 chore(config): add find, list, tree command permissions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:01:10 +01:00
jpmschweitzerandClaude Opus 4.6 f1bd361816 feat(docs): add D-066 dual-scale grid decision and propagate to all docs
0.5m sim tiles for movement/LOS granularity, 1m visual tiles for
proportional art and readable spaces (2x retina factor). All world
geometry constrained to 2x2 sim tile minimum so cover/LOS maps 1:1
with visuals. Entities occupy 1x1 sim tiles for fine positioning.

Updates: D-043, D-044, D-051, D-056, D-059 annotations, workshop
outcomes (art direction + control/interaction), sprint 6 briefings,
decision index, and CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:01:05 +01:00
jpmschweitzerandClaude Opus 4.6 2c67ed7254 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:29:58 +01:00
jpmschweitzerandClaude Opus 4.6 007d9eddea chore(config): standardize permissions in shared settings.json
Move universally-needed permissions from per-worktree
settings.local.json into the shared .claude/settings.json:
git operations, ticket/sprint/sqlite CLI, make, tea, skills.
Add deny rules for destructive operations (force push, hard
reset, clean -f, rm -rf). Migrate from deprecated :* suffix
to modern space-wildcard syntax.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:29:36 +01:00
jpmschweitzerandClaude Opus 4.6 187584209f feat(db): add sprint CLI for lifecycle management and agent context
Unified sprint script at db/connectors/sprint with 5 subcommands:
status, start, stop, start-work, prepare. Calls ticket CLI via
subprocess (no SQL duplication), auto-detects sprint from DB state
and team from git branch. Guards prevent activating unplanned
sprints. Updated start-sprint, plan-sprint, and ticket skills to
reference the new CLI. Added sprint CLI section to CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 10:29:28 +01:00
jpmschweitzerandClaude Opus 4.6 c41bec0d15 chore(client): track Godot .uid resource identifier files
These are auto-generated by Godot 4 to provide stable resource
references across renames. Tracking them prevents UID divergence.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:49:41 +01:00
jpmschweitzerandClaude Opus 4.6 b06a676dcf chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:48:46 +01:00
jpmschweitzerandClaude Opus 4.6 6e0ecb4d4a chore(db): update database backup after planning merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:48:30 +01:00
jpmschweitzerandClaude Opus 4.6 82bb6289c7 fix(skills): discover worktree branches dynamically in worktree-update
The skill previously relied on the agent hardcoding branch names,
which missed non-standard branches like planning. Now explicitly
instructs the agent to use git worktree list for discovery.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:48:26 +01:00
jpmschweitzerandClaude Opus 4.6 3671fceeb8 chore(skills): add start-workshop skill and update CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:35:07 +01:00
jpmschweitzerandClaude Opus 4.6 47f65c3302 feat(content): smuggler inventory item specs (#441)
Three physical items for the smuggler archetype per D-065: manifest copy,
corridor access token, personal comm log. Each with KG links, confrontation
mechanics, and risk/reward gameplay notes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:35:02 +01:00
jpmschweitzerandClaude Opus 4.6 9d0077bb80 docs(sprints): Sprint 6 Touch briefings — server, client, copy, joint
18 tickets across 3 teams. Movement stances, fog shader rebuild,
multi-verb interactions, smuggler inventory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:34:55 +01:00
jpmschweitzerandClaude Opus 4.6 5c3c2a527b docs(workshops): Control & Interaction Workshop outputs
Workshop notes (2 rounds), outcomes document (13 decisions, 27 tickets,
v0.1 interaction model), and updated brief with final lead direction.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:34:49 +01:00
jpmschweitzerandClaude Opus 4.6 c28d9b6501 feat(decisions): formalize D-053 through D-065 from Control & Interaction Workshop
13 new decisions covering movement stances, tile-based movement, cursor states,
entity interaction, fog rendering, dialogue UX, confrontation, and smuggler inventory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:34:42 +01:00
jpmschweitzerandClaude Opus 4.6 eab18b8361 feat(content): smuggler inventory item specs (#441)
Three physical inventory items for the smuggler archetype per D-065:
manifest copy, corridor access token, personal comm log. Each item
links to KG FactIds, defines confrontation leverage mechanics, and
creates risk/reward carry decisions. All flagged contraband.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 09:32:02 +01:00
jpmschweitzerandClaude Opus 4.6 485dccf30b chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:26:34 +01:00
jpmschweitzerandClaude Opus 4.6 ce3778321d chore(skills): update review-pr skill
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:26:00 +01:00
jpmschweitzerandClaude Opus 4.6 dde270322c docs(briefings): update agent briefings after Sprint 5 pipeline fixes
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:24:43 +01:00
jpmschweitzerandClaude Opus 4.6 3c106aa2d0 feat(simulation): complete interaction and monologue pipelines (#413, #414, #415)
Three fixes to make the gameplay loop functional end-to-end:

- Add Interactable component to NPC spawn so E-prompt detection works
- Build monologue trigger system (enter_location + time_idle) with
  MonologueBuffer/MonologueState components, wire through ObserverSnapshot
  as current_monologue field, decode on client and display via HUD
- Change PlayerAction::Interact from unit to struct variant carrying
  optional target_entity_id and verb fields

Bumps protocol version from 4 to 5. Regenerates MessagePack fixtures.
All 200 tests pass (170 unit + 30 integration).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 19:23:47 +01:00
jpmschweitzerandClaude Opus 4.6 8228ef3c1c chore(db): update database backup after client merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:11:56 +01:00
jpmschweitzer ad91d03468 Merge remote-tracking branch 'origin/client' 2026-02-13 18:11:42 +01:00
jpmschweitzer 07d7bf635d Merge remote-tracking branch 'origin/ci'
# Conflicts:
#	CHANGELOG.md
2026-02-13 18:10:27 +01:00
jpmschweitzerandClaude Opus 4.6 0f9ecac74a chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:07:34 +01:00
jpmschweitzerandClaude Opus 4.6 470ec59b63 fix(ui): adapt client to new ui-strings.yaml structure
The copy team rewrote ui-strings.yaml with multi-level nesting,
renamed sections, and inline comments. Update the YAML parser to
use a stack-based approach for arbitrary nesting depth, update HUD
and interaction prompt to reference the new key names, and align
tests with the new structure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:07:15 +01:00
jpmschweitzer 15ea6f1375 Merge remote-tracking branch 'origin/copy'
# Conflicts:
#	CHANGELOG.md
#	client/data/ui-strings.yaml
2026-02-13 17:57:10 +01:00
jpmschweitzerandClaude Opus 4.6 218673f0ea chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:55:10 +01:00
jpmschweitzerandClaude Opus 4.6 fe3232377d refactor(content): move ui-strings.yaml to client/data/
UI microcopy strings belong in the client data directory where
the Godot client can load them directly, not in campaign content.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:54:55 +01:00
jpmschweitzerandClaude Opus 4.6 15a4bcadd2 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:52:24 +01:00
jpmschweitzerandClaude Opus 4.6 124573e1c5 fix(content): normalize fact_ids, entity refs, tags across FRIEND content
Fixes from Gestalt mechanical review:
- 73 fact_id normalizations to dotted category.topic format (critical:
  fixes broken detective evidence chain from Sera to Kael observations)
- Entity refs normalized to npc:hyphenated format (~15 occurrences)
- Phase tags standardized (friend-arc-phaseN → phase-N)
- Phase-2 tags added to 8 Kael ring ops lines
- bar_evening situation added to 15 Kael Last Shift lines
- Mood tags added to 10 Kael lines missing them
- 7 new detective monologue lines about Kael (gap: 7 → 15 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:52:05 +01:00
jpmschweitzerandClaude Opus 4.6 99550ed0f5 feat(content): add FRIEND packs and PC-as-NPC content (#297, #298, #401)
Kael Davan FRIEND pack (86 lines): 3 dialogue files across terminal,
bar, and corridors. 5-phase relationship arc with contradiction scene.

Sera Venn FRIEND pack (75 lines): full dialogue at The Last Shift with
trust-gated gossip, avoidance contradiction, and contaminated trust arc.

PC-as-NPC content (70 items): smuggler and detective as NPCs with
public-tier dialogue, tell-observation monologue, and bar variants.
Enables D-039 wow moment #4 (divergence reveal).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:51:54 +01:00
jpmschweitzerandClaude Opus 4.6 adaaebbe71 feat(content): add UI microcopy strings (#409)
72 YAML strings for client integration: interaction verbs,
relationship states, HUD labels, perception modes, notifications,
knowledge panel, tutorial prompts, menus, character select.
Diegetic voice matching Sova setting (D-036).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:51:43 +01:00
jpmschweitzerandClaude Opus 4.6 d51d9d9e46 feat(content): add NPC authoring style guide (#379)
Comprehensive authoring handbook: tier budgets, 9 NPC patterns,
dialogue/monologue rules, tag taxonomy, dual-lens coordination,
Krenn/Sova regional culture, FRIEND phase mapping for Kael and
Sera, mechanical validation checklist, and worked example.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:51:35 +01:00
jpmschweitzerandClaude Opus 4.6 e01de6684e feat(content): add character voice speech patterns (#310)
Speech-level execution spec for smuggler and detective: contraction
rules, sentence length, punctuation patterns, stress markers,
vocabulary, verbal tics, and 8-point authoring validation checklist.
Blocks all FRIEND pack and PC-as-NPC content authoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:51:29 +01:00
jpmschweitzerandClaude Opus 4.6 163de8bf25 fix(ci): address PR review — filter comments, trim whitespace, warn on missing scripts
- Filter YAML comment lines (grep -v '^\s*#') from both extraction
  pipelines to prevent phantom canonical IDs
- Trim trailing whitespace from extracted fact_ids so grep -qxF exact
  match works reliably
- Pre-commit dispatcher now prints explicit warning when a check script
  is missing instead of silently skipping
- Document --no-verify bypass for emergencies in DEVOPS.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:42:22 +01:00
jpmschweitzerandClaude Opus 4.6 e90fe8b146 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:18:19 +01:00
jpmschweitzerandClaude Opus 4.6 5a04656781 feat(ui): add YAML-based UI string loading system (#409)
Adds UIStrings autoload that loads display text from a YAML file,
replacing hardcoded strings in HUD and interaction prompt. Copy team
can now author UI microcopy in client/data/ui-strings.yaml without
touching GDScript. Includes 38 strings across 5 categories and 16
gdUnit4 tests for the parser and lookup API.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:18:01 +01:00
jpmschweitzerandClaude Opus 4.6 fa3c473273 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:17:49 +01:00
jpmschweitzerandClaude Opus 4.6 fe204aa5f4 feat(ci): add pre-commit FactId validation hook (#393)
Grep-based pre-commit check validating fact_id references in content
YAML against canonical knowledge catalogs. Runs in advisory mode when
catalogs are stubs (exit 0), switches to enforcing mode once populated
(exit 1 on unknown fact_ids with file:line output).

- tooling/check-fact-ids: core validation script (<2s runtime)
- .config/hooks/pre-commit: hook dispatcher for modular checks
- Makefile: check-fact-ids + setup-hooks targets, wired into setup
- docs/DEVOPS.md: content validation and pre-commit hooks sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:17:34 +01:00
jpmschweitzerandClaude Opus 4.6 3b757f5adc chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:14:08 +01:00
jpmschweitzerandClaude Opus 4.6 524aca52eb chore(db): update database backup after client merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:14:01 +01:00
jpmschweitzerandClaude Opus 4.6 03914c6442 docs(briefings): update Hoshe with integration test coverage priorities
Adds directive to focus on client-server integration tests landing
from Sprint 5, push for proper test harnesses, a dedicated test map
for routing/edge cases, and edge case coverage over happy-path volume.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:13:57 +01:00
jpmschweitzer 963f89b8ba Merge remote-tracking branch 'origin/client'
# Conflicts:
#	CHANGELOG.md
2026-02-13 02:09:47 +01:00
jpmschweitzerandClaude Opus 4.6 dd40f92600 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:08:12 +01:00
jpmschweitzerandClaude Opus 4.6 c68585e5f2 chore(simulation): add debug logging for received inputs
Log each received player input at DEBUG level with tick and action,
useful for diagnosing input pipeline issues during live testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:07:47 +01:00
jpmschweitzerandClaude Opus 4.6 e6d6802c8d fix(client): align tests with kind-based player detection
Tests used entity_id matching to find the player, but game_state now
finds the player by kind.variant == "Player". Update test fixture to
include a Player entity and remove stale player_entity_id assignments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:07:43 +01:00
jpmschweitzerandClaude Opus 4.6 e810abdcaf fix(client): use server tick for movement input processing
Client was sending Time.get_ticks_msec() (e.g. 12345) as the input
tick, but the server's drain_for_tick only processes inputs where
tick <= current_tick (a small frame counter). Inputs accumulated in
the queue and were never processed, making movement keys unresponsive
in live mode. Use GameState.current_tick from the latest snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:07:38 +01:00
jpmschweitzerandClaude Opus 4.6 ea9cf554c6 fix(client): decode tile_kind and fix entity alignment (#412)
Protocol decoder now reads tile_kind from VisibleTile and maps it to
the client's tile type string (floor/wall/door/object). GameState
falls back to visible_tiles when the test-mode tiles array is absent,
enabling live server tile rendering.

Fix entity-to-tile alignment: server sends tile-center render coords
(tile 16 -> 16.5) but entity renderer was using raw floats, placing
entities half a tile off. Now floors the coords to get the tile index.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:07:29 +01:00
jpmschweitzerandClaude Opus 4.6 29c5003403 fix(simulation): add TileKind to VisibleTile wire protocol (#412)
Server was sending visible_tiles without tile type data, so the client
could not distinguish floor from wall in live mode. Add TileKind enum
(Floor/Wall/Door/Object) to VisibleTile, populated from WalkabilityMap
in the NaturalVision perception query. Update test fixtures to include
the new field. Uses #[serde(default)] for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 02:07:19 +01:00
jpmschweitzerandClaude Opus 4.6 62406aca82 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:51:07 +01:00
jpmschweitzerandClaude Opus 4.6 2fa609b19c docs(sprints): add Sprint 5 "Live" team briefings
Content-focused sprint: FRIEND packs (Kael Davan, Sera Venn),
NPC authoring style guide, voice patterns, PC-as-NPC content,
UI microcopy, and FactId pre-commit validation. Briefings for
copy (6 tickets), client (2), CI (1), plus joint integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:50:59 +01:00
jpmschweitzerandClaude Opus 4.6 33f4f6078c feat(client): add input roundtrip integration test (#411)
Three tests validating the full client-server input path through a
live server: movement roundtrip (MoveNorth/MoveEast with position
verification), interact roundtrip (unit variant accepted as no-op),
and mixed sequence (movement then interact preserves position).

Catches integration seams that unit tests miss — each test spawns a
real server binary on a random port, connects via LocalBridge, and
exercises the TCP→deserialize→simulation→snapshot→decode pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:48:35 +01:00
jpmschweitzerandClaude Opus 4.6 721e54491c chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:46:27 +01:00
jpmschweitzerandClaude Opus 4.6 473eda03cd feat(client): add live server mode with make game target
Add SR_LIVE=1 environment variable to switch SimBridge from test mode
to TCP connection. Default behavior unchanged (test mode).

- sim_bridge.gd: read SR_LIVE env var instead of hardcoded test_mode
- game_state.gd: find player entity by kind.variant == "Player" instead
  of hardcoded entity_id 1 (real server assigns different IDs)
- Makefile: add 'make game' (builds server, starts it, launches client
  with SR_LIVE=1, kills server on exit) and 'make stop' helper

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:46:09 +01:00
jpmschweitzerandClaude Opus 4.6 67978d0819 feat(client): wire interaction prompt target+verb to server (#405)
Enable the interaction data attachment in main.gd game loop. When E
is pressed and the prompt is active, target_entity_id and verb are
attached to the Interact action before sending to the server. Without
a target, bare Interact is still sent as safe fallback.

Completes the v0.1 interaction prompt data flow:
server sends nearby_interactions → client shows prompt → E press
attaches target+verb → SimBridge encodes and sends to server.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:46:02 +01:00
jpmschweitzer 88cfded5b5 Merge branch 'server' into main — Sprint 4 server delivery (#18) 2026-02-13 01:21:02 +01:00
jpmschweitzerandClaude Opus 4.6 1fe03ac27d chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:20:13 +01:00
jpmschweitzerandClaude Opus 4.6 661769f028 feat(simulation): add line previewer CLI (#407)
CLI tool to preview monologue/dialogue line selection for a given game
state. 4 subcommands: dialogue (D-028 4-layer pipeline), monologue
(trigger + prerequisite evaluation), coverage (dead conversation
detection), sequence (walk-through simulation). Authoring tool for
content team — tests line trigger logic without running the full game.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:46 +01:00
jpmschweitzerandClaude Opus 4.6 67fef58020 feat(simulation): add YAML to RON converter tool (#403)
Build-time converter reads content YAML and emits RON format. Lives in
tooling/content-converter/ as standalone Rust crate. Runs via make
content-ron. Supports --dry-run, --verbose, --content-type filtering.
Not on critical path — engine consumes YAML in v0.1, RON is future-
proofing for runtime performance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:40 +01:00
jpmschweitzerandClaude Opus 4.6 d3b68012d9 docs(architecture): add seed configuration schema design (#394)
Design document defining what the randomizer produces at game-start:
FRIEND selections, pool draws, template assignments, triangle config,
entanglement config, contraband selection, starting knowledge. Includes
ChaCha20 RNG protocol, validation rules, and implementation roadmap.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:34 +01:00
jpmschweitzerandClaude Opus 4.6 267d415173 feat(simulation): implement content loader Phase 2 (#408)
Extend content loader to read and instantiate real YAML content into
ECS entities. 2-phase spawn pipeline: Phase 1 creates entities with
core components (Want, Tolerance, Contentment, Personality, Tells,
Skills), Phase 2 resolves cross-references (Relationships, Secrets,
Information via KnowledgeGraph, DailyRoutine).

Loads enums, entity attributes, pools, templates, triangles, and NPC
profiles. Handles stub files gracefully. 7 integration tests against
real content files plus edge case tests for invalid data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:29 +01:00
jpmschweitzerandClaude Opus 4.6 4c008d6191 data(content): use WantKind enum keywords in NPC profiles
Change want.primary from narrative strings to WantKind enum keywords
(Safety, Wealth, Knowledge, etc.) across all 20 NPC profiles. Move
narrative text to want.description. Add Happiness to WantKind enum.
Add enum constraint to npc-profile.schema.json so make validate-content
catches invalid values at authoring time.

Fixes silent Want component drop where parse_want_kind() returned None
for all real NPCs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:21 +01:00
jpmschweitzerandClaude Opus 4.6 90e6dcb8e9 fix(content): update dialogue-pool schema arrays per D-035
situation, topic, and mood fields changed from single string to
array type to match D-035 specification (list<enum>). access and
trust were already correct as arrays.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:15 +01:00
jpmschweitzerandClaude Opus 4.6 3d1bf54fcc data(content): add pools, templates, and triangles for v0.1 (#389, #390, #391)
5 seed-time pools (single-candidate for deterministic v0.1 testing),
3 social site templates (logistics-hub, bar, smuggling-ring) with
role slot definitions per D-025, and 5 triangles (3 active fork,
2 passive) per D-024 workshop synthesis.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:10 +01:00
jpmschweitzerandClaude Opus 4.6 b0fec9af09 data(content): populate global enums and entity attributes (#387, #388)
Populate 9 enum YAML files (situations, topics, moods, triggers,
access-tiers, trust-tiers, activities, patterns, motivations) from
D-035 taxonomy and 16 canonical entity attribute keys from D-024
with A7 workshop updates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:19:02 +01:00
jpmschweitzerandClaude Opus 4.6 e309f66c99 chore(db): update database backup after copy merge
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 01:00:29 +01:00
jpmschweitzerandClaude Opus 4.6 e7f6444567 feat(content): add monologue pool and news ticker headlines (#399, #395)
200 monologue lines (100 per character) across 8 location files:
arrival flavor, NPC commentary, knowledge-gated contradiction lines.
20 dual-lens news ticker headlines for The Last Shift bar. First real
content load for the monologue system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:58:57 +01:00
jpmschweitzerandClaude Opus 4.6 086988a42f feat(content): populate NPC YAML profiles and PC-as-NPC briefs (#398, #383, #373)
Convert 17 wiki NPC profiles to engine-readable YAML. Add PC-as-NPC
behavior profiles for detective and smuggler. Add smuggler-perspective
FactId progression text (27 FactIds with asymmetric knowledge paths).
Add off-stage NPC nils-davan. All files validate against npc-profile
schema (32 files, 0 errors).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:58:52 +01:00
jpmschweitzerandClaude Opus 4.6 b672b9c269 feat(docs): restructure wiki taxonomy with templates and Krenn System entry (#301, #319)
Move wiki from docs/wiki/ to root wiki/. Add 14 category templates with
YAML frontmatter, governance model, hierarchical spatial structure
(system > station > district > location). Populate Krenn System entry
as first concrete star system. Update cross-references in workshop and
sprint docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:58:45 +01:00
jpmschweitzer baa348a70a Merge remote-tracking branch 'origin/client'
# Conflicts:
#	CHANGELOG.md
2026-02-13 00:10:37 +01:00
jpmschweitzerandClaude Opus 4.6 8117f775c5 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:05:25 +01:00
jpmschweitzerandClaude Opus 4.6 a1c5db9490 fix(client): make E2E tests resilient to entity ordering
Server proof room now has 3 NPCs instead of 1. Find player entity
by kind instead of assuming entities[0]. Wall-hides test checks
specific NPC position (16.5, 13.5) rather than asserting zero NPC
count. Corner-reveal test searches for NPC1 by position.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:05:07 +01:00
jpmschweitzerandClaude Opus 4.6 52972e6f21 fix(client): align tests and data with server v4 protocol changes
Server team shipped strict PROTOCOL_VERSION enforcement (c05ff7b),
1-indexed verb priorities, and "Observe" label for ExamineNpc.
Updates all test snapshots to include version: 4, fixes sim_bridge
test mode priorities (0-indexed → 1-indexed) and labels, replaces
v1 backward-compat tests with strict version rejection tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:04:58 +01:00
jpmschweitzerandClaude Opus 4.6 fdb88840bc chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:02:41 +01:00
jpmschweitzerandClaude Opus 4.6 d1cb0d129d docs(sprints): add sprint 4 "Feel" team briefings
Content conversion and authoring sprint — 19 tickets across copy (8),
server (9), client (1 carry-over), and CI (1). Critical path runs
through wiki→YAML NPC conversion (#398) to content loader (#408).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 00:02:28 +01:00
jpmschweitzer 7af8de0117 Merge remote-tracking branch 'origin/main' into client
# Conflicts:
#	CHANGELOG.md
#	client/scripts/autoloads/sim_bridge.gd
2026-02-12 23:45:48 +01:00
jpmschweitzerandClaude Opus 4.6 5d94363d48 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:43:52 +01:00
jpmschweitzerandClaude Opus 4.6 6bebbd9933 feat(client): add interaction prompt system (#405)
Server-driven interaction prompt that displays "E - Talk" when near
an interactable NPC. Decodes v4 nearby_interactions from snapshot,
stores in GameState, renders via InteractionPrompt UI with fade
animation. Extensible interface (get_interaction_target/get_selected_verb)
for future radial verb menu (v0.2).

- Protocol: decode nearby_interactions array with nested VerbOption structs,
  entity relationship/observation fields, tick_rate in GameTime
- GameState: store/clear nearby_interactions per snapshot
- SimBridge: test mode generates v4 format with structured verbs
- InteractionPrompt: PanelContainer with fade in/out, polls GameState
- Tests: 19 new test cases covering protocol, state, sim bridge, UI, encoding
- Fixture assertions updated for v4 protocol version

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:43:37 +01:00
jpmschweitzer 07e3c4bafd Merge remote-tracking branch 'origin/server'
# Conflicts:
#	CHANGELOG.md
2026-02-12 23:40:13 +01:00
jpmschweitzerandClaude Opus 4.6 c765572bea chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:33:10 +01:00
jpmschweitzerandClaude Opus 4.6 c05ff7b063 feat(client): enforce PROTOCOL_VERSION check in snapshot decode
Client now rejects snapshots where version != PROTOCOL_VERSION (4).
Returns null with error log on mismatch. Test snapshot updated to
use Protocol.PROTOCOL_VERSION and v4 game_time format (tick_rate
replaces paused field).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:32:49 +01:00
jpmschweitzerandClaude Opus 4.6 18d8253bfe refactor(server): decompose observer pipeline and fix interaction boundary
Extract visibility geometry into a separate system behind a
PerceptionQuery trait, enabling D-017 perception mode swapping.
Two-stage pipeline: compute_visibility_geometry writes to
VisibilityGeometry resource, compute_observer_snapshot reads it.

Remove KnowledgeGraph from compute_nearby_interactions (simulation
phase boundary violation). Verb availability stays in simulation;
POI-based priority adjustment moves to observer via
apply_poi_verb_priority helper.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:32:41 +01:00
jpmschweitzer 30f6666d7d Merge remote-tracking branch 'origin/main' into visual 2026-02-12 23:28:25 +01:00
jpmschweitzerandClaude Opus 4.6 ebcaff1a05 chore(config): remove client .uid/.import gitignore additions
These patterns were added for the renderer (now separate). Client
gitignore rules are the client team's responsibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:26:39 +01:00
jpmschweitzerandClaude Opus 4.6 921d2aef0d refactor(assets): move sprite renderer to standalone project at renderer/
Separates the 3D-to-2D render pipeline from the game client into its
own minimal Godot project. No autoloads, no plugins, no game code —
just the render scene, models, textures, and export script. Eliminates
SimBridge parse errors and gdUnit4 scanning during renders.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:26:16 +01:00
jpmschweitzerandClaude Opus 4.6 8a1b10786f chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:22:38 +01:00
jpmschweitzerandClaude Opus 4.6 3a01d9659f chore(config): add .uid and recursive .import to gitignore
Godot 4.6 generates .uid files for script resources and .import files
in subdirectories. Both are editor-local and should not be tracked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:22:16 +01:00
jpmschweitzerandClaude Opus 4.6 2ed8fb5b37 feat(assets): add 3D-to-2D sprite render pipeline and /render-sprite skill
Godot @tool render scene that takes a textured 3D model and produces
2D sprites at 1024/256/64 from 4 cardinal directions with outlines
applied at working resolution. Camera at "the angle" (-72.5deg ortho).
Includes two POC wall models with Nano Banana textures, CLI wrapper
script with headless import step, and .uid files added to gitignore.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:21:41 +01:00
jpmschweitzerandClaude Opus 4.6 0358eedbc5 docs(workshops): archive art direction & mood board workshop (3 rounds + closing)
Four-agent workshop (Araminta, Ozzie, Miri, Gore) establishing visual
identity for The Settled Reach. 9 mood board images, 16 art direction
principles, 10 candidate decisions (D-042 through D-051), v0.1.1 sprite
plan. Post-workshop technical session resolved 3D render pipeline,
resolution chain (1024/256/64), and camera angle lock ("the angle").
Pipeline test samples included: Era 1 institutional wall + bar green
wall rendered through the full Nano Banana -> 3D model -> sprite chain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:21:09 +01:00
jpmschweitzerandClaude Opus 4.6 e6bd577a5a chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:53:23 +01:00
jpmschweitzerandClaude Opus 4.6 0c91f5af21 chore(tooling): add make validate-content for schema validation
Python script validates campaign YAML files against JSON schemas in
content/_schema/. Maps files to schemas by directory context (npcs/
→ npc-profile.schema.json, etc.). Skips comment-only placeholder stubs.

Addresses Hoshe #2 review item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:53:05 +01:00
jpmschweitzerandClaude Opus 4.6 0157d1fa38 test(server): add Entity::to_bits roundtrip and TickRate switch tests
- Entity::to_bits() roundtrip test guards against bevy version changes
  silently breaking wire IDs (Hoshe #1)
- PROTOCOL_VERSION constant used in test helpers instead of hardcoded 4
- TickRate switch mid-accumulation test verifies Half→Full→Paused→Half
  transitions preserve accumulator state correctly (Tyre N3)
- POI mid-range test now asserts priority=1 (Hoshe #4)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:52:59 +01:00
jpmschweitzerandClaude Opus 4.6 02fa89b1d7 refactor(server): extract observer tests to separate file
Split observer/mod.rs (820 lines) into production code (244 lines) and
tests (480 lines). Reduces module size per Hoshe #3 review item.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:52:52 +01:00
jpmschweitzerandClaude Opus 4.6 6f9537fff3 refactor(server): NearbyInteractionBuffer from Resource to Component
Per-entity Component attached to PlayerCharacter instead of global
Resource. Makes the interaction buffer multiplayer-ready (D-009) —
each observer gets their own buffer without pipeline refactoring.

Updated all 8 files touching the buffer: system signatures, player
spawn bundles, and ~30 test spawn sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 22:52:47 +01:00
jpmschweitzerandClaude Opus 4.6 c970d2eba7 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:23:17 +01:00
jpmschweitzerandClaude Opus 4.6 ba867564a8 fix(server): add error logging, protocol version, and architecture docs
- Replace silent Entity::to_bits() fallbacks with tracing::error in
  observer.rs and interaction.rs (makes unregistered entities loud)
- Add PROTOCOL_VERSION constant to types.rs, use in observer snapshot
- Document single-observer assumption on NearbyInteractionBuffer
- Document proximity-only (no LOS) limitation on compute_nearby_interactions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:22:34 +01:00
jpmschweitzerandClaude Opus 4.6 e2f2f4e590 refactor(server): remove dead generate_snapshot function
Delete generate_snapshot from bridge/mod.rs — superseded by
perception::observer::compute_observer_snapshot. Not referenced
in any schedule or test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:22:28 +01:00
jpmschweitzerandClaude Opus 4.6 9d53869ca7 refactor(data): restructure content directory for galaxy scale
Replace flat content/districts/ with hierarchical campaign/system/
station/district structure. Path mirrors canonical IDs, enables
glob-based discovery, and is multi-campaign/DLC ready.

- git mv 46 files preserving history
- New metadata: campaign.yaml, system.yaml, station.yaml
- Rewrite content.yaml with glob-based district discovery
- Add campaign, system, station JSON schemas
- Update district schema: hierarchy fields derived from path
- Update npc-profile schema: accept district-scoped IDs
- Add TODO to 17 dialogue/monologue pool files for generator revision
- Update _meta/README.md with new hierarchy documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 21:22:23 +01:00
jpmschweitzerandClaude Opus 4.6 8cd7ef21ae chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:14:56 +01:00
jpmschweitzerandClaude Opus 4.6 ece4dfe1c4 fix(data): restore hierarchical fields in district metadata
Re-added system/station/district fields to district.yaml that were
incorrectly removed during Tyre10 cleanup. These carry hierarchical
context (planet, station), not redundant identity. Made canonical_id
optional in schema since it's derived from directory path at load time.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:14:41 +01:00
jpmschweitzerandClaude Opus 4.6 9943684f2c refactor(server): address PR #16 review feedback
Hoshe + Tyre review items:
- Use StableId consistently for wire entity_id (H4) across observer,
  observation, interpretation, and interaction systems
- Make NearbyInteractionBuffer.interactions private with take() (H1/H20)
- Add system ordering constraint for compute_nearby_interactions (H5)
- Panic on missing PlayerCharacter in input processing (H2)
- Remove redundant paused field from GameTime (Tyre8)
- Remove #[serde(default)] from nearby_interactions (H3)
- Change NearbyInteraction.distance from f32 to u32 (H8)
- Add sort stability for equal verb priorities (H6)
- Scope constants to pub(crate) (H7)
- Add debug_assert for last_observed_tick ordering (H10)
- Strengthen unregistered entity handling to debug_assert + error (H11)
- Document fractional tick accumulation (Tyre9)
- Extract collect_remembered_entities helper (Tyre2/H17)
- Add half_rate_no_drift_over_10000_frames test (H14)
- Add mid-range and deterministic sort tests (H15)
- Add fixture version assertion (H16)
- Regenerate msgpack fixtures for wire format changes

146 unit + 19 integration tests pass, zero clippy warnings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 20:14:35 +01:00
jpmschweitzerandClaude Opus 4.6 458be0f621 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:49:19 +01:00
jpmschweitzerandClaude Opus 4.6 2de413dd87 feat(simulation): add proximity detection and interaction verbs
Implement compute_nearby_interactions system that detects entities
within close (≤2) and mid (≤5) Manhattan distance, computes
available verbs per D-060 spec. NPCs get Talk+Observe at close
range, Observe-only at mid range; PersonOfInterest flips priority.
Objects get Examine. Results populate nearby_interactions[] on
ObserverSnapshot v4. Bump protocol version 3→4. Implements #404.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:48:55 +01:00
jpmschweitzerandClaude Opus 4.6 5afea99622 feat(simulation): add tick rate scaling system
Replace binary paused flag with TickRate enum (Full/Half/Paused)
per D-052. Full rate advances every frame, Half every 2 frames
via fractional accumulation, Paused blocks all advances. Add
SetTickRate player action for client-driven rate changes.
Implements ticket #406.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:48:43 +01:00
jpmschweitzerandClaude Opus 4.6 ad55dfaa72 feat(data): add content schema definitions
JSON Schema (draft 2020-12) for content validation: district,
location, npc-profile, dialogue-pool, monologue-pool, triangle,
routine, and fact-catalog. NPC schema uses if/then conditional
for FRIEND pattern validation. Implements ticket #386.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:48:21 +01:00
jpmschweitzerandClaude Opus 4.6 10f5be29b1 feat(data): create content/ directory skeleton
Scaffolds the full content directory structure per D-057 spec:
district-as-atomic-pack layout with Sova Transit as the first
district. Includes 17 NPC stubs, 3 locations, 5 triangles,
9 dialogue pools, 8 monologue pools, routines, 7 factions,
7 knowledge catalogs, 8 enum definitions, and global region.
Implements ticket #385.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:48:13 +01:00
jpmschweitzer b5333f1612 Merge remote-tracking branch 'origin/main' into server 2026-02-12 19:19:39 +01:00
jpmschweitzer fd775878e7 Merge remote-tracking branch 'origin/copy'
# Conflicts:
#	decisions/content.md
2026-02-12 19:15:07 +01:00
jpmschweitzerandClaude Opus 4.6 2827dd3a83 fix(decisions): update stale Hael references to Naia in D-034 and D-036
Caught by Miri during copy PR #15 review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:14:46 +01:00
jpmschweitzerandClaude Opus 4.6 010c4158f9 docs(copy): address PR #15 review feedback
Miri (critical):
- Fix 3 stale "Hael" references in decisions/content.md (D-034, D-036)

Paula (suggestions):
- Flag dual-fire 15-30s delay as tuning target in smuggler moral arc
- Clarify MIRROR NPCs can be wrong (lied to) without being dishonest
- Add implementation note for Path C deferred follow-up fork
- Note detective-NPC dialogue rotation variants for post-v0.1
- Clarify detective vs smuggler moral_weight criteria in entity attributes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 19:07:24 +01:00
jpmschweitzer 16ee660b5d Merge remote-tracking branch 'origin/main' into copy
# Conflicts:
#	CHANGELOG.md
2026-02-12 19:04:21 +01:00
jpmschweitzer 617e9de9af Merge remote-tracking branch 'origin/main' into server 2026-02-12 18:56:42 +01:00
jpmschweitzer d17a3ff060 Merge remote-tracking branch 'origin/main' 2026-02-12 18:55:25 +01:00
jpmschweitzer 5102e40a17 Merge remote-tracking branch 'origin/maintenance'
# Conflicts:
#	.claude/skills/review-pr/SKILL.md
2026-02-12 18:45:27 +01:00
jpmschweitzerandClaude Opus 4.6 d153035621 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:42:06 +01:00
jpmschweitzerandClaude Opus 4.6 f1bc79a0d2 refactor(skills): trim duplication and improve progressive disclosure
- search-docs: remove content already in CLAUDE.md (basic commands,
  endpoints, examples), keep only unique operations and workflows
- ticket: remove duplicated access method and SQL wrapper docs, add
  missing show --brief command reference
- review-pr: extract branch-type reviewer profiles to
  references/reviewer-profiles.md, reducing SKILL.md from 287 to 187
  lines while preserving all reviewer specifications
- create-skill: remove empty .gitkeep placeholders from unused
  references/ and scripts/ directories

Reviewed all skills against /create-skill conventions. Main findings
were CLAUDE.md content duplication wasting context tokens and a
progressive disclosure opportunity in the longest skill.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:41:53 +01:00
jpmschweitzerandClaude Opus 4.6 905fc764f5 fix(skills): pass file contents to non-Bash reviewer agents in review-pr
Paula, Miri, Ozzie lack Bash access and cannot git show from branches.
Skill now documents which agents can self-serve and instructs the caller
to read and paste file contents into prompts for agents that can't.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:41:07 +01:00
jpmschweitzerandClaude Opus 4.6 8734e7d835 chore(skills): add push-pr skill for safe PR lifecycle
Pushes current branch and creates or updates a PR without ever
merging into main. Prevents accidental PR merges by restricting
the skill to branch-side operations only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:27:50 +01:00
jpmschweitzer ae40d8bd2c Merge pull request 'feat(server): Sprint 3 Know — NPC model, pathfinding, routines, observation events' (#14) from server into main 2026-02-12 18:20:40 +01:00
jpmschweitzer c83d129703 Merge remote-tracking branch 'origin/main' into server
# Conflicts:
#	CHANGELOG.md
2026-02-12 18:20:32 +01:00
jpmschweitzerandClaude Opus 4.6 526696206e chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:18:17 +01:00
jpmschweitzerandClaude Opus 4.6 ad653cffc3 refactor(server): fix clippy warnings from Rust 1.93
- Use #[derive(Default)] + #[default] instead of manual Default impls
  for FacingDirection, KnowledgeState, RelationshipState, EntityVisibility
- Replace manual modulo check with .is_multiple_of()
- Collapse nested if in shadowcast symmetry check

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:18:04 +01:00
jpmschweitzerandClaude Opus 4.6 2fd16f08b0 fix(server): address PR #14 review — ordering, docs, version pin
- Fix NpcPlugin system ordering: .before(compute_paths) instead of
  .after(advance_tick) so PathRequests are picked up same frame
- Fix stale doc comment in interpretation.rs: system runs BEFORE
  knowledge events, not after
- Add TODO(v0.2) on RelationshipGraph about information boundary
  limitation for multiplayer
- Document cardinal-only movement as deliberate v0.1 choice
- Add comment on manhattan_distance u32::MAX fallback for cross-z
- Pin pathfinding crate to 4.11

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:17:55 +01:00
jpmschweitzerandClaude Opus 4.6 1761e1aa11 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:16:10 +01:00
jpmschweitzerandClaude Opus 4.6 e6bf06fb40 chore(meta): update changelog
Sprint 3 "Know" deliverables: dual lens guide, MIRROR spec, smuggler
voice card + moral arc, PC-as-NPC spec, Triangle 1 fork, content
directory structure, interaction verbs, wow moments checklist, NPC
pattern mapping, entity attribute updates, wiki maintenance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:15:55 +01:00
jpmschweitzerandClaude Opus 4.6 941547082f feat(skills): add branch-type reviewer routing to review-pr
Review-pr now selects reviewers based on branch type: server/client
get Hoshe+Tyre (code), copy gets Hoshe+Paula+Miri (narrative+lore),
visual gets Hoshe+Araminta (art direction), audio gets Hoshe+Ozzie
(player experience). Hoshe always present as QA baseline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:15:55 +01:00
jpmschweitzerandClaude Opus 4.6 d06c0e0f4e docs(design): add smuggler voice card and moral arc spec
Wave 4 deliverable for #382 (subsumes #310):
- Voice card: register parameters, 5 anchors, 4 anti-patterns, paired
  comparison examples, display constraints, authoring checklist
- Moral arc: 4 phases (Comfort→Doubt→Reckoning→Compromise), FactId
  gates, monologue trigger rules, Kael intersection mapping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:12:31 +01:00
jpmschweitzerandClaude Opus 4.6 d9448b5af0 docs(wiki): add Triangle 1 fork, PC-as-NPC spec, expand Drin to Tier 2
Wave 3 deliverables:
- Triangle 1 Hub Power Volume Escalation fork (#377): 3-path smuggler
  decision (escalate/stabilize/mediate), NPC state changes, ~41 dialogue lines
- PC-as-NPC unified spec (#381): starting knowledge/relationship graphs,
  tell inversion, orientation monologue, 9-step conversion checklist,
  v0.1 smuggler + detective briefs (29 items each)
- Drin Rosta Tier 2 expansion (#397): full 10-axis profile, 6 voice lines,
  formalized Triangle 2 + Triangle 5 roles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 18:06:07 +01:00
jpmschweitzerandClaude Opus 4.6 58c7d882bc docs(design): add dual lens guide, MIRROR spec, interaction verbs, wow moments checklist, NPC pattern mapping
Wave 2 of Sprint 3 "Know":
- #261: Dual Lens Authoring Guide (7 chapters, smuggler/detective voice registers)
- #380: THE MIRROR pattern spec with Naia Tamm reference implementation
- #396: Pattern/Motivation sections added to all 18 NPC wiki pages + index
- #400: v0.1 wow moments checklist mapping 6 D-039 moments to deliverables
- #402: Interaction verb spec (7 verbs, priority resolution, architecture)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:55:57 +01:00
jpmschweitzerandClaude Opus 4.6 03884164b3 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:52:29 +01:00
jpmschweitzerandClaude Opus 4.6 9c3c32e854 feat(perception): add observation event generator
Implements ticket #239 for Sprint 3:
- ObservationTrigger enum: RoutineDeviation (NPC not at expected
  routine location), Absence (expected location visible but NPC
  missing), NewEntity (unknown entity in LOS)
- ObservationEventQueue resource with push/drain/len/is_empty
- generate_observation_events system runs after emit_observation_events
  but before process_knowledge_events so it can compare current
  snapshot against previous-tick knowledge state
- Populate PerceptionPlugin with ObservationEventQueue resource and
  system registration with correct ordering constraints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:52:02 +01:00
jpmschweitzerandClaude Opus 4.6 0b599ed662 feat(simulation): spawn 3 NPCs with full component bundles
Implements ticket #84 for Sprint 3:
- Replace single bare NPC with 3 distinct NPCs: dock worker (16,13)
  with full 4-phase routine and MovementSpeed(2), field tech (14,18)
  with partial routine, stationary guard (18,14) with no routine
- Register all entities (player + 3 NPCs) in EntityRegistry
- Populate RelationshipGraph with colleague and rival edges
- Add NpcPlugin to app for routine system registration
- Add multi-entity visibility tests: 3 NPCs in LOS all visible,
  NPC behind wall excluded from snapshot

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:51:50 +01:00
jpmschweitzerandClaude Opus 4.6 f186cec264 fix(bridge): add error variants and diagnostic logging for IPC
Implements ticket #341 for Sprint 3:
- Add DeserializationWithDump and MutexPoisoned variants to BridgeError
- Replace .expect("mutex poisoned") with graceful error propagation
  in LocalBridge and TcpBridge (4 locations)
- Log first 256 bytes as hex dump on deserialization failure for
  debugging malformed payloads
- Classify errors in receive_bridge_inputs: BrokenPipe/ConnectionReset
  → clean shutdown, MutexPoisoned → shutdown, DeserializationWithDump
  → skip frame (recoverable)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:51:38 +01:00
jpmschweitzerandClaude Opus 4.6 f899624103 feat(simulation): add A* pathfinding and NPC path following
Implements tickets #237 and #238 for Sprint 3:
- Add pathfinding crate dependency for A* algorithm
- PathRequest component triggers compute_paths system which uses
  cardinal-neighbor A* with manhattan distance heuristic
- ComputedPath component with step navigation (next_step, advance,
  is_complete) and PathBlocked marker for no-route cases
- MovementSpeed component throttles NPC movement (ticks_per_step)
- follow_paths system advances NPCs along computed paths, creating
  MoveIntent per step; cleanup_path_blocked removes markers after
  one tick
- System ordering: input → compute_paths → follow_paths →
  validate_movement → cleanup_path_blocked → advance_tick

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:51:27 +01:00
jpmschweitzerandClaude Opus 4.6 3c04a0c568 feat(npc): add structured NPC data model, relationships, and daily routines
Implements tickets #86, #87, #88 for Sprint 3:
- Replace stub string/f32 NPC fields with typed enums and integer
  types for D-010 determinism (WantKind, SecretSeverity, Skill, etc.)
- Add RelationshipGraph global resource with BTreeMap<(StableId,
  StableId), RelationshipEdge> for efficient prefix queries
- Add DailyRoutine with phase-based RoutineEntry and PreviousDayPhase
  resource for detecting day-phase transitions
- Create NpcPlugin that initializes relationship graph, day-phase
  tracking, and registers check_phase_transition system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:51:15 +01:00
jpmschweitzerandClaude Opus 4.6 5fdb7f9c53 docs(design): add content directory structure, update entity attributes, stub Nils Davan
Wave 1 critical blockers for Sprint 3:
- #384: Content directory structure design doc (10 sections + 2 appendices,
  defines canonical IDs, 8 JSON schemas, 3-tier validation pipeline, migration path)
- #374: Update entity-attributes.md (14→16 keys, secret→leverage rename,
  add 4 role-perspective keys, archetype interpretation note)
- #376: Nils Davan off-stage NPC stub (GHOST+HANDLER pattern, lattice messages,
  Triangle 1 role, relationship to Kael)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:45:16 +01:00
jpmschweitzerandClaude Opus 4.6 64fea5139a docs(wiki): rename Hael to Naia Tamm, apply canonical full names, standardize shortcodes
Wave 0 wiki maintenance for Sprint 3:
- #372: Rename Hael → Naia Tamm across 16 living wiki files (phonetic collision fix)
- #371: Apply canonical full names to 11 single-name NPC headers and roster table
- #375: Replace hub_m_ → terminal_m_ shortcodes, add shortcode reference table,
  situation overlap rules, and mood exclusivity rules to monologue guide

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:45:04 +01:00
jpmschweitzerandClaude Opus 4.6 1333926ac2 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:38:20 +01:00
jpmschweitzerandClaude Opus 4.6 128dd0f16d feat(skills): spawn team agents from sprint briefing in start-sprint
Replace the solo plan-mode step with team orchestration: parse the
Agents line from the sprint briefing, create a team, create tasks
from tickets with dependency chains, and spawn all listed agents as
background teammates. User confirmation gates the spawn.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:38:06 +01:00
jpmschweitzerandClaude Opus 4.6 4b180c0c0d chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:15:33 +01:00
jpmschweitzerandClaude Opus 4.6 ef091de281 docs(sprints): regenerate sprint 3 briefings from database
Previous briefings referenced tickets not assigned to sprint 3.
Rewrote all four team briefings (server, client, joint, copy) to
match actual sprint 3 ticket assignments in the database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 17:15:16 +01:00
jpmschweitzerandClaude Opus 4.6 4e155da234 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:54:38 +01:00
jpmschweitzerandClaude Opus 4.6 60ca9d951c feat(db): add database backup/restore tooling
- tooling/db-backup: copies shared settledreach.db to docs/backups/
  for git tracking (main branch only)
- tooling/db-install: restores from backup for new clones
- make db-backup / make db-install Makefile targets
- worktree-update skill runs make db-backup after merges on main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:54:21 +01:00
jpmschweitzerandClaude Opus 4.6 0784d484c4 chore(config): remove $REPO_ROOT env var, use relative paths
Claude Code's CWD is always the worktree root, so all script
invocations work with plain relative paths (db/connectors/ticket,
db/connectors/qdrant-search, etc.). The REPO_ROOT environment
variable and its settings.local.json definitions are no longer needed.

Updated: CLAUDE.md, all skill files, qatux agent file.
The settings.local.json env sections were also removed across all
9 worktrees (non-git files, edited directly).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:54:12 +01:00
jpmschweitzerandClaude Opus 4.6 c21671d44c refactor(db): move ticketing database to shared worktree location
The SQLite database now lives at ../settledreach.db, shared across all
git worktrees. This eliminates binary merge conflicts that occurred
whenever two branches modified the database independently.

All Python connectors (ticket CLI, sqlite_connector, decisions_sync)
now resolve the DB path relative to their own location — three levels
up from db/connectors/ reaches the worktree parent. The _main_worktree()
git-rev-parse pattern and config.json sqlite_db key are removed.

The database file is untracked from git; a backup mechanism commits
snapshots through the main branch (see following commits).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:54:00 +01:00
jpmschweitzer ffb73a6a77 Merge branch 'copy'
# Conflicts:
#	CHANGELOG.md
#	db/commonwealth.db
2026-02-12 16:21:37 +01:00
jpmschweitzerandClaude Opus 4.6 e73e87b3e9 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:40 +01:00
jpmschweitzerandClaude Opus 4.6 b08a038990 chore(db): create 39 tickets from v0.1 content scoping workshop
38 new tickets (#371-409) + 10 existing ticket updates + 42
dependency links. Sprint 3: copy 15, server 6, client 1. Sprint 4:
18 tickets. Sprint 5+: 3 tickets. #261 is critical path root.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:23 +01:00
jpmschweitzerandClaude Opus 4.6 5925a0c79b chore(meta): add large content push team pattern to CLAUDE.md
Document the librarian + parallel writers + reviewer pattern for
producing many files efficiently with agent teams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:16 +01:00
jpmschweitzerandClaude Opus 4.6 8c95bda639 docs(workshops): add control & interaction brief, update workshop index
Add workshop brief for Control & Interaction Scheme (next workshop).
Update index with wiki-review (complete) and v0.1-content-scoping
(complete, 38 tickets).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:10 +01:00
jpmschweitzerandClaude Opus 4.6 3944b4e79f docs(workshops): archive v0.1 content scoping workshop (2 rounds + closing)
Scoped the vertical slice: 16 EntityKnowledge keys, v0.1 mechanical
NPC mapping, YAML content format, 7-verb interaction model, server-
authoritative pause, 38 tickets created across copy/server/client/ci.
20 decisions (D-042 through D-061). 8 agents, 2 rounds + closing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:19:05 +01:00
jpmschweitzerandClaude Opus 4.6 0100b33635 docs(workshops): archive wiki review workshop (4 rounds + lead interview)
Long-term content strategy workshop: 300-world generator model,
cultural ingredients menu, three-system NPC architecture (9 patterns
x 6 motivations), Sacred/Profane/Middle Kingdom framework. 9 agents
across 4 rounds plus lead interview establishing the production path
from hand-authored Sova to generated 300 worlds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:18:56 +01:00
jpmschweitzerandClaude Opus 4.6 3776443e8a docs(wiki): establish Sova Transit District game world glossary
45 files covering the v0.1 vertical slice content: 17 NPC profiles
across 3 tiers, 5 triangles, 3 social sites, 7 factions, 6 technology
entries, 3 contraband types, knowledge vocabulary (FactIds, entity
attributes, relationship states), and monologue authoring guide.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:18:49 +01:00
jpmschweitzer 679714a6ba Merge branch 'maintenance' 2026-02-12 16:18:43 +01:00
jpmschweitzerandClaude Opus 4.6 b9d62ee043 chore(db): update ticketing database
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:18:40 +01:00
jpmschweitzerandClaude Opus 4.6 a9236e2424 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:10:52 +01:00
jpmschweitzerandClaude Opus 4.6 7e6fd09770 chore(skills): add worktree-update skill
Non-destructive branch sync utility. On main: shows worktree branches
ahead of main with PR flags, lets user pick which to merge. On other
branches: merges main in. Stops on conflicts, never force-pushes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 16:10:42 +01:00
jpmschweitzerandClaude Opus 4.6 17c77c0275 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:40:20 +01:00
jpmschweitzerandClaude Opus 4.6 de25e1b8e5 chore(db): update ticketing database
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:40:08 +01:00
jpmschweitzer 178e3e34d7 Merge branch 'maintenance' 2026-02-12 15:39:35 +01:00
jpmschweitzerandClaude Opus 4.6 5af42708e9 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:37:51 +01:00
jpmschweitzerandClaude Opus 4.6 c20c1810b1 chore(config): use $REPO_ROOT env var in all instruction files
Each worktree now has REPO_ROOT pre-set in .claude/settings.local.json,
removing the need for git rev-parse --show-toplevel at runtime. Updated
CLAUDE.md, skills (start-sprint, plan-sprint, ticket, search-docs),
briefing template, and qatux agent to use the env var prefix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 15:37:38 +01:00
jpmschweitzerandClaude Opus 4.6 bb9bd6fabf chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:16:47 +01:00
jpmschweitzerandClaude Opus 4.6 6f5c62fbc1 chore(db): create Sprint 3 and assign tickets
Sprint 3: Know — 11 tickets across server, client, joint, copy.
Sprint 2 remains active until #368 completes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:14:54 +01:00
jpmschweitzerandClaude Opus 4.6 ed09439a31 docs(sprints): add Sprint 3 "Know" team briefings
NPC data model, pathfinding, observation events, and interaction
foundation. Briefings for server, client, joint, and copy teams.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:14:52 +01:00
jpmschweitzer 9d0774cae8 Merge remote-tracking branch 'origin/client' 2026-02-12 02:09:01 +01:00
jpmschweitzerandClaude Opus 4.6 1906242675 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:02:07 +01:00
jpmschweitzerandClaude Opus 4.6 68a554f1f0 fix(client): address PR #13 review — LOS tests, reset consistency, docs
- Use reset_test_state() consistently in test_snapshot_parsing.gd (Tyre critical)
- Fix misleading wall comment: (12,9) → (12,10) (Hoshe warning)
- Add coordinate space documentation to _test_snapshot() (Tyre warning)
- Add 4 Bresenham LOS unit tests: clear, blocked, diagonal, self (Hoshe warning)
- Check OS.is_process_running() in connection loop (Hoshe warning)
- Verify NPC position in corner reveal test (Hoshe suggestion)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:01:54 +01:00
jpmschweitzerandClaude Opus 4.6 ab406821d6 chore(skills): delegate plan-sprint to SI agent
Sprint planning is SI's job. Skill now spawns SI via Task tool
instead of running the workflow directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 02:00:42 +01:00
jpmschweitzerandClaude Opus 4.6 91167f489a chore(db): update ticket database
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:56:49 +01:00
jpmschweitzer bf8e8f1baa Merge remote-tracking branch 'origin/main' into server 2026-02-12 01:49:28 +01:00
jpmschweitzerandClaude Opus 4.6 84b3035945 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:49:24 +01:00
jpmschweitzerandClaude Opus 4.6 fe4d01c5d4 chore(skills): add team-agent mapping to plan-sprint skill
Defines default agents per team so sprint briefings name the
right agents to spawn. Registers Inigo for the audio team.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:49:12 +01:00
jpmschweitzerandClaude Opus 4.6 e1510a48f8 chore(agents): add Inigo sound designer agent
Standby agent for audio implementation phase. Named after the
Dreamer from the Void Trilogy. Covers soundscape design, ambient
layers, diegetic cues, and D-018 audio propagation rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:49:10 +01:00
jpmschweitzerandClaude Opus 4.6 eaab224bb0 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:47:55 +01:00
jpmschweitzerandClaude Opus 4.6 22de6c714f fix(simulation): address PR #12 review — z-level filter, visible tile dedup, version bump
- Filter remembered entities by z-level (Hoshe + Tyre warning)
- Skip remembered ghosts on currently visible tiles (Hoshe warning)
- Bump ObserverSnapshot version to 3 (Tyre suggestion)
- Add edge case tests: visible tile collision, different z-level,
  knowledge without position (Hoshe suggestion)
- Regenerate msgpack fixtures for v3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:47:35 +01:00
jpmschweitzerandClaude Opus 4.6 0db120ed58 feat(client): Sprint 2 proof E2E tests + dynamic test snapshot (#357)
Three E2E proof tests verify all acceptance criteria through the real
server pipeline: player movement, v2 snapshot with visible_tiles,
wall hiding (NPC behind wall invisible), corner reveal (move around
wall to see NPC). Dynamic test snapshot tracks player position from
queued inputs with simple LOS and Manhattan-distance visibility for
standalone demo mode. 88 client + 112 server tests passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:45:54 +01:00
jpmschweitzerandClaude Opus 4.6 a7e2c8fbf4 feat(simulation): add proof room with wall and NPC (#357)
Wall at (16,14) and NPC at (16,13) for Sprint 2 fog-of-perception
proof. Player starts at (16,16) — NPC hidden behind wall until
player moves around it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:45:47 +01:00
jpmschweitzer c8fb2a48dd Merge remote-tracking branch 'origin/main' into copy 2026-02-12 01:37:03 +01:00
jpmschweitzerandClaude Opus 4.6 bc311c9893 fix(skills): recognize all team branches in sprint skills
start-sprint and plan-sprint only recognized server/client as valid team
branches. Updated to include all six teams (server, client, copy, audio,
visual, ci) matching the SI agent's ticket assignment rules.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:36:41 +01:00
jpmschweitzerandClaude Opus 4.6 715c7e16ab docs(sprints): add copy team briefing for Sprint 2
Sprint briefing for #368 (knowledge vocabulary for v0.1 content).
Defines FactId categories, known_attributes keys, and
RelationshipState transition patterns the copy team needs to deliver.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:31:00 +01:00
jpmschweitzerandClaude Opus 4.6 8c802a2412 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:29:05 +01:00
jpmschweitzerandClaude Opus 4.6 9c0190c7c2 feat(simulation): integrate knowledge graph into observer snapshot (#366)
VisibleEntity now carries relationship state (D-033 entity color) and
observation type (Visible vs Remembered). compute_observer_snapshot
queries the player's KnowledgeGraph to overlay relationship data on
visible entities and include remembered (not-in-LOS) entities as fog
ghosts at their last known position. Regenerated msgpack fixtures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:28:53 +01:00
jpmschweitzerandClaude Opus 4.6 5c7d546f48 chore(config): add new team branches to protected list
Add copy, audio, visual, ci to protected branches in CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:26:53 +01:00
jpmschweitzerandClaude Opus 4.6 fb9d684767 chore(agents): add ticket assignment rules to SI agent
Teams are server, client, copy, audio, visual, ci. Never use
"joint" — split cross-team work into per-team tickets with
explicit dependencies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:22:33 +01:00
jpmschweitzerandClaude Opus 4.6 12dc0bc0bf chore(db): mark #130 and #360 done after PR merge
Sprint 2 progress: 19/22 tickets done.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:17:42 +01:00
jpmschweitzer 7d6503d8eb Merge remote-tracking branch 'origin/client'
# Conflicts:
#	CHANGELOG.md
2026-02-12 01:16:02 +01:00
jpmschweitzer c02b762ddc Merge remote-tracking branch 'origin/server' 2026-02-12 01:15:46 +01:00
jpmschweitzerandClaude Opus 4.6 995baf393d chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:07:19 +01:00
jpmschweitzerandClaude Opus 4.6 64897a40b3 fix(client): address PR #11 review — type safety, perf, tests
Add Dictionary validation in game_state visible_tiles loop to prevent
crash on malformed data (Hoshe critical). Skip redundant modulate.a
writes (Hoshe suggestion). Add 3 tests: malformed visible_tiles,
facing rotation accuracy, v1 backward compatibility. Clarify D-033
Phase 1/Phase 2 comments.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:07:02 +01:00
jpmschweitzerandClaude Opus 4.6 3115c7a84c fix(simulation): address PR #10 review — plugin wiring, stale reset, warnings
- Register KnowledgePlugin in main.rs and game_loop test (Tyre critical)
- Add KnowledgeGraph component to player spawn (Tyre critical)
- Reset Stale -> Active on fresh direct observation (Hoshe warning)
- Make registry/queue non-optional in emit_observation_events (Tyre/Hoshe)
- Add tracing::warn for missing EntityRegistry entries (Hoshe warning)
- Replace HashSet with Vec for small entity ID lookups (Hoshe suggestion)
- Add const static assertion for KnowledgeConfidence ordering (Hoshe)
- Add is_empty() and known_facts_iter() to KnowledgeGraph (Tyre)
- Add decay_skips_non_minute_ticks and observe_resets_stale tests (Hoshe)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 01:06:33 +01:00
jpmschweitzerandClaude Opus 4.6 30b028e85a chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:53:46 +01:00
jpmschweitzerandClaude Opus 4.6 f3793bf16e feat(client): D-033 entity colors, peripheral dimming, facing indicator (#130)
Replace hardcoded entity colors with D-033 relationship palette:
Player=#e0e8ff, Npc=unknown teal #4a9ebb, Object=grey #8b8ba0.
Phase 1 defaults by entity kind; Phase 2 (#361) will derive color
from RelationshipState via knowledge graph.

Entities in peripheral vision dimmed to 50% alpha (D-015).
Player entity gets a Polygon2D triangle indicator showing facing
direction, rotated from GameState.player_facing.

82 tests total, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:53:28 +01:00
jpmschweitzerandClaude Opus 4.6 57da75c509 feat(client): upgrade protocol decoder and GameState for v2 snapshot
Server shipped ObserverSnapshot v2 with game_time, player_facing,
visible_tiles (with visibility sectors), and per-entity visibility.
Protocol decoder was silently ignoring these fields. Now extracts
all v2 data with null defaults for backward compatibility.

GameState gains game_time, player_facing, visibility_sectors vars.
Derives visible_positions from visible_tiles when present (for real
server mode). Test snapshot updated with v2 fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:53:18 +01:00
jpmschweitzerandClaude Opus 4.6 a68a3ef429 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:49:08 +01:00
jpmschweitzerandClaude Opus 4.6 804379dee2 feat(simulation): wire direct observation to knowledge graph (#364, #367)
Perception system emits DirectObservation/LeftLOS knowledge events
based on snapshot visibility. Entities entering LOS get Direct
confidence; leaving LOS downgrades to KnowsDetails. System params
are optional for backward compatibility when KnowledgePlugin is not
registered. 28 knowledge graph unit tests included.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:48:51 +01:00
jpmschweitzerandClaude Opus 4.6 e733a4c223 feat(simulation): add knowledge graph system per D-041 (#361, #362, #363, #365)
Implement per-entity KnowledgeGraph component with BTreeMap storage
for deterministic iteration (D-010). StableEntityId + EntityRegistry
for entity identity across save/load. KnowledgeEventQueue + processing
system for decoupled knowledge updates. Decay system runs once per
game-minute. All types conform to D-041 canonical structs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:48:43 +01:00
jpmschweitzer 419f308451 Merge remote-tracking branch 'origin/maintenance' 2026-02-12 00:42:45 +01:00
jpmschweitzerandClaude Opus 4.6 2da6923189 docs(config): prefer ticket CLI over raw SQL in CLAUDE.md
Agents were writing raw SQL with incorrect column names (e.g. `sprint`
instead of `sprint_id`). Leading with the ticket CLI as the preferred
approach avoids this class of errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:39:16 +01:00
jpmschweitzer a9f1cbd865 Merge remote-tracking branch 'origin/client'
# Conflicts:
#	CHANGELOG.md
2026-02-12 00:32:30 +01:00
jpmschweitzer 4a153d31fd Merge remote-tracking branch 'origin/server' 2026-02-12 00:31:55 +01:00
jpmschweitzerandClaude Opus 4.6 6ff6be9a78 fix(client): use tick-based invalidation for world renderer
Count-based dirty tracking missed content changes when tile/visibility
count stayed the same (e.g., door opening, player moving through
corridor). Now re-renders all layers when GameState.current_tick
advances, which correctly handles every snapshot change.

Adds 3 tests for tick advancement and same-count-different-content
scenarios. 68 total tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:24:30 +01:00
jpmschweitzerandClaude Opus 4.6 d646d86212 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:21:33 +01:00
jpmschweitzerandClaude Opus 4.6 e205b38938 feat(simulation): integrate observer visibility query (#112)
Replace unfiltered generate_snapshot with compute_observer_snapshot
that combines shadowcasting + vision cone to send only visible
entities and tiles. Enforces information asymmetry (D-011): NPCs
behind walls or in the blind spot are excluded from the snapshot.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:21:07 +01:00
jpmschweitzerandClaude Opus 4.6 f760624967 feat(simulation): add vision cone with forward/peripheral/blind sectors (#111)
Implement direction-dependent visibility modulation per D-015.
Forward cone (~120 deg) at full range, peripheral (~180 deg each side)
at reduced range, blind spot (~60 deg behind) excluded. Facing component
updated on player movement via facing_from_delta.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:21:01 +01:00
jpmschweitzerandClaude Opus 4.6 3323678ddf feat(simulation): implement symmetric shadowcasting for LOS (#359, #110)
Add Albert Ford's symmetric shadowcasting algorithm using rational
fraction slopes. Benchmarked 1.2-10.5x faster than recursive with
guaranteed symmetry (if A sees B, B sees A). Resolves Q-018 as D-035.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:20:54 +01:00
jpmschweitzerandClaude Opus 4.6 bb6bc287e9 feat(simulation): add ObserverSnapshot v2 schema with visibility types (#358, #25)
Expand wire protocol with version field, GameTime (day/phase/paused),
FacingDirection (8-directional), VisibleTile, VisibilitySector
(Forward/Peripheral), and visibility tag on VisibleEntity.
Regenerate all msgpack fixtures for client compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:20:46 +01:00
jpmschweitzerandClaude Opus 4.6 aa3a3fea7d fix(client): address PR review — invalidation, constants, tests
Review feedback from Hoshe + Tyre on PR #8:

- Extract shared TILE_SIZE to Constants class_name (Tyre #5, Hoshe #7)
- Fix tile invalidation: detect tile/visibility count changes instead
  of one-shot dirty flag, supports chunk loading (Tyre #3, #4)
- Fog dirty tracking: only re-render when visible_positions changes
- Add bounds warning for unknown tile types (Hoshe #1)
- Add player-not-found warning in GameState (Hoshe #5)
- Use Image.fill_rect() instead of pixel loops (Tyre #10)
- Document _player_pos as reserved for fog decay #113 (Tyre #6)
- Add TODO(#130) for D-033 relationship colors (Hoshe #3, Tyre #8)
- Add class_name to EntityRenderer, FogRenderer, TileRenderer
- 20 new rendering tests (D-030 Layer 1): entity lifecycle, fog
  registration, tile type mapping, snapshot completeness, constants
- 65 total tests passing, 0 failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 00:15:42 +01:00
jpmschweitzerandClaude Opus 4.6 1443f9dc9e chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:48:28 +01:00
jpmschweitzerandClaude Opus 4.6 dcf887cb49 feat(client): add tile rendering, fog overlay, and camera smoothing
Sprint 2 rendering pipeline: tiles (#129), fog (#131), camera (#116).

- tile_renderer.gd: programmatic TileSet with floor/wall/door/object
  placeholder tiles, renders from snapshot tile data
- fog_renderer.gd: TileMapLayer overlay with three visibility states
  (visible/fog-edge/hidden), computed from visible_positions data
- Camera2D: smoothing enabled (speed 6.0), 2x zoom, locked to player
- game_state.gd: stores visible_tiles and visible_positions from snapshots
- sim_bridge.gd: test data with 8x8 room, corridor, and Manhattan
  distance visibility for development without server
- Scene render order: Tiles -> FogOverlay -> Entities
- Background clear color set to near-black for unexplored areas

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:48:02 +01:00
jpmschweitzerandClaude Opus 4.6 55b775ee73 fix(client): fix entity renderer to match protocol format
entity_renderer.gd used wrong field names ("id" instead of
"entity_id", "type" instead of "kind.variant", "position" instead
of x/y). Now matches Protocol.decode_entity() output exactly.
Also centers entities (24x24) within 32px tiles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:49 +01:00
jpmschweitzer 3de9c29073 Merge branch 'maintenance' 2026-02-11 23:32:02 +01:00
jpmschweitzerandClaude Opus 4.6 7b81871ba4 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:31:24 +01:00
jpmschweitzerandClaude Opus 4.6 5805d17f0b chore(skills): require plan mode in start-sprint skill
Replace immediate ticket status change with a mandatory plan mode
step. Agent must now enter plan mode, create a concrete sprint plan
covering implementation approach and ticket ordering, and get user
approval before marking any ticket as in_progress.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:31:10 +01:00
jpmschweitzerandClaude Opus 4.6 93abd9b9d0 docs(workshops): complete Knowledge Graph & Information Boundaries workshop
Two-round workshop producing D-041 (Knowledge Graph Data Model):
- Round 1: independent analyses from Dudley, Gestalt, SI, Tyre, Paula
- Round 2: synthesis resolving debates + Gestalt mechanics validation

Key decisions:
- 4-level confidence hierarchy (Suspects < KnowsOf < KnowsDetails < Direct)
- BTreeMap for deterministic iteration (D-010 principle 4)
- Per-entity Component model, not centralized Resource
- StableEntityId + EntityRegistry for save/load stability (partial Q-019)
- Sprint 2 stub: structs + direct observation + basic decay (~6.5 dev-days)

Resolved Q-016 (knowledge hierarchy), raised Q-024/Q-025/Q-026.
Created tickets #361-#368 under epic #351, reconciled #49 children.
Updated sprint 2 briefings, agent briefings, and decision files.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:24:37 +01:00
jpmschweitzer 6580346c75 Merge branch 'main' into client 2026-02-11 23:03:32 +01:00
jpmschweitzerandClaude Opus 4.6 c065d98325 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:02:27 +01:00
jpmschweitzerandClaude Opus 4.6 edfb1bb93d feat(db): add team column to ticket system
Adds a comma-separated team field to tickets so work can be
attributed to server, client, or joint teams. Includes --team
filter on list, --team on create, and a new team subcommand.
Schema, CLI, and skill docs all updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:02:06 +01:00
jpmschweitzer 70a7d6527d Merge branch 'main' into client 2026-02-11 22:57:23 +01:00
jpmschweitzerandClaude Opus 4.6 811a5f8a4c chore(config): track .claude/settings.json in version control
Settings.json is the shared team config (distinct from settings.local.json
which remains per-user). Remove it from .gitignore so it's tracked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:55:56 +01:00
jpmschweitzerandClaude Opus 4.6 1883a6a6d4 docs(sprints): add updated joint briefing for sprint 2
Joint tasks with ticket IDs (#356-#360), dependency chains,
and sprint completion proof criteria for client team reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:34:24 +01:00
jpmschweitzerandClaude Opus 4.6 160add9719 docs(sprints): update sprint 2 briefings with ticket IDs and dependencies
Server team added #356-#360 for pre-sprint design tasks and
integration milestones. Updated server.md and joint.md with
actual ticket numbers, dependency chains, and done status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:30:05 +01:00
jpmschweitzerandClaude Opus 4.6 79603c9a7f chore(db): add sprint 2 pre-sprint tickets and update joint briefing
Server team added #356-#360: fog bridge, sprint proof, snapshot
v2 schema, Q-018/Q-019 resolution tasks. Updated joint.md with
actual ticket numbers and #81 done status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:24:09 +01:00
jpmschweitzer 45ad3cdf0c Merge remote-tracking branch 'origin/maintenance' 2026-02-11 22:20:10 +01:00
jpmschweitzerandClaude Opus 4.6 8c5a022cfd chore(db): activate sprint 2 "See" with 9 tickets
Server: #110 shadowcasting, #111 vision cone, #112 observer
query, #25 game clock. Client: #116 camera lock, #129 tile
rendering, #130 entity sprites, #131 fog overlay, #113 fog
rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:17:31 +01:00
jpmschweitzerandClaude Opus 4.6 468aad60d4 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:11:44 +01:00
jpmschweitzerandClaude Opus 4.6 39f85dfb3a fix(skills): use absolute paths in start-sprint for worktree safety
The skill used bare relative paths like db/connectors/ticket which
fail when the shell CWD doesn't match the worktree root. Now requires
resolving REPO_ROOT via git rev-parse --show-toplevel first.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:11:34 +01:00
jpmschweitzerandClaude Opus 4.6 08e794086e chore(db): close sprint 1 and mark epic #37 done
Sprint 1 "Run" completed — 8/8 tickets done. Player-controlled
character moving on screen through working IPC bridge achieved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:10:27 +01:00
jpmschweitzerandClaude Opus 4.6 deb834e64a chore(db): mark sprint 1 tickets #81 and #236 as done
E2E connection test (#81) merged via PR #7. Tile collision
system (#236) already on main. Sprint 1 now 7/8 complete.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 22:08:32 +01:00
jpmschweitzer fcd16433c8 Merge remote-tracking branch 'origin/client'
# Conflicts:
#	CHANGELOG.md
2026-02-11 22:04:04 +01:00
jpmschweitzerandClaude Opus 4.6 beee44b3c8 feat(client): add batch fixture, fixture smoke test, decode_errors test
Tyre #1: Added Rust-generated input_batch_two.msgpack fixture for
bidirectional D-030 Layer 1 symmetry (Vec<PlayerInput>).
Tyre #2: Added all_fixtures_deserialize Rust test that reads every
.msgpack fixture and verifies it deserializes (corruption guard).
Hoshe #3: Added test_decode_snapshot_malformed_entities_counted test
verifying the decode_errors counter on D-010 boundary violations.
45 client tests, 54 server tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:57:18 +01:00
jpmschweitzerandClaude Opus 4.6 8d12776b5d fix(client): address PR #7 review — ephemeral port, input drop docs
Hoshe #1: E2E test now uses random ephemeral port (49152-65535) with
port rotation on bind failure, avoiding conflicts in parallel CI.
Hoshe #2: Documented intentional input drop on encode failure in
SimBridge — re-queuing would retry bad data and server tick has
already advanced.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:57:11 +01:00
jpmschweitzerandClaude Opus 4.6 a54a3a8100 chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:46:01 +01:00
jpmschweitzerandClaude Opus 4.6 f160f07e6d fix(skills): remove references to nonexistent init/package scripts
Replace init_skill.py and package_skill.py references with manual
mkdir and validation checklist — the scripts were never created.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:45:42 +01:00
jpmschweitzerandClaude Opus 4.6 c35248f103 chore(skills): add plan-sprint skill for sprint briefing generation
Automates the sprint planning workflow: gather carry-overs, scan
backlog, propose tickets, write per-team briefing files. Includes
briefing template reference matching the established format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:45:40 +01:00
jpmschweitzerandClaude Opus 4.6 7f97cb7ce3 docs(sprints): add Sprint 2 "See" briefings for server, client, joint
Sprint 2 goal: fog of perception working through the bridge. Server
computes LOS visibility, client renders fog. Carries over #236 and #81
from Sprint 1.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:45:38 +01:00
jpmschweitzerandClaude Opus 4.6 d96a0599b6 feat(db): add multi-ID show and --brief flag to ticket CLI
ticket show now accepts multiple IDs (e.g. ticket show 110 111 112).
--brief flag outputs compact human-readable summaries instead of JSON,
useful for sprint planning scans. Single-ID JSON output unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:45:35 +01:00
jpmschweitzerandClaude Opus 4.6 c737d687aa chore(meta): update changelog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:44:51 +01:00
jpmschweitzerandClaude Opus 4.6 5e4b8dbcf3 feat(client): add E2E connection test and batch encoding tests
D-030 Layer 3: E2E test spawns the Rust server binary, connects via
LocalBridge, sends a batched MoveNorth input, and verifies the player
moved to (16.5, 15.5). Also adds batch encoding roundtrip tests,
framed batch test, and Player entity fixture decode test. 43/43 pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:44:27 +01:00
jpmschweitzerandClaude Opus 4.6 e1e4e346c7 feat(client): add EntityKind::Player fixture and update multi-entity fixture
Server team added EntityKind::Player variant. Added snapshot_player
fixture and updated snapshot_multi_entity to include all 4 entity kinds
(Player, Npc, Object, Terrain) for complete D-030 Layer 1 coverage.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:44:20 +01:00
jpmschweitzerandClaude Opus 4.6 2338941721 fix(client): batch-encode inputs as Vec<PlayerInput> per server wire format
Server expects a MessagePack array of PlayerInput objects in one framed
message per tick, not individual inputs per frame. Added
Protocol.encode_player_inputs() for batch encoding. Changed SimBridge to
buffer raw input dicts and batch-encode in _process(). Also fixed server
port default (9876) and positional arg format to match server CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 21:44:14 +01:00
770 changed files with 145908 additions and 912 deletions
+39
View File
@@ -0,0 +1,39 @@
---
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.
tools: Read, Glob, Grep, Edit, Write, Bash
model: sonnet
memory: project
---
You are INIGO, the Sound Designer on a game development team building a top-down immersive sim set in an original science fiction universe.
## Your personality
You think in layers. Every space has a base tone, a rhythm, and the things that break the rhythm. You hear what's missing as much as what's present. You say things like "What does silence sound like here?" and "That sound tells the player something before they see it" and "Ambience is information." You understand that in an immersive sim, sound is the player's second sight — what they hear around corners, through walls, behind closed doors. Sound is gameplay.
Named after Inigo from the Void Trilogy — the Dreamer who shared immersive multi-sensory experiences with billions. He understood that atmosphere isn't decoration, it's the thing that makes a world feel inhabited.
## Your role on the team
- Design the soundscape layers for each location (Logistics Hub, Bar, Ring spaces)
- Define ambient audio: station hum, crowd murmur, machinery, ventilation, distant announcements
- Specify diegetic sound cues that carry gameplay information (D-018 sound model)
- Design audio behavior for the sound propagation system (close/medium/long range per D-018)
- Define what sounds travel through walls, around corners, and across z-levels
- Create sound palettes per location and time-of-day
- Specify audio tells for NPC behavior (footsteps, conversations, alerts)
- Work with Gestalt to ensure sound cues map to mechanical significance
- Ensure audio reinforces the information asymmetry core mechanic — what you hear but can't see matters
## Design principles
- **Sound is information**: Every ambient sound should either establish atmosphere or communicate game state. No filler noise.
- **Diegetic first**: The player hears what their character hears. No non-diegetic audio cues for gameplay events (music is the exception).
- **Layers, not walls**: Soundscapes are built from distinct layers (base ambience, activity, events) that can be independently controlled.
- **Silence is a sound**: The absence of expected sound (a machine stopping, a crowd going quiet) is as meaningful as any cue.
- **Range matters**: Sound propagation (D-018) is a core mechanic. Design sounds with their falloff and occlusion behavior in mind.
## Project context
Read your briefing at `docs/briefings/inigo.md` before starting work.
+1 -1
View File
@@ -44,7 +44,7 @@ Named after Qatux, the Raiel with perfect memory who helped Paula Myo by recalli
- **Work in dedicated round files:** All new rounds happen in `docs/discussions/round-NN-topic.md` from the start. DISCUSSION.md is retired for new content.
- **Update the discussion index ONLY when closing:** After a round is formally closed, update `docs/discussions/README.md` with the round entry (number, topic, decisions produced, file link).
- **Update briefings:** After a round produces new decisions, update the relevant agent briefing files in `docs/briefings/`.
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `python3 db/connectors/qdrant_connector.py index-file <path>`.
- **Re-index documents:** After archiving or updating documents, re-index them in Qdrant via `db/connectors/qdrant-index <path>`.
## Team workflow (mandatory)
+10
View File
@@ -29,6 +29,16 @@ Named after the Sentient Intelligences that manage all Commonwealth infrastructu
You are execution-focused. When a decision is made, you immediately think about what needs to happen, in what order, by whom, and what depends on what. You maintain the project's pulse - always knowing what's in progress, what's blocked, and what's next. You don't wait to be asked for status updates; you surface risks early.
## Ticket assignment rules
The development teams are: **server**, **client**, **copy**, **audio**, **visual**, **ci**.
When creating or splitting tickets:
- **Always assign a ticket to exactly one team.** Every ticket must have a team.
- **Never use "joint" as a team.** If work spans multiple teams, split it into separate tickets — one per team — with explicit dependencies between them.
- For example, a feature requiring server-side logic and client-side rendering becomes two tickets: one for server (implement the data/system), one for client (consume and render it), with the client ticket blocked by the server ticket.
- Sprint proof/acceptance tickets should be assigned to the team responsible for the final integration step, with blockers on the upstream tickets.
## Project context
Read your briefing at `docs/briefings/si.md` before starting work.
+65
View File
@@ -0,0 +1,65 @@
{
"env": {
"CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS": "1"
},
"teammateMode": "in-process",
"permissions": {
"allow": [
"Bash(git add *)",
"Bash(git commit *)",
"Bash(git push *)",
"Bash(git fetch *)",
"Bash(git merge *)",
"Bash(git pull *)",
"Bash(git status *)",
"Bash(git log *)",
"Bash(git diff *)",
"Bash(git show *)",
"Bash(git checkout *)",
"Bash(git stash *)",
"Bash(git branch *)",
"Bash(git worktree *)",
"Bash(git config *)",
"Bash(git mv *)",
"Bash(git rm *)",
"Bash(git ls-tree *)",
"Bash(git rev-parse --show-toplevel)",
"Bash(db/connectors/ticket *)",
"Bash(db/connectors/sprint *)",
"Bash(db/connectors/sqlite-query *)",
"Bash(db/connectors/sqlite-exec *)",
"Bash(db/connectors/qdrant-search *)",
"Bash(db/connectors/qdrant-index *)",
"Bash(db/connectors/qdrant-health)",
"Bash(db/connectors/qdrant-count)",
"Bash(db/connectors/sqlite-init)",
"Bash(db/connectors/decisions-sync)",
"Bash(db/connectors/audio-generate *)",
"Bash(db/connectors/audio-health)",
"Bash(db/connectors/audio-post *)",
"Bash(make *)",
"Bash(make)",
"Bash(tea *)",
"Bash(chmod *)",
"Bash(ls *)",
"Bash(find *)",
"Bash(list *)",
"Bash(tree *)",
"Skill(commit)",
"Skill(worktree-update)",
"Skill(start-sprint)"
],
"deny": [
"Bash(git push --force *)",
"Bash(git reset --hard *)",
"Bash(git clean -f *)",
"Bash(rm -rf *)"
]
}
}
+16 -18
View File
@@ -149,14 +149,14 @@ Establish a list of reusable resources: scripts, references, and assets.
### Step 3: Initialize the Skill
Run the init script to generate a template skill directory:
Create the skill directory manually:
```
scripts/init_skill.py <skill-name> --path <output-directory>
mkdir -p <output-directory>/<skill-name>
```
Creates: skill directory, SKILL.md template with TODO placeholders, example
resource directories with sample files.
Then create `SKILL.md` with frontmatter and body. Add `scripts/`, `references/`,
and `assets/` subdirectories only as needed.
Skip if iterating on an existing skill.
@@ -178,26 +178,24 @@ For design patterns, consult:
**Writing guidelines:** Always use imperative/infinitive form.
**Frontmatter:**
- `name`: The skill name
- `name`: The skill name — use **domain-action** naming: `{domain}-{action}`.
The domain is the system/area the skill operates on, the action is what it does.
Examples: `pr-review`, `sprint-plan`, `docs-search`, `git-commit`, `debt-scan`.
Multi-action wrappers (like `ticket`) can use the domain name alone.
The directory name must match the `name` field.
- `description`: Primary triggering mechanism. Include what the skill does AND
specific triggers/contexts. All "when to use" info goes here (not in body).
**Body:** Instructions for using the skill and its bundled resources.
### Step 5: Package the Skill
### Step 5: Validate the Skill
```
scripts/package_skill.py <path/to/skill-folder>
```
Optional output directory:
```
scripts/package_skill.py <path/to/skill-folder> ./dist
```
The script validates (frontmatter, naming, description, file organization) then
packages into a `.skill` file (zip with .skill extension). Fix any validation
errors and re-run if needed.
Check the skill manually:
- Frontmatter has `name` and `description`
- SKILL.md body is under 500 lines
- No extraneous files (README.md, CHANGELOG.md, etc.)
- Scripts are executable and tested
- References are referenced from SKILL.md
### Step 6: Iterate
+187
View File
@@ -0,0 +1,187 @@
---
name: gen-audio
description: >
Generate audio assets for The Settled Reach using the Stable Audio Open API
(self-hosted Gradio app at tower-of-joy:11500). Use when generating any game
audio: ambient loops, SFX, UI sounds, monologue chimes, footsteps, or any
sound asset from docs/assets/audio/. Also use when the user asks about audio
generation, sound design pipeline, or audio asset iteration. Triggers on:
"generate audio", "make sounds", "create ambient", "audio pipeline",
"generate sfx", "stable audio", "gen audio", "sound design".
---
# Audio Generation — The Settled Reach
Generate sonically consistent audio assets using the Stable Audio Open API via
wrapper scripts at `db/connectors/audio-*`.
Asset descriptions, filenames, bus routing, and design intent are documented in
`docs/assets/audio/`. This skill provides the prompt system, generation
workflow, and quality validation.
## API Access
**Never call the API directly.** Use the wrapper scripts:
```bash
# Check API health
db/connectors/audio-health
# Generate audio
db/connectors/audio-generate "prompt text" \
--duration 10 \
--steps 100 \
--cfg 7 \
--output path/to/output.wav \
--timeout 600
```
### Parameters
| Parameter | Default | Range | Notes |
|-----------|---------|-------|-------|
| `--duration` | 10 | 0-47s | Max 47s per generation. For longer loops, generate 45s with crossfade overlap. |
| `--steps` | 100 | 10-200 | More steps = better quality, slower. Use 50 for quick previews, 100-150 for final. |
| `--cfg` | 7 | 1-15 | Classifier-free guidance. Higher = more prompt-adherent but less natural. 5-9 is the sweet spot. |
| `--output` | auto | — | Output file path. Auto-names from prompt if omitted. |
| `--timeout` | 600 | — | Max wait in seconds. Generation can take 2-5 minutes on 11GB VRAM. |
### Critical Constraints
- **NEVER parallelize requests.** The server has 11GB VRAM and runs one
generation at a time. Always wait for a generation to complete before
starting the next. Sequential only.
- **Generation takes 2-5 minutes** per clip depending on duration and steps.
Be patient. The timeout default (600s) is generous.
- **Max 47 seconds** per generation. For 60-90s ambient loops, generate 45s
clips and crossfade-stitch in post-processing.
- **Output is WAV at 44.1kHz stereo.** Convert to .ogg for Godot import:
`ffmpeg -i input.wav -c:a libvorbis -q:a 6 output.ogg`
## Prompt Assembly
Every generation uses three parts:
```
[SONIC FAMILY PREFIX] + [CATEGORY TEMPLATE] + [ASSET DESCRIPTION from docs/assets/audio/]
```
Never call the API with just the asset description. Always prepend the sonic
family prefix and matching category template.
- **Sonic palette and families:** Read `references/sonic-palette.md`
- **Category templates:** Read `references/category-templates.md` and match by
asset type (ambient, sfx, ui)
- **Asset description:** Look up the specific asset in `docs/assets/audio/{category}.md`
## Single Asset Workflow
1. Find the asset in `docs/assets/audio/{ambient,sfx,ui}.md` — note filename,
duration, bus, method, and design intent.
2. Read `references/sonic-palette.md` for the sonic family prefix.
3. Read `references/category-templates.md` for the matching template.
4. Assemble the full prompt.
5. Run `db/connectors/audio-health` to verify the API is up.
6. Run `db/connectors/audio-generate` with the assembled prompt. **One request
at a time. Wait for completion.**
7. Listen to the output (or describe it based on file size/duration).
8. If acceptable, convert to .ogg and place in `client/assets/audio/`.
9. Update the asset status in `docs/assets/audio/{category}.md`.
## Iteration Workflow
For each asset, generate 4-6 candidates:
1. **Generate candidates** — vary the prompt slightly (add/remove descriptors,
adjust CFG between 5-9). Run each generation sequentially — never in
parallel.
2. **Solo test** — does each candidate sound right alone?
3. **Stack test** — play the candidate alongside other layers. Does it mask or
clash?
4. **Fatigue test** (loops only) — can you listen for 5+ minutes without a
jarring repeat?
5. **Close-your-eyes test** — does it create a mental image or sensation?
6. Select the best candidate, trim, normalize, convert.
## Post-Processing
After selecting the best generation:
```bash
# Trim silence from start/end
ffmpeg -i input.wav -af "silenceremove=start_periods=1:start_silence=0.1:start_threshold=-50dB,areverse,silenceremove=start_periods=1:start_silence=0.1:start_threshold=-50dB,areverse" trimmed.wav
# LUFS normalize to -16 LUFS (broadcast standard, good for game audio)
ffmpeg -i trimmed.wav -af loudnorm=I=-16:LRA=11:TP=-1 normalized.wav
# Convert to .ogg for Godot
ffmpeg -i normalized.wav -c:a libvorbis -q:a 6 output.ogg
# For loops: verify loop point
ffplay -loop 0 output.ogg
```
For ambient loops, create crossfade overlap:
```bash
# Create a 45s loop with 3s crossfade overlap
# (manual: export 48s, crossfade first 3s with last 3s in Audacity)
```
## Manual Synthesis (Insert-Tech Sounds)
For sounds under 200ms (cursor hover, weapon aim), Stable Audio Open cannot
produce meaningful output. Use manual synthesis instead:
```python
# Example: 50ms cursor hover tick
import numpy as np
import wave
sr = 44100
duration = 0.05 # 50ms
t = np.linspace(0, duration, int(sr * duration), endpoint=False)
freq = 3200 # Hz
signal = np.sin(2 * np.pi * freq * t)
envelope = np.exp(-t * 80) # exponential decay
audio = (signal * envelope * 32767).astype(np.int16)
with wave.open("cursor_hover.wav", "w") as f:
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(sr)
f.writeframes(audio.tobytes())
```
## Quality Checklist
After generating, verify:
- Sound matches the sonic family (insert-tech = synthetic/precise, organic = warm/natural)
- Frequency range doesn't mask other layers (check docs/assets/audio/palette.md)
- Duration matches spec
- No unwanted artifacts (clicks, pops, digital noise at start/end)
- Loop point is clean (ambient loops only)
- Volume sits well relative to other assets (LUFS normalized)
- Passes the close-your-eyes test
## File Placement
Generated assets go to `client/assets/audio/` with exact filenames from the
asset docs:
```
client/assets/audio/
amb_station_base.ogg # Ambient bus
amb_workplace_layer.ogg # Ambient bus
amb_bar_layer.ogg # Ambient bus
amb_corridor_layer.ogg # Ambient bus
sfx_footstep_metal.ogg # Player Actions bus
sfx_footstep_metal_run.ogg # Player Actions bus
cursor_hover.ogg # UI Sounds bus
implant_open.ogg # UI Sounds bus
fog_recognition.ogg # UI Sounds bus
weapon_aim.ogg # UI Sounds bus
sfx_monologue_chime.ogg # UI Sounds bus
sfx_monologue_chime_urgent.ogg # UI Sounds bus
```
AudioManager discovers these by directory scan — filenames must match exactly.
@@ -0,0 +1,67 @@
# Category Templates — Audio Generation
Match the asset's category to the right template. Append after the sonic family
prefix and before the specific asset description.
## Ambient Loops
**Template:**
> Continuous ambient soundscape loop, seamless looping audio, no distinct
> beginning or end, steady background atmosphere, [DURATION]s duration
**Notes:**
- Generate at 45s (the SAO sweet spot below the 47s ceiling)
- Add 3-5s of overlap material for crossfade looping in post
- These play continuously — they must be boring enough to fade into the
background but rich enough to reward attention
- Station baseline prefix is required (see sonic-palette.md)
**Steps:** 100-150 (higher quality for loops — artifacts are more noticeable)
**CFG:** 5-7 (lower guidance = more natural variation, less mechanical)
## Sound Effects (SFX)
**Template:**
> Single isolated sound effect, clean recording, [DURATION] duration,
> clear attack and natural decay, no background noise, no reverb tail
> beyond natural
**Notes:**
- For footsteps: specify surface material, pace, weight
- For environmental: specify the physical mechanism (door hinge, cargo latch)
- Keep short and punchy — these are event-driven, not continuous
**Steps:** 80-100 (shorter sounds need fewer steps)
**CFG:** 7-9 (more guidance = more precise sound matching)
## UI Sounds
**Template:**
> Interface feedback sound, digital UI element, [DURATION] duration,
> immediate attack, clean decay, isolated sound with no background
**Notes:**
- Most UI sounds are under 200ms — use manual synthesis, not SAO
- For SAO-generated UI sounds (>200ms like fog_recognition, chimes):
generate at 3-5s duration, then trim to the best portion
- Insert-tech prefix is required for lattice/interface sounds
- Organic prefix for fog_recognition (cognitive, not technological)
**Steps:** 80-100
**CFG:** 7-9 (precision matters for UI sounds)
## Chimes / Notification Sounds
**Template:**
> Single musical tone, crystalline quality, [DURATION] duration, gentle
> attack, resonant sustain, natural fade out, no accompaniment, no rhythm
**Notes:**
- Generate at 3-5s to give SAO enough temporal context
- Trim to the best 0.5-1.0s section
- The attack transient is where the character lives — select for that
- Normal vs urgent variants: same base prompt, vary "gentle/soft" vs
"brighter/sharper/more present"
**Steps:** 100 (tonal quality matters)
**CFG:** 6-8 (some freedom for natural harmonic content)
@@ -0,0 +1,45 @@
# Sonic Palette — Prompt Prefixes
Two sonic families. Every generated sound uses one of these as a prompt prefix.
## Insert-Tech (Synthetic)
For UI sounds, neural lattice interface, augmented cognition.
**Prompt prefix:**
> Clean digital audio, synthetic electronic tone, precise and clinical sound
> design, no reverb, no room ambience, studio-dry recording, futuristic
> interface sound
**Character:** Mathematical precision. The sound of well-designed technology.
No organic texture, no room reflections. Exists "inside the head," not in
physical space.
**Frequency range:** 800Hz-4kHz primary. Sharp attack, controlled decay.
**Used for:** cursor_hover, implant_open, weapon_aim, sfx_monologue_chime,
sfx_monologue_chime_urgent
## Organic (Biological/Environmental)
For environmental sounds, human cognition, physical world.
**Prompt prefix:**
> Warm organic audio, natural sound recording, slight room ambience,
> lived-in industrial space, realistic acoustic properties, authentic
> environmental sound
**Character:** Human warmth. Natural attack and decay. Reverberant — exists
in physical space. The sound of a real place with real materials.
**Frequency range:** 60Hz-3kHz primary. Soft or natural attack, room tail.
**Used for:** fog_recognition, all amb_* ambient loops, all sfx_footstep_*
## Station Baseline
For ambient loops specifically, add the station context:
> Interior of a large industrial space station, metal and composite
> construction, background mechanical hum from power systems, air circulation
> audible, no music, no prominent voices unless specified
@@ -1,5 +1,5 @@
---
name: asset-gen
name: gen-image
description: >
Generate themed visual assets for the Lords of Ash CK3 total conversion mod
using the generate_image MCP tool (Nano Banana / Gemini 2.5 Flash Image).
+173
View File
@@ -0,0 +1,173 @@
---
name: plan-sprint
description: >
Plan the next sprint and generate team briefing files. Use when the user says
"plan sprint", "prep sprint briefing", "plan next sprint", "sprint planning",
or invokes /plan-sprint. Gathers current sprint status, scans the backlog,
proposes ticket selection, and writes briefing files per team to
docs/sprints/sprint-N/.
user-invocable: true
allowed-tools: Task, Read, Grep, Glob
---
# Plan Sprint
**Delegate this entire skill to SI** (Project Manager agent, `subagent_type: si`).
When this skill is invoked, spawn SI using the Task tool:
```
Task(
subagent_type: "si",
prompt: "Run /plan-sprint for Sprint N. Read the skill at
.claude/skills/plan-sprint/SKILL.md for the full workflow,
then execute it. Use the arguments provided: {args}",
description: "Plan sprint N"
)
```
Pass through any arguments the user provided (e.g. sprint number).
Present SI's output to the user when done. Do NOT run the workflow yourself.
---
The remainder of this file is SI's reference for executing the workflow.
Generate sprint briefing files for each active team (e.g. `server.md`,
`client.md`, `copy.md`, `joint.md`) for the next sprint based on current
project state. Only generate briefings for teams that have tickets in the sprint.
## Teams and Default Agents
| Team | Branch | Default Agents | Focus |
|------|--------|----------------|-------|
| `server` | `server` | Dudley (dev), Tyre (arch), Hoshe (QA) | Rust/bevy_ecs simulation, ECS systems, knowledge graph, perception |
| `client` | `client` | Stig (dev), Tyre (arch), Hoshe (QA) | Godot 4 client, rendering, UI, HUD, input handling |
| `copy` | `copy` | Mellanie (author), Paula (narrative), Gestalt (systems) | Dialogue, monologue, UI text, knowledge vocabulary, lore |
| `audio` | `audio` | Inigo (sound design) | Soundscapes, ambient layers, diegetic cues, audio propagation |
| `visual` | `visual` | Araminta (art direction) | Art assets, sprites, visual consistency, style guides |
| `ci` | `ci` | Justine (build/deploy) | Build pipelines, CI/CD, tooling, packaging |
When writing briefings, name the assigned agents in the **Agents** line of each
file so the team knows who to spawn.
## Workflow
### 1. Run sprint prepare
Get carry-overs, backlog candidates, and decision gaps in one shot:
```bash
db/connectors/sprint prepare
```
This auto-detects the next sprint number (max ID + 1), creates the sprint
record in `planning` status if needed, and outputs:
- Previous sprint status and carry-over candidates
- Backlog candidates grouped by team
- Decision coverage gaps
- Already-assigned tickets (if any)
### 2. Deepen the scan
For critical epics, check their children for granular candidates:
```bash
db/connectors/ticket children <epic_id>
```
Use `db/connectors/ticket show --brief <id> [<id>...]` to quickly scan multiple tickets.
### 3. Read existing code state
Scan what's already built to write accurate "what exists" notes:
```bash
# Server modules
ls server/src/ server/src/*/
# Client scripts
ls client/scripts/ client/scripts/*/
```
Read key files that sprint tickets will build on (bridge types, existing
renderers, etc.) to reference specific integration points in the briefing.
### 4. Select tickets — propose to user
Based on the backlog scan, propose a sprint with:
- **Sprint theme** — a short name (Sprint 1 was "Run", Sprint 2 was "See")
- **Sprint goal** — one sentence shared across all teams
- **Server tickets** — stories from server-side epics
- **Client tickets** — stories from client-side epics
- **Joint tasks** — integration proofs, pre-sprint decisions, schema work
Present the proposal using AskUserQuestion for the user to approve or adjust.
Selection heuristics:
- Follow dependency chains (don't pick a ticket if its blocker isn't in scope)
- Respect D-030 test priority phases (sprint 1-2: infra, sprint 3-4: integration)
- Mix carry-overs with new work
- Aim for 3-6 tickets per team, with parallel tracks where possible
- Check `decisions/questions.md` for open Q-NNN items that block candidates
### 5. Read relevant decisions
For the selected tickets, identify which `decisions/*.md` files are relevant.
Read them to provide accurate cross-references in the briefing.
### 6. Write briefing files
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:
`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.
Key requirements per file:
- **server.md**: Carry-overs, new tickets, dependency chain, key decisions, notes
referencing existing Rust modules by path from worktree root (e.g. `server/src/simulation/movement.rs`)
- **client.md**: Same structure, notes referencing existing GDScript files by path from worktree root (e.g. `client/scripts/rendering/fog.gd`)
- **copy.md**: In-game text tasks — dialogue, UI copy, tooltips, flavor text, lore
- **audio.md**: Sound design, music, audio integration tasks
- **visual.md**: Art direction, asset creation, visual consistency tasks
- **ci.md**: Build pipeline, CI/CD, tooling, infrastructure tasks
- **joint.md**: Pre-sprint decisions table, integration tickets, sprint
completion proof (concrete observable criteria), test plan alignment
Only generate briefing files for teams that have tickets assigned in the sprint.
Not every sprint will have work for every team.
### 7. Assign tickets to sprint in DB
After the user approves, assign all selected tickets. The sprint record
was already created by `sprint prepare` in step 1 (status: `planning`).
Update it with the theme and goal, then assign tickets:
```bash
# Update the sprint with theme and goal
db/connectors/sqlite-exec "UPDATE sprints SET name='Sprint N: Theme', goal='goal' WHERE id=N"
# Assign tickets
db/connectors/ticket sprint assign <ticket_id> <sprint_id>
```
The sprint stays in `planning` status until explicitly activated via
`db/connectors/sprint start`. This prevents starting an unplanned sprint.
### 8. Present summary
Output:
- Sprint number, theme, and goal
- Ticket count per team
- Carry-over count
- Open questions that need early resolution
- Files written
@@ -0,0 +1,79 @@
# Sprint Briefing Template
Each team gets one briefing file at `docs/sprints/sprint-N/<team>.md`.
## File structure
```markdown
# Sprint N: <Theme> — <Team> Tasks
**Goal:** <One-sentence sprint goal, shared across all teams>
**Branch:** `<team>`
**Agents:** <Agent names and roles>
## Carry-over from Sprint N-1
(Only if there are incomplete tickets from the previous sprint)
| # | Title | Status | Notes |
|---|-------|--------|-------|
| #ID | Title | status | Why it carried over |
## New Tickets
| # | Title | Blocked by |
|---|-------|------------|
| #ID | Title | #dependency or — |
Use `db/connectors/ticket show <id>` for full details.
## Key Decisions
- `decisions/<domain>.md` — D-NNN (short name), D-NNN (short name)
## Open Questions to Resolve Early
(Only if there are Q-NNN items that block sprint tickets)
- **Q-NNN: Title** — Brief context. Resolve before #ID starts.
## Notes
One bullet per ticket with:
- What exists already (files, modules, stubs)
- What the ticket actually needs to deliver
- Integration points with other tickets
- Non-obvious gotchas
## Dependency Chain
```
#A (name) → #B (name) → #C (name)
#D (name) → standalone, parallel track
```
## PR Workflow
When ready to submit, create a PR with `tea` CLI. **All flags are required** to avoid TTY prompts (see CLAUDE.md "Gitea access" section):
\```bash
tea pr create --repo jpmschweitzer/settled-reach --login schweitz --title "feat(<scope>): description" --description "body" --base main --head <branch>
\```
```
## Joint briefing extras
The `joint.md` file additionally includes:
- **Pre-Sprint** table: decisions/schema work that must happen before implementation
- **Sprint Completion Proof**: concrete observable criteria (what you can see/do when the sprint is done)
- **Test plan** alignment with D-030 phases
## Team assignments
| Team | Branch | Agents | Scope |
|------|--------|--------|-------|
| server | `server` | Dudley (simulation), Oscar (networking) | Rust/bevy_ecs simulation |
| client | `client` | Stig (UI), Oscar (networking) | Godot client rendering |
| joint | both | All implementation agents | Integration, proofs, cross-team schema |
| content | (none) | Mellanie, Paula, Miri, Araminta | Content authoring, no code branch |
+128
View File
@@ -0,0 +1,128 @@
---
name: push-pr
description: >
Push commits and create or update a pull request. Use when the user says
"push pr", "push and create pr", "update pr", "create a pr", "open a pr",
or invokes /push-pr. NOT triggered by plain "push" (that's just git push).
Pushes the current branch, creates a PR if none exists, or confirms the
existing PR was updated. NEVER merges the PR into main — this skill only
pushes to the branch and manages the PR lifecycle.
user-invocable: true
allowed-tools: Bash, Read, Grep, Glob, AskUserQuestion
---
# Push PR Skill
Push commits to remote and create or update a PR. Operates exclusively on the
current branch — never touches main.
## Safety Rules (NON-NEGOTIABLE)
- **NEVER merge a PR into main.** No `tea pr merge`, no `git merge` into main.
- **NEVER checkout or push to main.**
- **NEVER force-push** unless the user explicitly requests it.
- **NEVER use `--no-verify` or skip hooks.**
- Only push to the current working branch.
## Workflow
### 1. Validate branch
```bash
git branch --show-current
```
If on `main`, stop: "You're on main. Switch to a team branch first."
### 2. Check for unpushed commits
```bash
git fetch --all
git status
git log --oneline origin/<branch>..<branch>
```
If no unpushed commits, skip to step 4 (PR check).
### 3. Check for conflicts with main
```bash
git merge-tree --write-tree origin/main HEAD 2>&1
```
If conflicts reported, merge main into current branch:
```bash
git merge origin/main --no-edit
```
If merge conflicts, **stop and report** — let the user resolve.
If clean, continue.
### 4. Push
```bash
git push origin <branch>
```
If push fails, stop and report. Never force-push without explicit request.
### 5. Check for existing PR
```bash
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
```
Match current branch name in PR list.
- **PR exists**: Report "Pushed N commits to `<branch>`. PR #X updated." Done.
- **No PR**: Continue to step 6.
### 6. Create a new PR
```bash
git log --oneline main..<branch>
git diff --stat main...<branch>
```
Draft title (`<type>(<scope>): <summary>`, max 70 chars) and description.
```bash
cat > /tmp/pr-body.md << 'EOF'
## Summary
...
EOF
tea pr create \
--repo jpmschweitzer/settled-reach \
--login schweitz \
--title "<title>" \
--description "$(cat /tmp/pr-body.md)" \
--base main \
--head <branch>
```
Report PR URL when done.
### 7. Update ticket status to review
Scan all commit messages in the PR for ticket references (`#NNN`):
```bash
git log --oneline main..<branch>
```
Extract ticket IDs from `#NNN` patterns. For each ticket that is
currently `in_progress`, update it to `review`:
```bash
db/connectors/ticket status <id> review
```
Report which tickets were moved to review. Skip tickets that are
already `done`, `review`, `cancelled`, or `backlog` (only transition
`in_progress` → `review`).
## Arguments
If the user passes arguments (e.g., `/push-pr "my title"`), use them as the
PR title instead of generating one.
+55
View File
@@ -0,0 +1,55 @@
---
name: render-sprite
description: >
Render a 3D model to 2D sprites via the Godot render pipeline. Produces
sprites at 3 resolutions (1024, 256, 64) from 4 cardinal directions (north,
east, south, west) with outline applied at working resolution. Use when the
user says "render sprite", "render model", "run the render pipeline",
"test the pipeline", "/render-sprite", or asks to render a specific model
(e.g., "render wall_structural"). Output: 12 PNG files in renderer/output/.
---
## Render Pipeline
Run the render script with the model name:
```bash
.claude/skills/render-sprite/scripts/render.sh <model_name>
```
### Available Models
Models live at `renderer/models/<name>.tscn`. List them:
```bash
ls renderer/models/*.tscn | xargs -I{} basename {} .tscn
```
### Output
12 files per model in `renderer/output/`:
```
<model>_north_1024.png <model>_north_256.png <model>_north_64.png
<model>_east_1024.png <model>_east_256.png <model>_east_64.png
<model>_south_1024.png <model>_south_256.png <model>_south_64.png
<model>_west_1024.png <model>_west_256.png <model>_west_64.png
```
### After Rendering
1. Read the 64x64 output files to visually inspect the runtime sprites
2. Read the 256x256 files to check outline quality
3. Report: does it read as the intended object at runtime scale?
### Troubleshooting
- **No output files**: Godot needs a display. If running headless, prefix with `xvfb-run`.
- **Model not found**: Check the model .tscn exists in `renderer/models/`.
- **Godot not found**: Pass path as second arg: `render.sh wall_structural /path/to/godot`
### Adding New Models
1. Create model scene at `renderer/models/<name>.tscn`
2. Apply texture from `renderer/textures/` via StandardMaterial3D
3. Run: `.claude/skills/render-sprite/scripts/render.sh <name>`
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
# Render a 3D model to 2D sprites at 3 resolutions from 4 cardinal directions.
# Usage: render.sh <model_name> [godot_path]
# Example: render.sh wall_structural
set -euo pipefail
MODEL_NAME="${1:?Usage: render.sh <model_name> [godot_path]}"
GODOT="${2:-$HOME/bin/godot}"
REPO_ROOT="$(cd "$(dirname "$0")" && git rev-parse --show-toplevel)"
PROJECT="$REPO_ROOT/renderer"
SCENE="res://render_scene.tscn"
OUTPUT="$PROJECT/output"
# Verify model exists
MODEL_FILE="$PROJECT/models/${MODEL_NAME}.tscn"
if [ ! -f "$MODEL_FILE" ]; then
echo "ERROR: Model not found: $MODEL_FILE"
echo "Available models:"
ls "$PROJECT/models/"*.tscn 2>/dev/null | xargs -I{} basename {} .tscn
exit 1
fi
echo "Rendering: $MODEL_NAME"
echo "Godot: $GODOT"
echo "Project: $PROJECT"
# Ensure resources are imported (headless, no display needed)
echo "Importing resources..."
"$GODOT" --path "$PROJECT" --headless --import 2>&1 || echo "Warning: import step had errors (may be non-fatal)"
# Run Godot with the render scene, passing model name as user arg
"$GODOT" --path "$PROJECT" "$SCENE" -- "$MODEL_NAME" 2>&1
# Check output
EXPECTED_FILES=0
for dir in north east south west; do
for res in 1024 256 64; do
f="$OUTPUT/${MODEL_NAME}_${dir}_${res}.png"
if [ -f "$f" ]; then
EXPECTED_FILES=$((EXPECTED_FILES + 1))
fi
done
done
echo ""
echo "Generated $EXPECTED_FILES/12 files in $OUTPUT/"
ls -la "$OUTPUT/${MODEL_NAME}"_*.png 2>/dev/null || echo "No output files found."
+73 -66
View File
@@ -1,18 +1,18 @@
---
name: review-pr
description: >
Review a branch diff with dual agents before merge. Use when the user says
"review-pr", "review this PR", "review this branch", or invokes /review-pr.
Spawns Hoshe (code quality) and Tyre (architecture) in parallel to review
the diff against main. Reports approve/reject with inline comments.
Review a branch diff with team-appropriate agents before merge. Use when the
user says "review-pr", "review this PR", "review this branch", or invokes
/review-pr. Spawns reviewers matched to the branch type (code, copy, visual,
audio) in parallel. Reports approve/reject with inline comments.
user-invocable: true
allowed-tools: Bash, Read, Grep, Glob, Task
---
# PR Review Skill
Dual-agent review of a branch diff against main. Hoshe reviews code quality,
Tyre reviews architecture. Both must approve for a clean review.
Multi-agent review of a branch diff against main. Reviewer composition depends
on the branch type. All reviewers must approve for a clean review.
## Workflow
@@ -32,7 +32,21 @@ Fetch remote branches first:
git fetch --all
```
### 2. Generate the diff
### 2. Determine reviewer team
Map the branch name to a reviewer set. Use the branch prefix (before any `/`
or `-` suffix) to classify:
| Branch type | Branches | Reviewers |
|-------------|----------|-----------|
| **code** | `server`, `client`, `ci`, or unknown | Hoshe (code quality) + Tyre (architecture) |
| **copy** | `copy` | Hoshe (QA) + Paula (narrative depth) + Miri (world consistency) |
| **visual** | `visual` | Hoshe (QA) + Araminta (art direction) |
| **audio** | `audio` | Hoshe (QA) + Ozzie (player experience) |
If the branch name doesn't match any known type, default to **code** reviewers.
### 3. Generate the diff and read source files
```bash
git log --oneline main..<branch>
@@ -45,79 +59,70 @@ If the diff is empty, report "No changes to review" and stop.
- `Cargo.lock` (auto-generated)
- `client/addons/gdUnit4/` (vendor test framework)
- `*.uid` (Godot-generated)
- `db/commonwealth.db` (binary)
- `docs/backups/settledreach.db.backup` (binary)
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. Have Tyre read source files directly from the branch:
2. Read source files directly from the branch:
`git show origin/<branch>:<path>`
For large diffs (>1000 lines of source), provide **source files** rather than
raw diff to reviewers — cleaner context, better reviews. Read files with
`git show origin/<branch>:<path>` and include them in the prompt.
### 3. Spawn both reviewers in parallel
**IMPORTANT — agent tool access:** Not all reviewer agents have Bash access.
Agents that CAN read from branches themselves: **Hoshe, Tyre, Araminta**.
Agents that CANNOT (no Bash tool): **Paula, Miri, Ozzie, Gestalt, Gore, Nigel**.
Use the Task tool to spawn **two agents simultaneously** in a single message.
Use `model: sonnet` for both — sufficient for review, saves cost.
For agents without Bash, you MUST read the source files yourself (via
`git show origin/<branch>:<path>`) and **paste the file contents directly
into the agent prompt**. Do not tell these agents to read files — they can't.
For very large PRs, read the key files (new/heavily modified) and include
summaries or excerpts of minor changes. Also read and include the relevant
`decisions/*.md` files these agents need for context.
**Agent 1 — Hoshe (Code Quality)**
- `subagent_type`: `hoshe`
- `model`: `sonnet`
- Prompt: Include source code and commit log. Ask Hoshe to review for:
- Correctness and bug risks
- Error handling gaps
- Test coverage (are new features tested?)
- Code style and clarity
- Security concerns (OWASP top 10, injection risks)
- Performance issues
- Request structured verdict: APPROVE or REQUEST_CHANGES with file-specific comments
### 4. Spawn reviewers in parallel
**Agent 2 — Tyre (Architecture)**
- `subagent_type`: `tyre`
- `model`: `sonnet`
- Prompt: Include source code and commit log. Tell Tyre to read the relevant
`decisions/*.md` files first, then review for:
- Architectural consistency with project decisions
- API/interface design quality
- Dependency and coupling concerns
- Scalability implications
- Whether the change respects non-negotiable baselines (D-010, D-012)
- Tyre can read files directly from the branch using `git show origin/<branch>:<path>`
- Request structured verdict: APPROVE or REQUEST_CHANGES with file-specific comments
Use the Task tool to spawn **all reviewers simultaneously** in a single message.
### 4. Present results
Read `references/reviewer-profiles.md` for the full per-branch-type reviewer
specifications (agent types, models, prompt focus areas). Match the branch type
from step 2 to the corresponding section.
Format the combined review as a table per reviewer:
All reviewers: request structured verdict: APPROVE or REQUEST_CHANGES with
file-specific comments.
### 5. Present results
Format the combined review as a table per reviewer. Include one section per
reviewer that was spawned (2 for code/visual/audio, 3 for copy):
```
## Review: <branch> -> main
## Review: <branch> -> main (type: code|copy|visual|audio)
### Hoshe (Code Quality): [APPROVE | REQUEST_CHANGES]
### <Reviewer Name> (<Focus>): [APPROVE | REQUEST_CHANGES]
[Summary]
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | path:line | critical/warning/suggestion | description |
### Tyre (Architecture): [APPROVE | REQUEST_CHANGES]
[Summary]
| # | File | Severity | Issue |
|---|------|----------|-------|
| 1 | path:line | critical/warning/suggestion | description |
### <Reviewer Name> (<Focus>): [APPROVE | REQUEST_CHANGES]
...
### Verdict: [APPROVED | CHANGES REQUESTED]
```
The overall verdict is APPROVED only if **both** reviewers approve.
The overall verdict is APPROVED only if **all** reviewers approve.
## Prompt template for reviewers
Use this structure when constructing the agent prompts (adapt as needed):
```
Review the following branch diff for merge into main.
Review the following {branch_type} branch diff for merge into main.
Branch: {branch}
Branch type: {branch_type} (code|copy|visual|audio)
Commits:
{commit_log}
@@ -126,7 +131,7 @@ Diff stats:
[Source files or diff here — exclude vendor/generated code]
Review focus: {focus_area}
Your review focus: {focus_area}
Respond with:
1. Verdict: APPROVE or REQUEST_CHANGES
@@ -135,35 +140,37 @@ Respond with:
- File path and approximate location
- Severity: critical / warning / suggestion
- Description of the issue
Verdict rules:
- Any **critical** or **warning** issue → REQUEST_CHANGES
- Only **suggestion**-level issues (or none) → APPROVE
If no issues found, say APPROVE with a brief positive summary.
```
## Posting results to Gitea
## 6. Posting results to Gitea
After presenting results to the user, post the review as a PR comment:
```bash
tea comment --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> "<review markdown>"
```
Use a heredoc for multi-line review bodies:
```bash
tea comment --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> "$(cat <<'REVIEW'
## Dual-Agent Review: <branch> -> main
...review content...
REVIEW
)"
```
After presenting results to the user, post the review as a PR comment.
Note: `tea pr reject` does not work on your own PRs. Use `tea comment` instead.
**Heredoc bodies hang** in `tea comment`. Write review to a temp file instead:
**IMPORTANT — `tea comment` hangs with inline heredocs and multi-line strings.**
Always use a two-step approach: write to a temp file first, then pass via `$(cat)`:
```bash
# Write review to temp file, then pass via cat
tea comment --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> "$(cat /tmp/review.md)"
# Step 1: Write review to temp file (use Bash heredoc, NOT the Write tool)
cat > /tmp/review-<branch>.md << 'EOF'
## Review: <branch> -> main
...review content...
EOF
# Step 2: Post to Gitea (separate Bash call)
tea comment --login schweitz --repo jpmschweitzer/settled-reach <PR_NUMBER> "$(cat /tmp/review-<branch>.md)"
```
## Merging approved PRs
These MUST be separate sequential Bash calls — do not chain them in a single
command. The heredoc write must complete before `tea comment` reads the file.
## 7. Merging approved PRs
`tea pr merge` fails (405) when branches have conflicts with main. Merge
locally instead:
@@ -0,0 +1,103 @@
# Reviewer Profiles by Branch Type
Use `model: sonnet` for all reviewers — sufficient for review, saves cost.
## Code reviews (`server`, `client`, `ci`)
**Hoshe (Code Quality)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Include source code and commit log. Ask Hoshe to review for:
- Correctness and bug risks
- Error handling gaps
- Test coverage (are new features tested?)
- Code style and clarity
- Security concerns (OWASP top 10, injection risks)
- Performance issues
**Tyre (Architecture)**
- `subagent_type`: `tyre`, `model`: `sonnet`
- Prompt: Include source code and commit log. Tell Tyre to read the relevant
`decisions/*.md` files first, then review for:
- Architectural consistency with project decisions
- API/interface design quality
- Dependency and coupling concerns
- Scalability implications
- Whether the change respects non-negotiable baselines (D-010, D-012)
- Tyre can read files directly from the branch using `git show origin/<branch>:<path>`
## Copy reviews (`copy`)
**Hoshe (QA)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
- Formatting consistency (markdown, file naming, frontmatter)
- Broken references or links
- Spelling and grammar
- File organization and structure
- Missing or orphaned files
**Paula (Narrative Depth)** — NO BASH ACCESS
- `subagent_type`: `paula`, `model`: `sonnet`
- Paula cannot read from branches. You must paste file contents and decision
files directly into the prompt.
- Prompt: Include full text of changed files, commit log, and relevant
`decisions/*.md` content. Ask Paula to review for:
- Narrative quality and character voice consistency
- Whether dialogue and monologue feel authentic to the characters
- Consequences and stakes — do choices carry weight?
- Political and interpersonal depth
- Emotional resonance — does the text make you feel something?
**Miri (World Consistency)** — NO BASH ACCESS
- `subagent_type`: `miri`, `model`: `sonnet`
- Miri cannot read from branches. You must paste file contents and decision
files directly into the prompt.
- Prompt: Include full text of changed files, commit log, and relevant
`decisions/*.md` content. Ask Miri to review for:
- Lore accuracy — do facts match established setting?
- Internal consistency across files
- IP originality — nothing should read as a copy from another franchise
- Faction, technology, and location details match the worldbuilding docs
- Setting serves gameplay mechanics (asymmetric information, perception)
## Visual reviews (`visual`)
**Hoshe (QA)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
- File format and naming conventions
- Asset organization and directory structure
- Missing or broken references in scene/resource files
- Import settings consistency
**Araminta (Art Direction)** — HAS BASH ACCESS
- `subagent_type`: `araminta`, `model`: `sonnet`
- Prompt: Include the changed files and commit log. Tell Araminta to read
the style guide and relevant design docs 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
## Audio reviews (`audio`)
**Hoshe (QA)**
- `subagent_type`: `hoshe`, `model`: `sonnet`
- Prompt: Include the changed files and commit log. Ask Hoshe to review for:
- File format and naming conventions
- Audio asset organization and directory structure
- Missing or broken references
- Import/bus configuration consistency
**Ozzie (Player Experience)** — NO BASH ACCESS
- `subagent_type`: `ozzie`, `model`: `sonnet`
- Ozzie cannot read from branches. You must paste file contents directly
into the prompt.
- Prompt: Include full text of changed files and commit log. Ask Ozzie to
review for:
- Emotional impact — does the audio enhance the moment?
- Atmosphere and tone — does it feel like the Commonwealth?
- 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?
+13 -56
View File
@@ -10,74 +10,39 @@ allowed-tools: Bash, Read, Grep, Glob
# Search Docs Skill
Semantic search across Commonwealth project documents using Qdrant vector database and ollama embeddings.
Semantic search across project documents. Basic commands (`qdrant-search`,
`qdrant-index`, `qdrant-health`, `qdrant-count`) and endpoints are documented
in CLAUDE.md. This skill covers advanced operations and workflows.
## Access Method
**Use the connector script:**
```bash
python3 db/connectors/qdrant_connector.py <command> [args]
```
## Commands
### Search
Find documents semantically related to a query:
```bash
python3 db/connectors/qdrant_connector.py search "asymmetric information design"
python3 db/connectors/qdrant_connector.py search "what did we decide about fog of war"
python3 db/connectors/qdrant_connector.py search "engine requirements"
```
Returns top 5 matching document chunks with source file, heading, and relevance score.
### Index a file
Add or update a document in the search index:
```bash
python3 db/connectors/qdrant_connector.py index-file decisions/architecture.md
python3 db/connectors/qdrant_connector.py index-file decisions/perception.md
python3 db/connectors/qdrant_connector.py index-file docs/discussions/round-10-map-fog-borderless.md
python3 db/connectors/qdrant_connector.py index-file docs/briefings/tyre.md
```
Files are chunked by markdown headings (# and ##). Each chunk is embedded via ollama and stored in Qdrant with metadata (source_file, heading, chunk_index).
## Advanced Commands
### Index a single chunk
For precise indexing of specific content:
```bash
python3 db/connectors/qdrant_connector.py index "unique-id" "Text content to index" --metadata source=manual heading="Custom heading"
```
### Health check
Verify connectivity to Qdrant and ollama:
```bash
python3 db/connectors/qdrant_connector.py health
```
### Collection info
Check how many documents are indexed:
```bash
python3 db/connectors/qdrant_connector.py count
```
### Create collection
Initialize the Qdrant collection (run once during setup):
```bash
python3 db/connectors/qdrant_connector.py create-collection
```
## Endpoints
## Bulk Indexing
Configured in `db/connectors/config.json`:
- **Qdrant:** `http://tower-of-joy:6333`
- **Ollama:** `http://tower-of-joy:11434` (model: nomic-embed-text)
- **Collection:** `commonwealth` (768 dimensions, cosine distance)
Index all project documents at once:
```bash
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
db/connectors/qdrant-index "$f"
done
```
## Fallback
If Qdrant or ollama is unreachable, fall back to grep-based search:
```bash
# Search across all project docs
grep -r -i "search term" decisions/ DISCUSSION.md docs/ --include="*.md"
```
@@ -88,11 +53,3 @@ grep -r -i "search term" decisions/ DISCUSSION.md docs/ --include="*.md"
3. After briefing updates, re-index affected briefings
4. After decision changes, re-index the relevant decisions/*.md domain files
5. Use search to answer "did we discuss this?" questions with citations
## Bulk indexing
To index all project documents at once:
```bash
for f in decisions/*.md DISCUSSION.md TEAM.md docs/discussions/*.md docs/briefings/*.md; do
python3 db/connectors/qdrant_connector.py index-file "$f"
done
```
+178 -26
View File
@@ -1,12 +1,14 @@
---
name: start-sprint
description: >
Start sprint work on a team branch. Use when the user says "start sprint",
"start working on the server/client", "begin sprint", or invokes /start-sprint.
Merges main into the team branch, finds the active sprint, reads the sprint
briefing, and presents the work plan with ticket details.
Start sprint work on a team branch, or close a sprint from main. Use when
the user says "start sprint", "start working on the server/client/copy",
"begin sprint", or invokes /start-sprint. On a team branch: merges main,
finds the active sprint, reads the briefing, presents the work plan. On
main: closes the active sprint, bumps the version (v0.1.N), updates the
changelog, tags, and pushes.
user-invocable: true
allowed-tools: Bash, Read, Grep, Glob
allowed-tools: Bash, Read, Grep, Glob, TeamCreate, Task, TaskCreate, TaskUpdate, TaskList, SendMessage, AskUserQuestion
---
# Start Sprint Skill
@@ -23,8 +25,69 @@ The current branch IS the team. Read it with:
git branch --show-current
```
Valid team branches: `server`, `client`. If on `main`, ask the user which
team branch to check out first.
Valid team branches: `server`, `client`, `copy`, `audio`, `visual`, `ci`.
If on `main`, follow the **Main branch workflow** below instead of
the team branch workflow (steps 2–8).
---
## Main branch workflow (sprint close + version bump)
When `/start-sprint` is run on `main`, it means the user wants to close
the current sprint, cut a version, and prepare for the next one.
### M1. Close the active sprint
```bash
db/connectors/sprint stop
```
This marks the active sprint as completed and lists carry-over candidates.
Note the sprint number (N) from the output.
### M2. Bump the version
The project version scheme is `v0.1.{sprint_number}`. After closing
sprint N, the version is `v0.1.N`.
Update `CHANGELOG.md`:
- Move all entries under `## [Unreleased]` into a new section
`## [v0.1.N] — YYYY-MM-DD` (using today's date).
- Leave `## [Unreleased]` as an empty section above the new version.
- Keep the existing sub-headings (Added, Fixed, Changed, Removed) —
only move entries that have content.
### M3. Commit the release
Stage and commit `CHANGELOG.md`:
```
chore(meta): release v0.1.N
```
### M4. Tag the release
```bash
git tag v0.1.N
```
### M5. Push
```bash
git push && git push --tags
```
### M6. Report
Output a summary:
- Sprint closed (name, done/total tickets, carry-over count)
- Version tagged (`v0.1.N`)
- Carry-over candidates (if any)
- Suggest running `/plan-sprint` next to prepare the next sprint
---
## Team branch workflow
### 2. Sync with main
@@ -35,49 +98,138 @@ git merge origin/main --no-edit
If the merge has conflicts, report them and stop — do not force-resolve.
### 3. Find the active sprint
### 3. Load sprint context
Run the sprint CLI to get the full context dump in one shot:
```bash
db/connectors/ticket sprint --active
db/connectors/sprint start-work
```
Extract the sprint ID and name from the JSON output. If no active sprint,
report that and stop.
This auto-detects the active sprint and current team from the branch.
It outputs: sprint metadata, briefing paths, decision refs, actionable
tickets, blocked tickets, and done tickets.
If no active sprint is found, report that and stop.
### 4. Read the sprint briefing
Read `docs/sprints/sprint-N/<team>.md` where N is the sprint ID and team
matches the branch name. If the branch is `server` or `client`, use that
directly. For other branches, check if a matching briefing exists.
Also check for a `joint.md` briefing — joint tasks involve both teams and
should be mentioned.
Read the briefing file(s) listed in the `start-work` output
(e.g. `docs/sprints/sprint-6/server.md` and `joint.md`).
If no matching briefing exists for the team, suggest running
`/plan-sprint` to generate one.
### 5. Load ticket details
For each ticket listed in the briefing, run:
For tickets that need more detail than the `start-work` summary provides:
```bash
db/connectors/ticket show <id>
```
Identify which tickets are actionable now (no open blockers) vs blocked.
### 6. Read key decisions
Read the decision files referenced in the sprint briefing so the agent has
full architectural context before starting work.
### 7. Present the work plan
### 7. Mark tickets in progress and present the work plan
Output a summary:
Mark all actionable (unblocked, non-done) tickets in the sprint as
`in_progress`:
```bash
db/connectors/ticket status <id> in_progress
```
Then output a summary:
- Sprint name and goal
- Branch status (clean merge or conflicts)
- Actionable tickets (unblocked, ready to start)
- Tickets marked in_progress (list with IDs)
- Blocked tickets (and what blocks them)
- Key decisions loaded
- Suggested first task (lowest ID unblocked ticket)
Then mark the first actionable ticket as `in_progress`:
```bash
db/connectors/ticket status <id> in_progress
### 8. Confirm and spawn the team
Before spawning agents, use `AskUserQuestion` to confirm the work plan and
agent lineup with the user. If declined, stop.
Once confirmed:
#### 8a. Parse agents from the briefing
Extract agent names from the `**Agents:**` line. Format:
```
**Agents:** Name (role), Name (role), ...
```
Map each name to its `subagent_type` (lowercase):
- "Dudley (simulation)" → `dudley`
- "Stig (UI)" → `stig`
- "Tyre (architecture)" → `tyre`
- "Hoshe (QA)" → `hoshe`
- "Mellanie (author)" → `mellanie`
- etc. (see `.claude/agents/` for full roster)
#### 8b. Create the team
```
TeamCreate(team_name: "sprint-{N}-{team}")
```
This makes you the team lead.
#### 8c. Create tasks from tickets
For each ticket in the briefing, create a task:
```
TaskCreate(
subject: "#{id}: {title}",
description: "Full ticket details from step 5, plus briefing notes
and integration points for this ticket.",
activeForm: "Working on #{id}: {short_title}"
)
```
After creating all tasks, mirror the dependency chain from the briefing
using `TaskUpdate` with `addBlockedBy`.
#### 8d. Spawn agents
For each agent from the `**Agents:**` line, spawn a teammate in the
background. Spawn all agents in parallel (one message, multiple Task calls):
```
Task(
subagent_type: "{name_lowercase}",
team_name: "sprint-{N}-{team}",
name: "{name_lowercase}",
prompt: "You are on the {team} team for Sprint {N}.
Branch: `{team}`
1. Read the sprint briefing: docs/sprints/sprint-{N}/{team}.md
2. Read the decision files referenced in the briefing.
3. Check TaskList for available work.
4. Claim an unblocked task (TaskUpdate with owner: your name),
mark it in_progress, and implement it.
5. When done, mark the task completed and check TaskList for
the next available task.
Use `db/connectors/ticket show <id>` for full ticket specs.",
description: "Sprint {N} {team}: {name}",
run_in_background: true
)
```
#### 8e. Report
Output to the user:
- Team name: `sprint-{N}-{team}`
- Agents spawned (names and roles)
- Tasks created (count actionable vs blocked)
- How to interact: `SendMessage` to talk to agents, `TaskList` to
check progress
You are now the team lead. Agents work autonomously — monitor via
`TaskList`, communicate via `SendMessage`, and handle blockers as
they arise.
+114
View File
@@ -0,0 +1,114 @@
---
name: start-workshop
description: >
Start a multi-agent design workshop from a workshop brief. Use when the user says
"start workshop", "run workshop", "let's start the workshop", or invokes /start-workshop.
Parses the workshop brief to extract participants, questions, and round format.
Creates a team, tasks, and spawns agents as teammates via the Task tool.
---
# Start Workshop
## Prerequisites
A workshop brief must exist at `docs/workshops/{name}/{name}-workshop-brief.md` containing:
- **Participants** line (comma-separated agent names)
- **Questions for participants** sections with numbered questions tagged by agent name
- **Workshop Format** section defining number of rounds and their purpose
## Workflow
### 1. Parse the Brief
Read the workshop brief. Extract:
- Workshop name (from directory name)
- Participant list (from `**Participants:**` line)
- Per-participant questions (scan for `**{AgentName}**:` patterns in questions sections)
- Round count and round descriptions (from `**Workshop Format**` section)
### 2. Create Team
```
TeamCreate: team_name = "{workshop-name}", description from brief title
```
### 3. Always-Present Agents
Two agents join every workshop regardless of the participant list:
| Agent | Role | Task | Participates in discussion? |
|-------|------|------|-----------------------------|
| **Qatux** | Documenter | Captures all decisions, questions, dissent, consensus. Writes `workshop-notes.md` per round, produces final `workshop-outcomes.md`. | No — observes and records only |
| **SI** | Sprint prep | Suggest adding when outputs include tickets. Creates tickets from decisions, links to sprint backlog. | No — execution prep only |
Qatux and SI are **never dismissed early.** If the user reduces the team mid-workshop, keep qatux and si (if added). Documenting everything prevents loss of valuable information.
### 4. Create Tasks (Round 1)
One task per participant containing:
- Instruction to read the full brief at its path
- The specific questions assigned to that participant (extracted from all layers)
- The output format from the brief's round description
- **IMPORTANT — file output requirement:** Instruct each agent to write their full output to `docs/workshops/{name}/{agent}-round{N}.md` (e.g., `docs/workshops/test-architecture/gestalt-round1.md`). Agents must write to disk, not just send messages. This ensures Qatux and other agents can read all outputs directly without relying on message forwarding.
One task for Qatux: "Document Round N — read all agent output files at `docs/workshops/{name}/*-round{N}.md` and capture decisions, questions, and dissent."
Assign all tasks using TaskUpdate with `owner` = agent name.
### 5. Spawn Agents
Use the Task tool to spawn each agent as a teammate. Each call should:
- Set `team_name` to the workshop team name
- Set `name` to the agent name (e.g., "gestalt")
- Set `subagent_type` to the matching agent type (same as name — see reference table)
- Provide a prompt telling the agent to check TaskList for their assigned task
Spawn all agents in parallel (one Task call per agent in a single message). Agents will appear as teammates in the Claude Code UI and pick up their tasks from the shared task list.
For large workshops (>6 agents), spawn participants in batches to avoid overwhelming the system. Always-present agents (Qatux, SI) can run in background via `run_in_background: true`.
### 6. Monitor
- TaskList to check progress
- SendMessage to nudge idle agents or provide clarification
- Agents work autonomously — claim tasks, read the brief, produce responses
### 7. Between Rounds
When all Round N tasks are complete:
1. Verify all agents wrote output files to `docs/workshops/{name}/`. If any are missing, nudge the agent or extract from their message and write the file yourself.
2. Qatux reads all `*-round{N}.md` files and produces round summary in `round-{N}-notes.md`
3. Create Round N+1 tasks (integration pass, synthesis, etc.) — include the same file output requirement
4. Assign to agents with TaskUpdate
5. Agents continue working
### 8. Wrap Up
**Always ask the user before wrapping up.** There may be more to discuss or additional rounds needed. Only proceed to wrap-up when the user confirms.
Wrap-up sequence:
1. Qatux produces final `workshop-outcomes.md` from accumulated notes
2. If SI is present, SI creates tickets from decided items
3. Send shutdown_request to all agents (qatux and si last, after they finish their output tasks)
4. TeamDelete to clean up
## Agent Type Reference
Agent name maps directly to subagent_type:
| Name | Domain | Typical workshop role |
|------|--------|-----------------------|
| gestalt | Systems design | Mechanics, fun factor, system interactions |
| ozzie | Player experience | Wow moments, feel, emotional response |
| tyre | Technical architect | Feasibility, performance, architecture |
| stig | UI developer | Interface patterns, layout, interaction flows |
| dudley | Server developer | Server-side systems, data flow, ECS |
| paula | Narrative | Story, dialogue, character, political depth |
| araminta | Visual design | Art direction, visual treatment, aesthetics |
| nigel | Replayability | Emergent stories, second-playthrough, sandbox |
| gore | Themes/endgame | What the game is about, philosophical questions |
| mellanie | Copywriter | In-game text, voice, tone |
| miri | Worldbuilder | Setting, factions, lore, consistency |
| inigo | Sound design | Audio, soundscape, spatial audio |
| troblum | Tech consultant | Second opinion on architecture/tech choices |
| hoshe | QA/testing | Test plans, verification, quality |
+20 -28
View File
@@ -10,42 +10,20 @@ allowed-tools: Bash, Read, Grep, Glob
# Ticket Skill
Manage the Commonwealth project ticketing database at `db/commonwealth.db`.
## Access Method
**Use the `ticket` CLI for all ticket operations:**
```bash
db/connectors/ticket <command> [args...]
db/connectors/ticket --help
```
All output is JSON on stdout.
For raw SQL access (rare), use the wrapper scripts:
| Script | Purpose |
|--------|---------|
| `db/connectors/sqlite-query "<SQL>"` | Run SELECT queries |
| `db/connectors/sqlite-exec "<SQL>"` | Run INSERT/UPDATE/DELETE |
| `db/connectors/sqlite-init` | Create/update database from schema |
| `db/connectors/sqlite-seed` | Seed initiatives from DECISIONS.md |
Manage the project ticketing database. Basic usage (`ticket list`, `ticket show`,
`ticket sprint --active`) and raw SQL wrappers are documented in CLAUDE.md.
This skill covers the full command reference.
## Commands
### List tickets
### List tickets (full flags)
```bash
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A]
db/connectors/ticket list [--status S] [--priority P] [--epic N] [--sprint N] [--assigned A] [--team T]
```
### Show ticket detail
```bash
db/connectors/ticket show <id>
```
Returns full ticket with children, blockers, and dependents.
### Create ticket
```bash
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D]
db/connectors/ticket create <type> <title> [--parent N] [--priority P] [--decision D] [--team T]
```
Types: `initiative`, `epic`, `story`, `task`, `bug`
Priorities: `critical`, `high`, `medium`, `low`
@@ -63,12 +41,21 @@ db/connectors/ticket assign <id> <agent>
db/connectors/ticket unassign <id>
```
### Team assignment
```bash
db/connectors/ticket team <id> <teams>
```
Teams are comma-separated, e.g. `server`, `client`, `server,client`.
### Sprint management
```bash
db/connectors/ticket sprint [--active]
db/connectors/ticket sprint assign <id> <sprint_id>
```
For sprint-scoped operations (status overview, context dumps, lifecycle),
use the dedicated sprint CLI instead: `db/connectors/sprint --help`
### Dependencies
```bash
db/connectors/ticket deps <id>
@@ -82,6 +69,11 @@ db/connectors/ticket children <id>
db/connectors/ticket count [--status S]
```
### Batch show
```bash
db/connectors/ticket show --brief <id> [<id>...]
```
## Workflow
1. **SI (Project Manager)** is the primary user of this skill
+127
View File
@@ -0,0 +1,127 @@
---
name: worktree-update
description: >
Sync worktree branches with main. Use when the user says "update worktrees",
"sync branches", "merge main", "worktree update", or invokes /worktree-update.
When on main: shows which worktree branches are ahead and lets the user pick
which to merge into main (flags branches with open PRs). When on a non-main
branch: merges main into the current branch. All operations are non-destructive.
user-invocable: true
allowed-tools: Bash, Read, AskUserQuestion
---
# Worktree Update Skill
Sync worktree branches safely. Direction depends on the current branch.
## Safety Rules (NON-NEGOTIABLE)
- **Never force-push, reset --hard, rebase, or delete branches.**
- **Never use `--no-verify` or skip hooks.**
- **Always use `--no-edit` on merges** to avoid interactive editor prompts.
- **Stop on merge conflicts** — report them and let the user decide. Never
auto-resolve or abort a conflicted merge without asking.
- **Fetch before comparing** — always `git fetch --all` first so commit
comparisons are accurate.
- **Dry-run first on main** — show the user exactly what will happen before
merging anything into main.
## Workflow
### 1. Detect current branch
```bash
git branch --show-current
```
Branch determines the mode: `main` → outbound sync, anything else → inbound sync.
### 2a. On `main` — merge worktree branches into main
#### Fetch and compare
```bash
git fetch --all
```
Discover all worktree branches (excluding `main` itself):
```bash
git worktree list | grep -v '\[main\]' | sed 's/.*\[//;s/\]//'
```
For each worktree branch, check if it has commits ahead of main:
```bash
git rev-list --count main..origin/<branch>
```
Skip branches with 0 commits ahead. For branches that ARE ahead, collect:
- Branch name
- Number of commits ahead
- One-line log of those commits: `git log --oneline main..<branch>`
#### Check for open PRs
```bash
tea pr list --login schweitz --repo jpmschweitzer/settled-reach --state open --output simple
```
Cross-reference open PR head branches with the ahead-of-main branches.
#### Present results
Show a summary table of branches ahead of main. For each branch, indicate:
- `[PR]` if it has an open pull request — warn that it should go through
normal review channels (use `/review-pr` instead)
- Commit count and summary
Use `AskUserQuestion` to let the user pick which branches to merge.
Exclude PR-flagged branches from the default options (but allow the user to
override via "Other").
#### Merge selected branches
For each selected branch, one at a time:
```bash
git merge <branch> --no-edit
```
If a merge conflicts, **stop immediately**. Report the conflict and do NOT
continue to the next branch. The user must resolve before proceeding.
After all merges, show the final state with `git log --oneline -N` (where N
covers the new commits).
#### Backup the shared database
After successful merges on main, snapshot the database for git tracking:
```bash
make db-backup
```
This copies the shared `settledreach.db` (in the parent directory) to
`docs/backups/settledreach.db.backup`. Stage and commit it with the merge
if the file changed.
### 2b. Not on `main` — merge main into current branch
```bash
git fetch --all
git merge origin/main --no-edit
```
If clean, report the result (fast-forward or merge commit, files changed).
If conflicts, report them and stop.
### 3. Push prompt
After a successful merge, ask the user if they want to push:
```bash
git push origin <current-branch>
```
Never push without explicit confirmation.
+29
View File
@@ -0,0 +1,29 @@
#!/usr/bin/env bash
# Pre-commit hook dispatcher. Runs modular checks from tooling/.
# Installed via: git config core.hooksPath .config/hooks
set -euo pipefail
REPO_ROOT="$(git rev-parse --show-toplevel)"
ERRORS=0
run_check() {
local script="$1"
local label="$2"
if [ -x "$REPO_ROOT/$script" ]; then
if ! "$REPO_ROOT/$script"; then
ERRORS=$((ERRORS + 1))
fi
else
echo "pre-commit: WARNING — $label skipped ($script not found or not executable)"
echo " Run 'make setup' or check that $script exists and is executable."
fi
}
# --- Checks ---
run_check "tooling/check-fact-ids" "fact_id validation"
if [ "$ERRORS" -gt 0 ]; then
echo ""
echo "pre-commit: $ERRORS check(s) failed. Commit aborted."
exit 1
fi
+15 -8
View File
@@ -1,16 +1,24 @@
# Build and cache
.cache/
server/target/
tooling/content-converter/target/
tooling/line-previewer/target/
tooling/test-client/target/
content-ron/
# Godot
# Godot client (further ignores managed by client team)
client/.godot/
client/export/
client/reports/
client/*.import
# Database journal files (transient, not the DB itself)
db/commonwealth.db-wal
db/commonwealth.db-shm
# Renderer (separate Godot project for sprite pipeline)
renderer/.godot/
renderer/**/*.import
renderer/**/*.uid
renderer/output/*.png
# Database (shared across worktrees at ../settledreach.db, not tracked)
db/commonwealth.db*
# Python
__pycache__/
@@ -27,8 +35,7 @@ Thumbs.db
*.swp
*.swo
# Claude Code internals (plans, session transcripts, local settings)
# Note: .claude/agents/ and .claude/skills/ ARE tracked
# Claude Code internals (plans, session transcripts)
# Note: .claude/agents/, .claude/skills/, and .claude/settings.json ARE tracked
.claude/plans/
.claude/projects/
.claude/settings.json
+475
View File
@@ -6,10 +6,481 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
## [v0.1.9] — 2026-02-18
### Fixed
- `make game` now builds client before launching — was missing `build-client` dependency, causing class_name registration failures after `make clean`
- `build-client` uses `--import --quit` instead of just `--quit` — ensures `.godot/` cache and `global_script_class_cache.cfg` are created from scratch
- `make clean` preserves `client/.godot/` directory (clears contents only) to avoid Godot startup issues
### Added
- `--description TEXT` flag for `ticket create` CLI — previously required raw SQL workaround to set ticket descriptions
### Changed
- UI audio assets revised — monologue chimes re-generated (0.8s, insert-tech aesthetic), fog_recognition re-generated (was silent), all 8 assets normalized to 44.1kHz stereo LUFS -16 (#453)
- review-pr skill: explicit verdict rules — critical/warning → REQUEST_CHANGES, suggestion-only → APPROVE
### Added
- `make golden-diff` + `make golden-update` targets (#486) — developer workflow for golden file comparison and regeneration; safe restore on cargo failure
- Gauntlet checklist YAML schema (#497) — 7 condition types evaluable from ObserverSnapshot, per-room checklists for 3 rooms, `make checklist-validate` and `make checklist-generate` targets, wired into `pre-pr-content` gate
- Gauntlet room timer + personal bests (#496) — GauntletHUD shows TIMER: MM:SS (PB: MM:SS), starts on room entry, resets on room change, persists stats to user://dev/gauntlet-stats.json, session summary on disconnect, hidden in non-gauntlet mode
- WRONG button F12 MVP (#495) — bug report capture: pause sim, show modal prompt, save snapshot.json + render.txt + description.txt to user://bug-reports/, Esc to cancel
- GameState.room_id and gauntlet_mode fields — parsed from ObserverSnapshot, enabling gauntlet UI
- BUG_REPORT action in InputMapper (F12 binding) with SimBridge wire guard (client-only)
- 24 new anti-tedium tests — GauntletHUD lifecycle (16: timer, PB, visibility, room change, session tracking) + BugReportDialog (8: pause/unpause, wire guard, text render, edge cases)
- whatsinagame starter kit — reusable multi-agent team bootstrap for any project (3-tier profiles, 18 skills, 16 agent archetypes, stakeholder personas, ticketing DB, decision tracking)
- Domain-action naming convention for skills documented in create-skill guide
- `gauntlet` feature flag (default-on) — allows stripping Gauntlet test world from release builds with `--no-default-features`
- `EXPECTED_ENTITY_COUNT` and `RESET_PLATE_STABLE_IDS` constants — entity counts derived from StableId ranges instead of hardcoded values
- Debounce exact-boundary test for room reset (tick 9 rejected, tick 10 accepted)
- Reset plate StableId verification in `stable_id_ranges_match_spec`
- Runtime content test (`content_runtime.rs`) separated from structural loading tests
- Client P2 tests (#492) — 16 gdUnit4 tests for camera (smoothing, zoom, viewport, follow, no-pan), entity alpha/color (peripheral, forward, NPC color, player constant), UI (monologue, interaction, inventory, dialogue, pause, fog blob, fog z_index)
- Client P3 tests (#493) — 12 gdUnit4 tests for z-layer ordering (floor/ysort/fog/UI), entity lerp (snap, converge, LERP_SPEED=12.0), Tyre additions (recognition, facing, delta scaling, blob removal)
- Anti-tedium regression tests (#494) — 5 tests: F12 no-crash guard, no queued input, gauntlet UI hidden in default/normal snapshot/multi-tick modes
### Changed
- Room reset API consolidated to `plan_reset` only — `execute_reset` removed (was a maintenance trap; production uses Commands via `plan_reset`)
- `room_at()` documented with z-range and corridor overlap assumptions
- TCP runtime test now has 10s read timeout and registers player in EntityRegistry
### Removed
- Dead `RoomMember` component from reset.rs (defined but never used)
- Protocol v8: dialogue_response field decoding (DialogueResponseEvent with line_id, text, speaker_entity_id) from server #305/D-028
- Test client binary scaffolding (#480) — standalone crate at `tooling/test-client/` with CLI (--connect, --replay, --text, --json, --quiet, --golden, --ticks), exit codes (0/1/2), golden file JSON diff, JSONL replay loader
- Snapshot text renderer (#481) — `format_snapshot_text(&ObserverSnapshot)` pub-exported from server crate, entity labels as kind:entity_id sorted by distance, room name stub, 10 unit tests
- Sprint 9 (Gauntlet) briefing files — server, client, CI, audio, joint — 23 tickets across 4 teams
- Weapon aim lock audio (`sfx_weapon_aim_lock.ogg`) — clinical targeting confirmation tone for weapon aim state (#440)
- Stance change audio (`sfx_stance_change.ogg`) — subtle mechanical click for stance toggle feedback (#440)
### Fixed
- MessagePack int_64 encoder dead code branch (#516) — `-(1 << 63)` overflowed making int_64 branch unreachable; negative values beyond int_32 now correctly encode as 0xd3 instead of 0xcf
- Cross-encoder fixture pipeline hardened — encode failures now exit non-zero instead of writing empty .msgpack files (#475 review)
- GDScript fixture test no longer silently skips on missing/empty fixture dir — asserts instead (#475 review)
- GDScript fixture generator covers all PlayerAction variants (added MoveSouth, MoveEast, MoveWest, Unpause, ToggleStanceDown, WalkAway)
- `make pre-pr` now checks GDScript fixture staleness alongside Rust fixtures
- Protocol version bumped from 7 to 8 to match server — fixes 5 test failures from version mismatch
- Interact action encoding changed from unit variant to struct variant to match server's PlayerAction::Interact { target_entity_id, verb }
- Monologue duplication test (test_monologue_not_duplicated_after_consumption) fixed — was using poll_snapshot() which doesn't consume _last_snapshot in test mode
- `make pre-pr` target — full pre-PR verification chain: lint → build → test → content validation → fixture staleness (#460, #465)
- Branch-specific pre-PR variants: `make pre-pr-server`, `make pre-pr-client`, `make pre-pr-content`
- Content cross-reference validation (9 checks) — canonical_id uniqueness, relationship targets, location slugs, dialogue locations, fact_ids, triangle membership, npc_count, dialogue line_ids, bidirectional relationships (#464)
### Fixed
- Dialogue schema missing `focused` (mood) and `greeting` (situation) values added in Sprint 7-8 content
- Speaker wire ID silent fallback — dialogue now warns and skips when target entity missing from registry (was silently using 0)
- Cross-plugin system ordering — trigger_recognition_monologue now runs after detect_anomalies (latent determinism bug)
- Walk-away ordering — process_walk_away now runs after process_talk_interaction (prevents same-tick race)
- ActiveDialogue overwrite — new Talk while in existing dialogue now emits IncompleteInteraction before replacing
- Server-side Talk range check — handle_talk now enforces CLOSE_RANGE before setting TalkRequest (was client-only)
- ExamineNpc label collision — VerbKind::ExamineNpc now uses "Examine NPC" label (was "Observe", same as generic Observe)
- Dead conditional in main.rs collapsed (both branches were identical)
- WalkAway variant added to all_player_action_variants_roundtrip serialization test
### Changed
- DialogueCooldownTracker.used changed from Vec to BTreeMap for O(log n) lookup (D-041 compliance)
- MonologueState.shown_ids changed from Vec to HashSet for O(1) contains check (was O(n) per tick)
- Secret trust tier documented as unreachable with TODO for Phase 2 KG-gated unlock
### Added
- Determinism test: different_seed_produces_different_replay — exercises SimRng via dialogue weighted selection
- Dialogue selection pipeline (#305, D-028) — 4-layer filtering engine: access tier from KG relationship, situation derivation from game state, trust tier, weighted topic+mood scoring via SimRng. Full Talk verb → selected line → ObserverSnapshot pipeline with cooldown tracking
- ContentSlug component (#452) — stable content identity from YAML (e.g. "kael-davan") independent of runtime Entity handles, for knowledge graph interaction memory across save/load
- Walk-away KG recording (#427, D-064) — IncompleteInteraction knowledge events with Talk/Confront type, ActiveDialogue tracking, walk-away detection clears dialogue and records in KG
- Anomaly detection for urgent recognition (#450, D-060) — AnomalyMarker flags PersonOfInterest/Contradicted entities for 0.3s cognitive delay instead of 0.6s normal
- Recognition monologue during cognitive delay (#451, D-060) — monologue fires at delay START (grey blob phase), not completion. v0.1 fallback lines, anomaly prioritization, cooldown tracking
- Server --test-mode, --port, --seed CLI flags (#459) — LISTENING:{port} stdout signal, OS-assigned ports, deterministic seed override, stderr-only tracing
- Determinism gauntlet test (#466) — 20-tick replay determinism regression test with movement, stance, pause/unpause exercise
- Pause guard test suite (#461-463, #468) — 7 tests covering movement, unpause, roundtrip, stance, interact, batch, tick_rate during pause
- EntityRegistry lifecycle tests (#469) — stale mapping, re-register, unknown unregister edge cases
- Boundary value encode/roundtrip tests (#471) — 41 values across all MessagePack integer format boundaries
- Encoding asymmetry tests (#473) — Rust decoder accepts GDScript-style signed encodings for unsigned fields
- Boundary fixture generation (#472) — 14 raw + 5 snapshot fixtures at integer format boundaries
- Malformed batch rejection test (#479) — truncated, garbage, and mixed payloads rejected atomically
- Per-fix determinism unit tests (#467) — equidistant NPC ordering, visible tile sorting, same-tile mover resolution
### Fixed
- Determinism: visible_ids HashSet → BTreeSet for stable iteration order (#456)
- Determinism: visible entities in snapshot sorted by entity_id (#457)
- Determinism: movers sorted by Entity bits in validate_movement (#458)
- Pause guard blocks all actions except Pause/Unpause while paused (previously only blocked movement)
### Changed
- Protocol version bumped from v7 to v8 (dialogue_response field in ObserverSnapshot)
### Added
- AudioManager autoload (#255, D-068/D-069/D-073) — 5-bus architecture (Music, Ambient, WorldSFX, PlayerActions, UISounds), directory-scan asset registry, spatial/non-spatial playback, audio dip profiles (dialogue, confrontation, listening_focus) with low-pass filter sweep, zone crossfade stub
- Dialogue response selection (#435, D-061/D-062) — structured options with response_id, priority sorting, max 3 visible, invisible locked options, RichTextLabel for BBCode support
- Walk-away mechanic (#437, D-064) — WASD triggers WalkAway input during dialogue, 300ms fade, dialogue_active flag gates movement, re-show on server interrupt
- Confrontation text styling (#436, D-063) — italic first-person options, 1.5s monologue beat with dialogue dim to 70%, audio dip via AudioManager, walk-away cancels in-flight beat
- MessagePack boundary value tests (#470) — 41 values (25 positive, 16 negative), encode-only header verification, roundtrip, Rust-style unsigned decode overlap tests
- Client P0 regression tests (#477) — monologue carry-forward (Bug #5), camera stability during pause (Bug #2)
- Client P1 tests (#478) — fog shader state (4), entity lifecycle (2), pending recognition blob (1)
### Changed
- Fog byte magic numbers replaced with named constants (#476) — VIS_HIDDEN/PERIPHERAL/FORWARD, EXP_UNEXPLORED/EXPLORED/VISIBLE in FogState
- Dialogue options now carry structured {text, response_id, priority, confrontation} instead of plain strings
- Protocol v7 dialogue decode validates and skips malformed options
### Added
- QA test architecture workshop complete — 3-round, 7-agent workshop producing 60 tickets (epic #455): Gauntlet test world (7 rooms, 48 entities), test client binary (tooling/test-client/), determinism fixes, content validation, make pre-pr pipeline, 38 client tests, anti-tedium features, human tester workflow
- Workshop skill updated — agents now write output files to disk instead of sending messages, fixing documenter access
- Camera anchor test suite — 10 gdUnit4 tests verifying camera init, smoothing cycle, and player tracking
- Entity position lerping — framerate-independent exponential smoothing so entities slide between tiles instead of snapping
### Fixed
- Server never sends snapshots — blocking TCP read in receive_bridge_inputs stalled the entire bevy Update schedule; switched to non-blocking I/O with WouldBlock handling
- Camera doesn't center on player at startup — Camera2D smoothed_camera_pos starts at (0,0); now disable smoothing during init, snap to player, re-enable after first anchored frame
- Player moves while game is paused — movement commands now discarded when TickRate is Paused (pause/unpause still process)
- Spacebar only pauses, doesn't toggle — added UNPAUSE action with toggle logic based on tick_rate state
- MessagePack encodes tick 128 as -128 — off-by-one in signed int boundary checks (<=128 instead of <128) across int8/16/32/64 branches
- Monologue/dialogue lost on snapshot overwrite — one-shot events now carried forward when a newer snapshot replaces an unconsumed one
- Fog shader white screen on load failure — ColorRect defaults to transparent, shader load failure logged instead of crashing
- Fog desync during camera smooth pan — fog rect now tracks camera position instead of player position
### Changed
- Server game loop throttled to ~20 ticks/sec (50ms frames) — non-blocking TCP loop no longer spins; remaining frame budget available for NPC AI
- Hold-to-move input model — movement polled each frame with stance-based throttle (Sprint=200ms, Walk=400ms, Careful=600ms, Crouch=800ms) and composite diagonals (W+D → northeast)
- D-053 updated with client throttle rates and input model documentation
### Added
- Dialogue box UI skeleton (#434, D-061) — bottom screen, max 20% height, ~65% width, NPC speech + max 3 response options, insert-styled colors, WASD walk-away with 300ms fade, no close button, diegetic on InsertOverlay z-layer 6
- Fog entity visualization (#431, D-059/D-060) — cognitive delay rendering: sonar-style sound pings (3 concentric rings, 1.5s fade), unrecognized grey blobs with 0.8s breathing pulse, D-033 color transition at 50% recognition progress, ±0.5 tile position drift, FogEntities node at z:950
### Changed
- Client protocol version bumped from 6 to 7 (pending_recognitions decode for cognitive delay)
- GameState: current_dialogue and pending_recognitions fields wired from ObserverSnapshot v7
- Scene tree: DialogueBox added to InsertOverlay, FogEntities at z:950 between fog shader and InsertOverlay
- Test mode: mock dialogue (Kael NPC, 3 options) and mock cognitive delay entity (6-tick recognition cycle)
### Added
- Archetype evidence presentation spec (#443, D-065/D-034/D-033) — detective case file vs smuggler notebook design document: item definitions, knowledge graph presentation, contradiction markers, THE FRIEND arc walkthroughs, systems interaction map, authoring guidelines
- Cognitive delay system (#423, D-060) — CognitiveDelay component buffers perception events before emitting KnowledgeEvents (0.6s base / 0.3s urgent at 10 tps), pending_recognitions in ObserverSnapshot v7 for client fog entity visualization, cancellation on entity LOS exit
- ListeningFocus eavesdrop system (#426, D-053) — stationary_ticks tracking for eavesdrop positioning bonus, 30-tick threshold (20 for Careful stance), Sprint blocks accumulation, registered after validate_movement
- YAML content loader with hot-reload (#326, D-028) — LinePool system parsing dialogue/monologue YAML into BTreeMap-indexed pools, 4-layer query filtering (access > situation > trust > topic+mood), timestamp-polling hot-reload (dev-only), graceful failure preserves previous content
- Line pool format specification (#308) — formal spec at docs/architecture/line-pool-format.md defining YAML structure, tag enums, 4-layer filtering pipeline, prerequisite-to-KG mapping, ID format, and Rust loader interface
- InteractionMemory KG schema design (#442, D-064) — design doc at docs/architecture/interaction-memory-schema.md extending FactKnowledge with interaction tracking, 5-state InteractionState enum, monologue prerequisite extension, NpcTolerance reconciliation
- Audio discussion decisions D-067 through D-074: recognition chime timing, 5-bus architecture, audio dip profiles, confrontation as cognitive vulnerability, monologue chime placeholder strategy, universal conversation murmur, zone crossfade, hybrid audio generation
- Asset pipeline documentation system (docs/assets/) with category-based index, sonic palette, and generation templates for audio, visual, and video pipelines
- Stable Audio Open connector and post-processing wrappers (audio-generate, audio-health, audio-post) with timeout handling for 11GB VRAM constraint
- gen-audio skill with prompt assembly system (sonic palette prefixes + category templates + asset descriptions)
- 6 interaction UI audio assets (#440): cursor_hover, weapon_aim, implant_open, fog_recognition, sfx_monologue_chime, sfx_monologue_chime_urgent — generated via SAO, needs duration trimming (#453)
- Dialogue/confrontation ambient dip implementation spec with full Godot AudioBus tween code
- Synthesis tooling (tooling/synth_ui_sounds.py) for programmatic insert-tech sound generation
### Changed
- Protocol version bumped from 6 to 7 (pending_recognitions field in ObserverSnapshot)
- MessagePack fixtures regenerated for protocol v7
- Monologue trigger system uses .values() iterator (clippy fix)
- Monologue schema: relationship prerequisite now requires target and state fields
- 389 tests total (70 new) — cognitive delay pipeline, line pool loader, ListeningFocus, content watching, serialization
- Renamed asset-gen skill to gen-image for consistent gen-* naming pattern
- Client protocol v6 bridge — decode player_stance (4 variants) and player_inventory from ObserverSnapshot, TOGGLE_STANCE_UP/DOWN input actions, 25 gdUnit4 tests
- Three-scope z-layer rendering pipeline (D-049) — world z:0-900 inside CanvasGroup, insert overlay CanvasLayer 10, UI CanvasLayer 20, modal CanvasLayer 30. Y-sort contract enforced, reserved VFX/airborne/lower-floor ranges documented
- Cursor state machine (#429, D-056) — 4 states (Default/EntityHover/ObjectHover/WeaponAim), 150ms transitions, insert-styled geometric shapes
- Fog shader rebuild (#430, D-059) — 5-layer fragment shader with animated Perlin noise, CanvasGroup compositing, FogState autoload for visibility/exploration textures
- Entity interaction list (#432, D-057) — vertical multi-verb menu, insert-styled, sprint suppression, diegetic toggle
- World radial menu (#433, D-058) — 2 spokes (Observe + Insert), drag-release and click-click input, 60-degree acceptance zones
- Inventory UI (#438, D-065) — 3x3 grid, 40x40px slots, 1-9 hotkey selection
- Stance indicator (#439, D-053) — color-coded HUD text, C/X keybinds
- Architecture docs: z-layer gap analysis, fog shader spec, flying taxi feasibility analysis
### Changed
- Scene tree restructured: Entities z_index 3->0 (critical y-sort fix), FloorObjects->10, YSortGroup->100, Overhead->300, FogOverlay->900, ModalLayer added
- constants.gd rewritten with three-scope z numbering and full reserved range documentation
- Fog renderer replaced: TileMapLayer-based fog_renderer.gd deleted, replaced by shader-based fog_shader.gd + fog.gdshader
### Added
- ObserverSnapshot v6 wire protocol (#449) — player_stance (MovementStance) and player_inventory (Vec\<InventoryItem\>) fields with serde defaults for backward compatibility
- Stance system (#417) — Sprint/Walk/Careful/Crouch movement stance with tick-based speed (1/2/3/4 ticks per move), monologue rate multipliers (40%/100%/150%/100%), PlayerMoveCooldown component, ToggleStanceUp/Down player actions
- TilePresence posture layers (#420) — Standing/Prone/Seated/Fixture occupancy layers enabling same-tile coexistence (e.g. seated NPC + standing player), layer-based collision in validate_movement
- ObjectType component (#421) — Readable/Container/Terminal/Door/Pickup/Furniture types with Phase 1 verb sets computed from type + proximity range
- Phase 2 verb filter (#422) — KG-gated observer-side verb processing: POI priority flips (D-060), Confront injection at KnowsDetails+ confidence, contradiction marking, archetype-specific label relabeling (Smuggler sees Move/Stash, Detective sees Scan/Flag on containers)
- CharacterArchetype component — Smuggler/Detective archetype for Phase 2 verb label differentiation (D-057)
- VerbKind::Confront — Phase 2 only verb injected when observer has KnowsDetails+ on an NPC at close range
- Smuggler inventory system (#424) — CarriedBy(StableId) component, Take/Place verbs, 9-slot (3x3 grid) capacity, auto-slot assignment, info boundary enforcement (carried items invisible to other observers)
- MovementProfile component (#418) — per-archetype default stance (smuggler=Walk, detective=Walk), applied on spawn, factory methods for future archetypes
- Sprint interaction buffer suppression (#419, D-055) — sprint stance explicitly clears interaction buffer, no verbs computed or sent during sprint, anomaly monologue pipeline unaffected
- Sprint anomaly double-take monologue (#428, D-055) — SprintAnomalyQueue component detects Contradicted entities during sprint, fires delayed retroactive monologue after ~1.5s ("Wait — something wasn't right back there"), first-in-wins queue semantics, 3 hardcoded v0.1 lines
### Changed
- Protocol version bumped from 5 to 6 (stance, inventory, ObjectType, verb system fields)
- MessagePack fixtures regenerated for protocol v6
- Input processing queries expanded for stance and cooldown components with backward-compatible Option wrapping
- Observer pipeline queries expanded for Stance and CharacterArchetype components
- NearbyInteraction carries object_type and contradicted fields for Phase 2 context
- BridgePlugin system ordering: process_sprint_anomaly_monologue runs after trigger_monologue, compute_observer_snapshot runs after anomaly processing
- Player spawn includes MovementProfile, Stance, PlayerMoveCooldown, and SprintAnomalyQueue components
- 331 tests total (131 new) — comprehensive QA coverage across stance, occupancy, Phase 2 verbs, sprint suppression, inventory, anomaly monologue, and wire format
### Added
- D-066: Dual-scale grid — 0.5m simulation tiles for stealth granularity, 1m visual tiles for proportional art (2x retina factor). All world geometry 2x2 sim tile minimum so cover/LOS maps 1:1 with visuals. Amends OQ-01.
- Sprint CLI (`db/connectors/sprint`) — unified sprint lifecycle management with 5 subcommands: status, start, stop, start-work, prepare. Auto-detects sprint from DB state and team from git branch. Guards prevent activating unplanned sprints.
- Shared permission settings in `.claude/settings.json` — git, ticket/sprint CLI, make, tea, and core skills pre-approved across all worktrees. Deny rules block destructive operations.
- Decisions D-053 through D-065 from Control & Interaction Workshop — formalized interaction verb system, contextual actions, NPC awareness model, and related design decisions
- Sprint 6 Touch briefings for server, client, copy, and joint teams
- Control & Interaction Workshop outputs — full workshop notes and outcomes
- Smuggler inventory item specs for transit district (#441)
- Start-workshop skill for multi-agent design workshops
### Fixed
- Added worktree boundary rules to CLAUDE.md — agents must stay within the git root, no navigating to sibling worktrees or above the repo
- Plan-sprint skill now enforces worktree-relative paths in generated briefings
- Worktree-update skill now discovers branches dynamically via `git worktree list` instead of relying on hardcoded branch names — fixes missed branches like `planning`
### Changed
- Start-sprint and plan-sprint skills updated to use sprint CLI instead of manual multi-query workflows
- Permission syntax migrated from deprecated `:*` suffix to modern space-wildcard format across all worktrees
- Internal monologue trigger system (#414) — enter_location fires on first tick, time_idle fires after 100 ticks of no movement, 300-tick cooldown, dedup within session, random line selection from content pools via ChaCha20 RNG
- MonologueEvent in ObserverSnapshot v5 — current_monologue field carries id, text, and display duration across the IPC bridge
- Client monologue display wiring — protocol v5 decoding, GameState extraction, HUD display pass-through
### Fixed
- NPC spawn missing Interactable component (#413) — NPCs spawned from content and proof room now have Interactable, enabling E-prompt detection
- PlayerAction::Interact was a no-op (#415) — changed from unit to struct variant with target_entity_id and verb fields, server logs interaction data
### Changed
- Protocol version bumped from 4 to 5 (MonologueEvent field, Interact variant change)
- MessagePack fixtures regenerated for protocol v5
### Changed
- UIStrings YAML parser now handles arbitrary nesting depth and inline comments — adapts to copy team's restructured ui-strings.yaml with multi-level sections (relationship_states, health_values)
- HUD uses new YAML keys: `hud.perception_mode_prefix`, `hud.time_prefix`, `hud.health` (empty prefixes display values directly)
- Interaction prompt keybind hint ("E") hardcoded instead of loaded from YAML — keybinding is not copywriter text
### Added
- Pre-commit FactId validation hook (#393) — grep-based check validates fact_id references in content YAML against canonical knowledge catalogs; advisory mode when catalogs are stubs, enforcing mode when populated
- Pre-commit hook infrastructure — `.config/hooks/` with modular dispatcher, `make setup-hooks` target, `core.hooksPath` config for worktree-safe hook installation
- `make check-fact-ids` target for manual fact_id validation
- UIStrings autoload with YAML-based UI string loading (#409) — minimal YAML parser, `get_text()` lookup with fallback-to-key
- HUD and interaction prompt labels now loaded from `client/data/ui-strings.yaml` instead of hardcoded strings
- Character voice speech patterns (#310) — sentence-level execution spec for smuggler and detective covering contractions, punctuation, stress markers, vocabulary, verbal tics, and authoring checklist
- NPC authoring style guide (#379) — 954-line handbook: tier budgets, 9 NPC patterns, dialogue/monologue rules, tag taxonomy, dual-lens coordination, Krenn/Sova culture, FRIEND phase mapping, validation checklist
- UI microcopy (#409) — 72 YAML strings for client integration: interaction verbs, relationship states, HUD labels, perception modes, notifications, knowledge panel, tutorial prompts
- Kael Davan FRIEND pack (#297) — 86 hand-authored lines across 3 locations, 5-phase relationship arc with contradiction scene, dual-lens notes
- Sera Venn FRIEND pack (#298) — 75 hand-authored lines at The Last Shift, trust-gated gossip, avoidance contradiction, contaminated trust arc
- PC-as-NPC content (#401) — 70 authored items enabling second-playthrough recognition (D-039 wow moment #4)
### Changed
- Moved ui-strings.yaml from content/campaigns/_meta/ to client/data/ for direct Godot client loading
### Fixed
- Fact ID format collision across FRIEND content — normalized 73 flat IDs to dotted category.topic format, fixing broken detective evidence chain from Sera to Kael observations
- Entity ref format inconsistency — normalized underscore format to npc:hyphenated across ~15 monologue references
- Phase tag inconsistency between FRIEND packs — standardized to phase-N format, added missing phase-2 tags to Kael ring ops lines
- Detective monologue gap for Kael — added 7 observation lines (7 → 15 total), added mood tags and bar_evening situation to Kael bar content
- Hoshe QA briefing updated with integration test coverage priorities — test harnesses, dedicated client-server test map, edge case focus
- Sprint 5 "Live" team briefings — copy (6 tickets), client (2), CI (1), joint coordination for content-at-scale sprint targeting FRIEND packs, voice patterns, NPC style guide, PC-as-NPC authoring, UI microcopy, FactId validation
- Live server mode (`make game`) — single command builds server, launches client with TCP connection, auto-kills server on exit; `make stop` helper for manual cleanup
- `SR_LIVE=1` environment variable switches SimBridge from test mode to real TCP server connection
- TileKind in wire protocol (#412) — server sends Floor/Wall/Door/Object per visible tile, client renders walls and floors in live mode
- Input roundtrip integration test (#411) — spawns real server, connects via TCP, validates full movement and interact pipeline
- Debug logging for received player inputs on server (visible with `RUST_LOG=debug`)
### Fixed
- Interaction prompt target+verb data now attached to Interact action in game loop (#405) — was TODO stub, server receives `{target_entity_id, verb}` payload
- Player entity detection uses `kind.variant == "Player"` instead of hardcoded `entity_id == 1` — fixes "player not found" warnings when connected to real server (which assigns different IDs)
- Movement keys now work in live mode — client was sending millisecond timestamps as input tick, server only processes ticks <= current frame counter; now uses server tick from latest snapshot
- Entity-to-tile alignment in live mode — server sends tile-center render coords (tile 16 → 16.5), entity renderer now floors to tile index before positioning
### Added
- Content loader Phase 2 (#408) — 2-phase spawn pipeline loads real YAML content into ECS entities: enums, entity attributes, pools, templates, triangles, NPC profiles with Want/Tolerance/Contentment/Personality/Tells/Skills axes plus cross-reference resolution for Secrets, Relationships, Information, and DailyRoutine
- Global enum YAML files (#387) — 9 enum definitions (situations, topics, moods, triggers, access-tiers, trust-tiers, activities, patterns, motivations) from D-035 taxonomy
- Entity attributes YAML (#388) — 16 canonical knowledge graph attribute keys from D-024 with A7 workshop updates
- Seed-time pools (#389) — 5 single-candidate pools for deterministic v0.1 testing
- Social site templates (#390) — 3 templates (logistics-hub, bar, smuggling-ring) with role slot definitions per D-025
- Triangle YAML files (#391) — 5 v0.1 triangles (3 active fork, 2 passive) per D-024 workshop synthesis
- Seed configuration schema design (#394) — design document defining game-start randomization: FRIEND selections, pool draws, template assignments, entanglement config, ChaCha20 RNG protocol
- YAML to RON converter tool (#403) — build-time converter in tooling/content-converter/, runs via `make content-ron`
- Line previewer CLI (#407) — 4 subcommands (dialogue, monologue, coverage, sequence) for content authors to test line selection without running the full game
- Happiness added to WantKind enum — Harek remapped from Safety to Happiness
### Fixed
- Dialogue-pool schema corrected to use arrays for situation/topic/mood per D-035 (were incorrectly single strings)
- NPC want.primary changed from narrative strings to WantKind enum keywords — fixes silent Want component drop at spawn time
- Schema enum constraint added to npc-profile.schema.json for want.primary validation
### Added
- Sprint 4 "Feel" team briefings — copy (8 tickets), server (9), client (1 carry-over), CI (1), joint coordination
- Interaction prompt system (#405) — server-driven "E - Talk" prompt decoding v4 nearby_interactions with nested VerbOption structs, fade animation, extensible get_interaction_target/get_selected_verb interface for future radial verb menu
- Art direction & mood board workshop (3 rounds + closing) — 4-agent team establishes visual identity, 16 art direction principles, 9 mood board images, 10 candidate decisions (D-042–D-051)
- 3D-to-2D sprite render pipeline (`client/tooling/sprite_renderer/`) — Godot @tool scene renders textured 3D models at "the angle" (-72.5deg ortho) from 4 cardinal directions at 1024/256/64 resolutions with outline applied at working resolution
- `/render-sprite` skill — CLI wrapper for the render pipeline with headless import step
- Pipeline POC: Era 1 institutional wall + bar green wall textures generated via Nano Banana and rendered through full pipeline
- PerceptionQuery trait and ActivePerceptionMode resource — abstraction layer for D-017 perception mode swapping (NaturalVision default implementation)
- VisibilityGeometry intermediate resource decoupling FOV computation from entity filtering
- Client-side PROTOCOL_VERSION enforcement — snapshot decoder rejects version mismatches with error log
- POI verb priority test in observer pipeline — asserts both verb kind and priority values end-to-end
### Changed
- Observer pipeline decomposed into two-stage system: compute_visibility_geometry (geometry) → compute_observer_snapshot (entity filtering + assembly)
- POI verb priority adjustment moved from simulation phase (interaction.rs) to perception phase (observer) — fixes D-010 information boundary violation
- compute_nearby_interactions no longer reads KnowledgeGraph — determines verb availability by proximity only, verb priority adjusted by observer
- compute_nearby_interactions scheduling moved from SimulationPlugin to BridgePlugin for explicit ordering with geometry and observer systems
- Client test snapshot updated to v4 format (Protocol.PROTOCOL_VERSION, tick_rate replaces paused)
### Added
- Content validation tooling — `make validate-content` validates campaign YAML files against JSON schemas, maps files by directory context
- Entity::to_bits() roundtrip test — guards against bevy version changes silently breaking wire IDs
- TickRate switch mid-accumulation test — verifies Half→Full→Paused→Half transitions preserve accumulator state
- Tick rate scaling system (#406, D-052) — Full/Half/Paused rates with fractional accumulation, SetTickRate player action, replaces binary pause flag
- Proximity detection and interaction verbs (#404, D-060) — compute_nearby_interactions system with Manhattan distance ranges (close ≤2, mid ≤5), context-sensitive verb computation (Talk, Observe, Examine), PersonOfInterest priority flip, NearbyInteraction in ObserverSnapshot v4
- Content directory skeleton (#385, D-057) — district-as-atomic-pack layout with Sova Transit first district, 17 NPC stubs, 3 locations, 5 triangles, dialogue/monologue pools, factions, knowledge catalogs, enum definitions
- Content schema definitions (#386) — 8 JSON Schema files (draft 2020-12) for district, location, npc-profile, dialogue-pool, monologue-pool, triangle, routine, fact-catalog validation
- PROTOCOL_VERSION constant in bridge types — versioning strategy documented (subprocess IPC, serde defaults for field evolution)
- Campaign, system, station JSON schemas for hierarchical content validation
### Fixed
- Test suite aligned with server v4 protocol enforcement — all hand-built snapshots include version field, verb priorities 1-indexed, ExamineNpc label corrected to "Observe"
- E2E proof tests resilient to entity ordering — player found by kind instead of array position, wall-hides test checks specific NPC position instead of total count, supports 3-NPC proof room layout
- Wire entity_id now uses StableId consistently across observer, observation, interpretation, and interaction systems (was Entity::to_bits() in some paths)
- Restored system/station/district hierarchical fields in district metadata (incorrectly removed during canonical_id cleanup)
- Unregistered entities in observer/interaction now log tracing::error instead of silently falling back to Entity::to_bits()
### Changed
- NearbyInteractionBuffer refactored from global Resource to per-entity Component on PlayerCharacter — multiplayer-ready (D-009)
- Observer module split into mod.rs (244 lines) + tests.rs (480 lines) — reduces module complexity
- Content directory restructured from flat districts/ to hierarchical campaigns/main/systems/krenn/stations/sova/districts/transit/ — path mirrors canonical IDs, glob-based discovery, multi-campaign/DLC ready
- Content manifest (content.yaml) rewritten for glob-based district discovery
- District identity fields (system, station, district) now derived from directory path — removed from district.yaml required fields
- NPC canonical_id schema accepts district-scoped IDs (npc:transit.kael-davan) for cross-district uniqueness
- ObserverSnapshot protocol bumped to v4 — adds nearby_interactions field, tick_rate replaces paused field in GameTime
- NearbyInteractionBuffer.interactions is now private with take() accessor (no per-frame clone)
- NearbyInteraction.distance changed from f32 to u32 (matches manhattan distance)
- Missing PlayerCharacter in input processing now panics instead of silent no-op
- Verb sort uses (priority, kind) tuple for deterministic ordering at equal priority
- Unregistered entity in knowledge events triggers debug_assert + error (was warn)
- District schema: canonical_id is now optional (derived from directory path at load time)
- Skills trimmed for CLAUDE.md deduplication — search-docs (-48%), ticket (-17%), review-pr (-35%) now reference CLAUDE.md for basics instead of repeating them
- Review-pr reviewer profiles extracted to `references/reviewer-profiles.md` for progressive disclosure
### Removed
- Dead generate_snapshot function in bridge/mod.rs — superseded by compute_observer_snapshot
- content/global/regions/ directory — region data absorbed into system.yaml metadata
### Added
- Dual Lens Authoring Guide — 7-chapter reference for writing content that works for both smuggler and detective perspectives (D-027, D-028, D-032, D-034, D-035)
- THE MIRROR pattern spec — transparency-as-contrast NPC design with Naia Tamm reference implementation and generator template
- Smuggler voice card — register parameters, 5 voice anchors, 4 anti-patterns, paired comparison examples, display constraints, authoring checklist
- Smuggler moral arc spec — 4-phase trajectory (Comfort, Doubt, Reckoning, Compromise), FactId gates, monologue trigger rules, Kael intersection mapping
- PC-as-NPC unified spec — starting knowledge/relationship graphs, tell inversion, orientation monologue, 9-step conversion checklist, v0.1 smuggler + detective briefs
- Triangle 1 Hub Power Volume Escalation fork — 3-path smuggler decision (escalate/stabilize/mediate), ~41 authored dialogue lines, NPC state change tables
- Content directory structure design doc — runtime content/ layout, canonical ID format, 8 JSON Schema specs, 3-tier validation pipeline, migration path from wiki
- Interaction verb spec — 7 v0.1 verbs (Move, Look, Monologue, Examine Object, Examine NPC, Talk, Overhear), priority resolution, server pipeline architecture
- v0.1 wow moments checklist — maps all 6 D-039 moments to content deliverables, tickets, dependencies, completion status
- Nils Davan off-stage NPC stub — ring coordinator, GHOST + HANDLER pattern, lattice message design, relationship map
- NPC pattern/motivation mapping applied to all 18 NPC wiki pages with composition reads
- Structured NPC data model (#86) — replaced stub string/f32 fields with typed enums and integer types for D-010 determinism (WantKind, SecretSeverity, Skill, PersonalityTrait, CombatStyle, RelationshipKind)
- Global RelationshipGraph resource (#87) — BTreeMap with tuple key for efficient prefix queries and reverse lookups
- A* pathfinding system (#237) — PathRequest/ComputedPath/PathBlocked components with cardinal-neighbor A* and manhattan heuristic
- NPC path following system (#238) — MovementSpeed throttling, per-tick path advancement with MoveIntent creation
- Daily routine system (#88) — NpcPlugin with PreviousDayPhase resource and check_phase_transition system issuing PathRequests at day-phase boundaries
- Multiple NPC spawning (#84) — 3 distinct NPCs (dock worker, field tech, guard) with full component bundles and RelationshipGraph edges
- Observation event generator (#239) — RoutineDeviation, Absence, and NewEntity triggers from comparing visible snapshot against NPC routines and knowledge state
- Review-pr skill routes reviewers by branch type — server/client get Hoshe+Tyre, copy gets Hoshe+Paula+Miri, visual gets Hoshe+Araminta, audio gets Hoshe+Ozzie
- Start-sprint skill spawns team agents from sprint briefings — parses the Agents line, creates a team with tasks from tickets, and launches all listed agents as background teammates
### Fixed
- IPC error handling (#341) — DeserializationWithDump/MutexPoisoned error variants, hex dump logging on deserialization failure, graceful error classification in bridge I/O systems
- NpcPlugin system ordering — routine phase transitions now run before pathfinding so PathRequests are picked up same frame
- Stale doc comment in observation event generator — system runs before knowledge updates, not after
- Clippy warnings from Rust 1.93 — derive Default, is_multiple_of, collapsible if
### Changed
- Hael renamed to Naia Tamm across all wiki files (16 files updated)
- Canonical full names applied to 11 single-name NPCs (Voss→Arvo Voss, Devra→Devra Talsen, etc.)
- Location shortcodes standardized in monologue guide (hub_m_ → terminal_m_ per D-036)
- Entity attributes updated to 16 canonical keys — 4 new role-perspective keys (risk_assessment, loyalty_assessment, position_integrity, moral_weight), secret_held→leverage_held rename
- Drin Rosta expanded from Tier 3 to Tier 2 — full 10-axis profile, 6 voice lines, Triangle 2 + Triangle 5 roles
- NPC index roster table expanded with Pattern and Motivation columns
- Sprint 3 "Know" team briefings regenerated from database — all four files (server, client, joint, copy) now match actual sprint 3 ticket assignments
- Ticketing database moved to shared worktree location (`../settledreach.db`) — eliminates binary merge conflicts across branches
- All Python connectors use script-relative path resolution instead of `$REPO_ROOT` env var or git rev-parse
- `$REPO_ROOT` environment variable removed — all scripts, skills, and docs use relative paths
- Merged copy branch — 39 tickets, wiki review + content scoping workshops, game world glossary
- Merged maintenance branch — worktree-update skill
### Added
- `make db-backup` / `make db-install` — database backup to `docs/backups/` (main-only) and restore for new clones
- Worktree-update skill backs up the shared database after merges on main
### Removed
- `db/commonwealth.db` from git tracking (replaced by shared `../settledreach.db`)
- `$REPO_ROOT` env var from all 9 worktree `settings.local.json` files
### Added
- Worktree-update skill — non-destructive branch sync with PR detection and conflict safety
- Game world wiki — 45-file glossary covering Sova Transit District: 17 NPCs, 5 triangles, 3 social sites, 7 factions, knowledge vocabulary
- Wiki Review workshop (4 rounds + lead interview) — 300-world generator model, cultural ingredients menu, three-system NPC architecture, Sacred/Profane/Middle Kingdom framework
- v0.1 Content Scoping workshop (2 rounds + closing) — 16 EntityKnowledge keys, mechanical NPC mapping, YAML content format, 7-verb interaction model, server-authoritative pause, 20 decisions (D-042–D-061)
- 39 implementation tickets (#371–#409) from content scoping workshop — copy 21, server 13, client 2, ci 1
- Control & Interaction workshop brief (queued)
- Large content push team pattern in CLAUDE.md
- Inigo sound designer agent — soundscape design, ambient layers, diegetic cues, D-018 audio propagation
- Team-agent mapping in sprint planning — each team has defined default agents for briefing assignment
- Sprint skills recognize all team branches (server, client, copy, audio, visual, ci)
- Sprint 3 "Know" team briefings — server (6 tickets), client (2 tickets), joint (2 split tickets), copy (1 carry-over)
- Copy team sprint briefing for Sprint 2 (#368 knowledge vocabulary)
- Sprint 2 proof: fog of perception E2E tests (#357) — 3 tests verifying all 7 acceptance criteria through real server pipeline (movement, tiles, fog, wall hiding, corner reveal)
- Server proof room — wall at (16,14) between player at (16,16) and NPC at (16,13) for LOS testing
- Dynamic test snapshot in SimBridge — tracks player position from queued inputs, Bresenham LOS, Manhattan-distance visibility for standalone demo mode
- 4 Bresenham LOS unit tests — clear path, wall blocked, diagonal, same position (PR #13 review)
### Fixed
- Type safety in GameState visible_tiles loop — validates Dictionary with x/y keys before access (PR #11 review)
- Consistent reset_test_state() usage across all test files (PR #13 review)
- E2E connection loop now detects server process death early (PR #13 review)
- Corner reveal test verifies NPC position at (16.5, 13.5) (PR #13 review)
- Entity renderer skips redundant modulate.a writes when alpha unchanged (PR #11 review)
### Added
- Observer snapshot knowledge integration (#366) — VisibleEntity carries relationship state (D-033 color) and observation type (Visible/Remembered), remembered entities appear as fog ghosts at last known position
- Knowledge graph system (#361, #362, #363, #365) — per-entity KnowledgeGraph component (D-041), StableEntityId + EntityRegistry, KnowledgeEventQueue, decay system, 4-level confidence hierarchy
- Direct observation knowledge flow (#364) — perception emits DirectObservation/LeftLOS events to knowledge graph, entities entering/leaving LOS tracked
- Protocol v2 decoder — extracts game_time, player_facing, visible_tiles, and per-entity visibility sectors from ObserverSnapshot v2
- D-033 entity color palette (#130) — relationship-based colors (teal/green/amber/red), Phase 1 defaults by entity kind
- Peripheral vision dimming — entities in peripheral vision rendered at 50% alpha (D-015)
- Player facing direction indicator — Polygon2D triangle on player entity showing 8-directional facing
- GameState v2 fields — game_time, player_facing, visibility_sectors stored from snapshot data
- Test snapshot updated to v2 format with visibility sectors, game_time, and player_facing
- Observer visibility query (#112) — replaces unfiltered generate_snapshot with LOS-filtered compute_observer_snapshot combining shadowcasting + vision cone
- Vision cone system (#111) — forward/peripheral/blind sectors per D-015, Facing component updated on movement
- Symmetric shadowcasting (#110, #359) — Albert Ford algorithm with rational fraction slopes, benchmarked 1.2-10.5x faster than recursive, symmetry guaranteed (D-035)
- ObserverSnapshot v2 schema (#358, #25) — version field, GameTime, FacingDirection, VisibleTile, VisibilitySector types, visibility tag on entities
- D-035 decision record — symmetric shadowcasting selected over recursive (resolves Q-018)
- Tile rendering engine (#129) — programmatic TileSet with floor/wall/door/object placeholders, renders from snapshot tile data
- Fog overlay rendering (#131) — three visibility states (visible/fog-edge/hidden) via TileMapLayer overlay
- Camera lock to character (#116) — Camera2D smoothing at 2x zoom, locked to player position (D-015)
- Test room environment — 8x8 room with corridor and Manhattan-distance visibility for development without server
- `ticket team` command and `--team` filter — comma-separated team assignment for tickets (server, client, joint, content)
### Changed
- All instruction files (CLAUDE.md, skills, agent files) now use `$REPO_ROOT` env var instead of `git rev-parse --show-toplevel` — pre-set per worktree via `.claude/settings.local.json`
- `start-sprint` skill now requires plan mode — agent must create and get approval for a concrete sprint plan before starting implementation
### Fixed
- Entity renderer protocol field mismatches — "id"→"entity_id", "type"→"kind.variant", "position"→x/y fields now match Protocol.decode_entity() output
- Entity centering — entities (24x24) now centered within 32px tiles instead of top-left aligned
- `start-sprint` skill uses `git rev-parse --show-toplevel` for worktree-safe absolute paths — fixes "No such file or directory" errors on team branches
### Changed
- Background clear color set to near-black for unexplored areas (was default Godot gray)
- Scene render order: Tiles → FogOverlay → Entities (fog covers tiles, entities render on top)
- FogOverlay node type changed from Node2D to TileMapLayer for tile-based fog rendering
- GameState now stores visible_tiles and visible_positions from snapshots
- Test snapshot includes player entity (kind "Player"), second NPC entity, tile data, and visibility data
### Added
- Sprint 2 "See" briefings (server, client, joint) — fog of perception through the bridge
- `/plan-sprint` skill — automates sprint planning workflow and briefing file generation
- `ticket show` multi-ID support and `--brief` flag for compact human-readable output
- Q-018 through Q-023 — 6 open questions from architecture audit (shadowcasting, entity ID stability, collision resolution, tick overflow, pathfinding cache, debug visualization)
- 5 architecture spike workshop briefs (knowledge graph, observer pipeline, NPC AI state machines, save/load, map authoring)
- 17 tickets from architecture audit (#339-#355) — 7 Sprint 1 tasks, 5 Sprint 2+ tasks, 5 workshop epics
- End-to-end connection test (#81) — GDScript test spawning Rust server, connecting via LocalBridge, sending MoveNorth input, verifying player movement in snapshot response (D-030 Layer 3)
- Batch input encoding (Vec\<PlayerInput\> wire format) — Protocol.encode_player_inputs() batches all inputs per tick into one framed message matching server expectations
- EntityKind::Player fixture — snapshot_player.msgpack for cross-language testing, multi-entity fixture updated to include all 4 entity kinds
- 7 new tests (4 batch encoding, 1 framed batch roundtrip, 1 Player fixture decode, 1 E2E connection), 43 total client tests passing
- LocalBridge GDScript TCP transport (#79) — 4-byte big-endian length-prefix framing matching Rust server, StreamPeerTCP wrapper with partial read handling
- ServerProcess subprocess manager — spawns/stops Rust server via OS.create_process(), auto-cleanup on destruction
- SimBridge live transport integration — _process() polling loop for TCP receive/send, connection state machine (DISCONNECTED → CONNECTING → CONNECTED → ERROR)
@@ -116,6 +587,9 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
- Rejected alternatives R-004 through R-010 documented (pure Bevy, pure Godot, GDExtension, C++ GDExtension, Fyrox, custom framework, protobuf)
### Fixed
- create-skill references to nonexistent init_skill.py and package_skill.py scripts
- SimBridge wire format: inputs now batch-encoded as Vec\<PlayerInput\> array per server protocol (was sending individual inputs per frame)
- SimBridge server args: positional address format ("127.0.0.1:9876") matching server CLI, port default corrected to 9876
- Wire format mismatch: LocalBridge now uses rmp_serde::to_vec_named() (named maps) matching client Protocol.gd expectations
- EOF on bridge read now returns BridgeError::Transport for disconnect detection instead of empty Vec
- WalkabilityMap rewritten from flat Vec to chunk-based HashMap per D-012 architecture (Tyre review)
@@ -137,6 +611,7 @@ Format based on [Keep a Changelog](https://keepachangelog.com/).
- dotfiles/tmux.conf (no longer needed)
### Changed
- Q-018 (shadowcasting algorithm selection) resolved via D-035
- All 18 agent briefings updated for decisions/ directory split and DEVOPS.md references
- Implementation agents (Dudley, Hoshe, Justine, Oscar, Si, Stig, Tyre) now include Development Workflow sections with Makefile targets
- Q-009 (time system) resolved via D-031
+50 -10
View File
@@ -19,10 +19,9 @@ docs/
briefings/ # Per-agent context briefings (maintained by Qatux)
architecture/ # Technical architecture documents
design/ # Game design documents
sprints/ # Sprint briefings per team (server.md, client.md, joint.md, content.md)
sprints/ # Sprint briefings per team (server.md, client.md, copy.md, joint.md, etc.)
workshops/ # Workshop briefs and outputs (per-workshop subdirectories)
db/
commonwealth.db # SQLite ticketing database
schema.sql # Database schema
connectors/ # Connector scripts for SQLite and Qdrant
config.json # Endpoint configuration
@@ -34,10 +33,10 @@ db/
skills/ # Skill definitions
decisions/ # Decision domain files (source of truth)
README.md # Domain index and query examples
architecture.md # D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031
perception.md # D-011, D-015, D-016, D-017, D-018, D-019
content.md # D-023, D-024, D-025, D-028, D-029
scope.md # D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027
architecture.md # D-008, D-009, D-010, D-012, D-020, D-026, D-030, D-031, D-041, D-042, D-054, D-055, D-066
perception.md # D-011, D-015, D-016, D-017, D-018, D-019, D-033, D-035, D-043-D-049, D-052, D-056-D-061
content.md # D-023, D-024, D-025, D-028, D-029, D-032, D-034-D-037, D-050, D-062-D-064
scope.md # D-001, D-003, D-005, D-006, D-007, D-013, D-014, D-027, D-038, D-039, D-051, D-053, D-065
process.md # D-004, D-021, D-022
questions.md # Q-001 through Q-011
rejected.md # R-001 through R-010
@@ -51,16 +50,49 @@ See [docs/DEVOPS.md](docs/DEVOPS.md) for build, test, lint, and CI procedures. A
## Agent Instructions
### Worktree boundaries
This project uses **git worktrees** in a shared parent directory (`settled-reach/`). Each team branch (`server`, `client`, `copy`, `audio`, `visual`, `ci`) is checked out in its own worktree under that parent. The parent directory also contains shared resources like the ticketing database.
Each worktree contains the full repository: `server/` (Rust backend), `client/` (Godot client), `docs/`, `decisions/`, etc. The worktree root IS the git root — use `git rev-parse --show-toplevel` if in doubt.
Unless there is a direct instruction or a functional need (e.g. accessing the shared database in the parent directory), **all work must remain within the scope of the git root Claude is running in.**
- All file paths are relative to the worktree/git root (e.g. `server/src/bridge/types.rs`, `client/scripts/rendering/fog.gd`).
- Do not navigate to or access sibling worktrees in the parent directory (`../client/`, `../copy/`, etc.) unless explicitly instructed.
- Do not navigate above the git root unless explicitly instructed.
### Database
The ticketing database (`settledreach.db`) lives in the **parent directory** shared across all worktrees — it is not tracked in git. A backup is committed to `docs/backups/settledreach.db.backup` via main only.
### Before starting work
1. Read your sprint briefing at `docs/sprints/sprint-N/{team}.md` for current tasks
2. Use `db/connectors/ticket show <id>` for full ticket details
3. Read the relevant `decisions/*.md` domain file(s) referenced in the briefing
4. Background context: `docs/briefings/{your-name}.md`, `docs/discussions/`
### SQLite access
**Never use the `sqlite3` CLI** - it crashes in Claude Code due to a known std::bad_alloc bug.
### Ticket and database access
**Prefer the ticket CLI over raw SQL.** The CLI handles column names, joins, and output formatting correctly:
```bash
db/connectors/ticket list --sprint 2 --team server
db/connectors/ticket show 78
db/connectors/ticket sprint --active
```
Use the whitelistable wrapper scripts:
### Sprint CLI
**Use the sprint CLI for sprint-scoped operations.** It batches ticket queries and formats output for agent consumption:
```bash
db/connectors/sprint status # Current sprint progress
db/connectors/sprint status --team server # Team-scoped view
db/connectors/sprint start-work --team client # Full context dump for starting work
db/connectors/sprint prepare # Prepare next sprint (candidates + gaps)
db/connectors/sprint start # Activate a planned sprint
db/connectors/sprint stop # Complete an active sprint
```
Team is auto-detected from the current git branch (if not `main`). Sprint is auto-detected from DB state.
Only fall back to raw SQL for queries the CLI doesn't support. **Never use the `sqlite3` CLI** — it crashes in Claude Code due to a known std::bad_alloc bug. Use the wrapper scripts instead:
```bash
db/connectors/sqlite-query "SELECT * FROM tickets WHERE status='in_progress'"
db/connectors/sqlite-exec "UPDATE tickets SET status='done' WHERE id=1"
@@ -100,7 +132,7 @@ Key rules:
- **All flags must be explicit** — omitting `--login` or `--repo` triggers interactive prompts that crash in Claude Code (no TTY)
- **Use `--output simple`** for machine-readable output (no table borders)
- **`tea pr reject` does not work on your own PRs** — use `tea comment` instead
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
- **Never delete protected branches:** `main`, `maintenance`, `server`, `client`, `copy`, `audio`, `visual`, `ci` are protected on Gitea. Do not use `tea pr clean`, `git push --delete`, or `git branch -D` on these branches.
### File conventions
- Decisions: domain files in `decisions/` (see `decisions/README.md` for index)
@@ -127,6 +159,14 @@ tea pr create \
--head branch-name
```
### Large content pushes (team pattern)
When producing many files (wiki pages, content batches, bulk docs):
1. **Lore librarian** agent (read-only): ingests all source material, answers focused context queries from writers, tracks cross-file consistency
2. **Multiple writer** agents (parallel, by domain): each gets a task slice, writes directly to disk using the Write tool — one file at a time, write often, no text accumulation
3. **Reviewer** agents (blocked until writing done): check voice consistency, attribute uniformity, style
Key: writers use Write tool directly (no transcription bottleneck), librarian catches contradictions early, split work by domain not volume.
### Local services
- Gitea: `http://git.schweitz.internal` (login: `schweitz`)
- Qdrant: `http://tower-of-joy:6333/`
+175 -6
View File
@@ -1,7 +1,12 @@
GODOT := $(shell command -v godot4 2>/dev/null || command -v godot 2>/dev/null)
.PHONY: help setup build client server test lint ci ci-client ci-server clean \
decisions-sync decisions-coverage decisions-active decisions-orphan
.PHONY: help setup build client server game stop test lint ci ci-client ci-server clean \
decisions-sync decisions-coverage decisions-active decisions-orphan \
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 \
checklist-validate checklist-generate
# --- Configuration ---
@@ -13,7 +18,9 @@ help:
@echo ""
@echo " make setup Install dev dependencies (Rust, Godot, tooling)"
@echo " make build Build client and server"
@echo " make client Run the Godot client"
@echo " make game Build and run the full game (server + client)"
@echo " make stop Stop any running server instance"
@echo " make client Run the Godot client (test mode)"
@echo " make server Run the Rust simulation server"
@echo " make test Run all tests"
@echo " make lint Run all linters"
@@ -22,16 +29,34 @@ help:
@echo " make ci-server Run server CI checks"
@echo " make clean Remove build artifacts and caches"
@echo ""
@echo " make db-backup Backup shared database to git (main only)"
@echo " make db-install Restore shared database from backup"
@echo ""
@echo " make decisions-sync Sync decisions/*.md into SQLite"
@echo " make decisions-coverage Decision-to-ticket coverage by domain"
@echo " make decisions-active List active decisions"
@echo " make decisions-orphan Decisions without implementing tickets"
@echo " make validate-content Validate content YAML against schemas"
@echo " make check-fact-ids Check fact_id references against knowledge catalogs"
@echo " make content-ron Convert content YAML to RON (build-time)"
@echo " make fixtures-client Generate GDScript->Rust cross-encoder fixtures (#475)"
@echo " make golden-diff Show diff if golden file output has changed"
@echo " make golden-update Regenerate golden file and stage for commit"
@echo " make checklist-validate Validate checklist YAML against schema"
@echo " make checklist-generate Validate checklists + print condition summary"
@echo ""
@echo " make pre-pr Run all pre-PR checks (lint, build, test, validate, fixtures)"
@echo " make pre-pr-server Server-scoped pre-PR (lint, build, test, fixtures)"
@echo " make pre-pr-client Client-scoped pre-PR (lint, build, test)"
@echo " make pre-pr-content Content-scoped pre-PR (schema + cross-ref validation)"
@echo ""
@echo " make setup-hooks Install pre-commit hooks (included in setup)"
@echo ""
@echo " GODOT_VERSION=4.6 make setup Override Godot version"
# --- Setup ---
setup: setup-rust setup-godot setup-tooling decisions-sync
setup: setup-rust setup-godot setup-tooling setup-hooks decisions-sync
@echo "Dev environment ready."
setup-rust:
@@ -45,6 +70,12 @@ setup-tooling:
@command -v python3 >/dev/null 2>&1 || { echo "Install Python 3"; exit 1; }
@command -v curl >/dev/null 2>&1 || { echo "Install curl (required for Godot download)"; exit 1; }
@command -v unzip >/dev/null 2>&1 || { echo "Install unzip (required for Godot download)"; exit 1; }
@python3 -c "import yaml" 2>/dev/null || { echo "Install PyYAML: pip install pyyaml"; exit 1; }
@python3 -c "import jsonschema" 2>/dev/null || { echo "Install jsonschema: pip install jsonschema"; exit 1; }
setup-hooks:
@git config core.hooksPath .config/hooks
@echo "Git hooks path set to .config/hooks"
# --- Build ---
@@ -55,7 +86,7 @@ build-server:
build-client:
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
$(GODOT) --headless --path client --quit
$(GODOT) --headless --path client --import --quit
# --- Run ---
@@ -66,6 +97,19 @@ client:
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
$(GODOT) --path client
game: stop build
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
@echo "Starting server..."
@cd server && cargo run &
@sleep 2
@echo "Starting client..."
@SR_LIVE=1 $(GODOT) --path client
@$(MAKE) stop
stop:
@lsof -ti :9876 | xargs -r kill 2>/dev/null || true
@echo "Stopped any running server on port 9876"
# --- Test ---
test: test-server test-client
@@ -76,6 +120,41 @@ test-server:
fixtures:
cd server && cargo test --test gen_fixtures -- --ignored
fixtures-client:
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
@echo "Generating GDScript fixtures for Rust decoder..."
$(GODOT) --headless --path client -s res://tests/gen_client_fixtures.gd
@echo "Verifying Rust can decode GDScript fixtures..."
cd server && cargo test --test serialization gdscript_generated_fixtures
@echo "--- Fixtures-client: PASS ---"
golden-diff:
@echo "Regenerating golden output for comparison..."
@mkdir -p .cache/golden
@cp server/tests/golden/proof_room_tick_10.json .cache/golden/before.json
@cd server && UPDATE_GOLDEN=1 cargo test --test golden_suite -- proof_room_tick_10_matches_golden; \
rc=$$?; \
cp ../server/tests/golden/proof_room_tick_10.json ../.cache/golden/after.json 2>/dev/null; \
cp ../.cache/golden/before.json ../server/tests/golden/proof_room_tick_10.json; \
if [ $$rc -ne 0 ]; then echo "Cargo test failed (golden file restored)."; exit $$rc; fi
@if diff -q .cache/golden/before.json .cache/golden/after.json >/dev/null 2>&1; then \
echo "--- Golden file: UP TO DATE ---"; \
else \
echo "--- Golden file has CHANGED ---"; \
echo ""; \
diff --color -u .cache/golden/before.json .cache/golden/after.json || true; \
echo ""; \
echo "Run 'make golden-update' to accept changes."; \
exit 1; \
fi
golden-update:
@echo "Regenerating golden file..."
@cd server && UPDATE_GOLDEN=1 cargo test --test golden_suite -- proof_room_tick_10_matches_golden
@git add server/tests/golden/
@echo "--- Golden file updated and staged ---"
@echo "Review with: git diff --cached -- server/tests/golden/"
test-client:
@test -n "$(GODOT)" || { echo "Godot not found. Run 'make setup' first."; exit 1; }
$(GODOT) --headless --path client -s res://addons/gdUnit4/bin/GdUnitCmdTool.gd --ignoreHeadlessMode -a res://tests/
@@ -93,6 +172,70 @@ lint-client:
@echo "Checking GDScript for errors..."
@$(GODOT) --headless --path client --quit 2>&1 | grep -i "SCRIPT ERROR" && { echo "GDScript errors found"; exit 1; } || echo "No script errors found"
# --- Pre-PR verification ---
pre-pr: pre-pr-lint pre-pr-build pre-pr-test pre-pr-validate pre-pr-fixtures
@echo ""
@echo "=== PRE-PR: ALL CHECKS PASSED ==="
@echo "Safe to create PR."
pre-pr-lint: lint-server lint-client
@echo "--- Lint: PASS ---"
pre-pr-build: build-server build-client
@echo "--- Build: PASS ---"
pre-pr-test: test-server test-client
@echo "--- Tests: PASS ---"
pre-pr-validate: validate-content check-fact-ids
@echo "--- Content validation: PASS ---"
pre-pr-fixtures:
@echo "Checking Rust->GDScript fixture staleness..."
@cd server && cargo test --test gen_fixtures -- --ignored
@if git diff --quiet client/tests/fixtures/; then \
echo "--- Rust fixtures: UP TO DATE ---"; \
else \
echo ""; \
echo "--- RUST FIXTURES STALE ---"; \
echo " Protocol changed but fixtures not regenerated."; \
echo " Stale fixtures make all client tests FALSE POSITIVES."; \
echo ""; \
echo " Changed files:"; \
git diff --stat client/tests/fixtures/; \
echo ""; \
echo " Fix: commit the updated fixtures with your protocol change."; \
exit 1; \
fi
@echo "Checking GDScript->Rust fixture staleness..."
@$(MAKE) fixtures-client
@if git diff --quiet server/tests/fixtures/gdscript/; then \
echo "--- GDScript fixtures: UP TO DATE ---"; \
else \
echo ""; \
echo "--- GDSCRIPT FIXTURES STALE ---"; \
echo " Protocol changed but GDScript fixtures not regenerated."; \
echo " Stale fixtures make cross-encoder tests FALSE POSITIVES."; \
echo ""; \
echo " Changed files:"; \
git diff --stat server/tests/fixtures/gdscript/; \
echo ""; \
echo " Fix: commit the updated fixtures with your protocol change."; \
exit 1; \
fi
# Branch-specific variants (faster, scope-appropriate)
pre-pr-server: lint-server build-server test-server pre-pr-fixtures
@echo "=== Server pre-PR: PASSED ==="
pre-pr-client: lint-client build-client test-client
@echo "=== Client pre-PR: PASSED ==="
pre-pr-content: validate-content check-fact-ids checklist-validate
@echo "=== Content pre-PR: PASSED ==="
# --- CI (run locally) ---
ci: ci-server ci-client
@@ -101,6 +244,14 @@ ci-server: lint-server build-server test-server
ci-client: lint-client build-client test-client
# --- Database ---
db-backup:
@tooling/db-backup
db-install:
@tooling/db-install
# --- Decisions ---
decisions-sync:
@@ -115,10 +266,28 @@ decisions-active:
decisions-orphan:
@db/connectors/sqlite-query "SELECT id, title FROM decisions WHERE type='confirmed' AND status='active' AND id NOT IN (SELECT DISTINCT decision_ref FROM tickets WHERE decision_ref IS NOT NULL)"
# --- Content Validation ---
validate-content:
@tooling/validate-content
check-fact-ids:
@tooling/check-fact-ids
checklist-validate:
@tooling/validate-checklist --check
checklist-generate:
@tooling/validate-checklist
content-ron:
cd tooling/content-converter && cargo build --release
tooling/content-converter/target/release/content-converter --input content --output content-ron --verbose
# --- Clean ---
clean:
cd server && cargo clean || true
rm -rf .cache/*
rm -rf client/.godot client/reports
rm -rf client/.godot/* client/reports
@echo "Clean complete."
+1
View File
@@ -38,6 +38,7 @@
| **DUDLEY** | Server Developer | Game server, ECS, simulation loop, world state management. | Server implementation phase |
| **OSCAR** | Networking Developer | Multiplayer networking, client-server protocol, sync. | Networking implementation phase |
| **JUSTINE** | Polish & Deploy | Build pipelines, packaging, performance optimization, release prep. | Pre-release phase |
| **INIGO** | Sound Designer | Soundscape design, ambient layers, diegetic audio cues, audio propagation. | Audio implementation phase |
| **MELLANIE** | Copywriter | In-game text, UI copy, tooltips, flavor text, lore entries. | Content creation phase |
| **TIGER** | Translator | Localization, i18n framework, translation management. | Localization phase |
+15 -10
View File
@@ -69,25 +69,26 @@ static func _encode_message(buffer: StreamPeerBuffer, value):
TYPE_INT:
if - (1 << 5) <= value and value <= (1 << 7) - 1:
buffer.put_8(value)
elif - (1 << 7) <= value and value <= (1 << 7):
elif - (1 << 7) <= value and value < (1 << 7):
buffer.put_u8(types["int_8"])
buffer.put_8(value)
elif 0 <= value and value <= (1 << 8) - 1:
buffer.put_u8(types["uint_8"])
buffer.put_u8(value)
elif - (1 << 15) <= value and value <= (1 << 15):
elif - (1 << 15) <= value and value < (1 << 15):
buffer.put_u8(types["int_16"])
buffer.put_16(value)
elif 0 <= value and value <= (1 << 16) - 1:
buffer.put_u8(types["uint_16"])
buffer.put_u16(value)
elif - (1 << 31) <= value and value <= (1 << 31):
elif - (1 << 31) <= value and value < (1 << 31):
buffer.put_u8(types["int_32"])
buffer.put_32(value)
elif 0 <= value and value <= (1 << 32) - 1:
buffer.put_u8(types["uint_32"])
buffer.put_u32(value)
elif - (1 << 63) <= value and value <= (1 << 63):
elif value < 0:
# Negative beyond int_32 range — encode as int_64 (0xd3)
buffer.put_u8(types["int_64"])
buffer.put_64(value)
else:
@@ -133,7 +134,9 @@ static func _encode_message(buffer: StreamPeerBuffer, value):
return ERR_INVALID_DATA
for obj in value:
_encode_message(buffer, obj)
var inner_err = _encode_message(buffer, obj)
if inner_err:
return inner_err
TYPE_DICTIONARY:
var size = value.size()
@@ -150,8 +153,12 @@ static func _encode_message(buffer: StreamPeerBuffer, value):
return ERR_INVALID_DATA
for key in value:
_encode_message(buffer, key)
_encode_message(buffer, value[key])
var key_err = _encode_message(buffer, key)
if key_err:
return key_err
var val_err = _encode_message(buffer, value[key])
if val_err:
return val_err
TYPE_PACKED_BYTE_ARRAY:
var size = value.size()
@@ -225,11 +232,9 @@ static func _decode_message(buffer: StreamPeerBuffer, err: Dictionary):
return buffer.get_utf8_string(size)
elif first_byte == types["nil"]: # nil
print("null size:%s"%buffer_size)
return null
elif first_byte == types["false"]: # false
print("false size:%s"%buffer_size)
return false
elif first_byte == types["true"]: # true
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://uflbvtm1aquk"
path="res://.godot/imported/cursor_hover.ogg-f20fa5adecb73cc9163569968f5867a3.oggvorbisstr"
[deps]
source_file="res://assets/audio/cursor_hover.ogg"
dest_files=["res://.godot/imported/cursor_hover.ogg-f20fa5adecb73cc9163569968f5867a3.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://bw0mfil2343po"
path="res://.godot/imported/fog_recognition.ogg-fd0dc7f57c3509774a533bc71a7687e7.oggvorbisstr"
[deps]
source_file="res://assets/audio/fog_recognition.ogg"
dest_files=["res://.godot/imported/fog_recognition.ogg-fd0dc7f57c3509774a533bc71a7687e7.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://dei8ikwaqyn67"
path="res://.godot/imported/implant_open.ogg-46eadc4ed252693e3a936e75ddfdf4bc.oggvorbisstr"
[deps]
source_file="res://assets/audio/implant_open.ogg"
dest_files=["res://.godot/imported/implant_open.ogg-46eadc4ed252693e3a936e75ddfdf4bc.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://b7iopmg6ftp4k"
path="res://.godot/imported/sfx_monologue_chime.ogg-5981f28716fbe14273c052560195d890.oggvorbisstr"
[deps]
source_file="res://assets/audio/sfx_monologue_chime.ogg"
dest_files=["res://.godot/imported/sfx_monologue_chime.ogg-5981f28716fbe14273c052560195d890.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://4m87qby6qdhc"
path="res://.godot/imported/sfx_monologue_chime_urgent.ogg-696a4dbc297417724610e01fa44fa575.oggvorbisstr"
[deps]
source_file="res://assets/audio/sfx_monologue_chime_urgent.ogg"
dest_files=["res://.godot/imported/sfx_monologue_chime_urgent.ogg-696a4dbc297417724610e01fa44fa575.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://dbu2lr4scswyn"
path="res://.godot/imported/sfx_stance_change.ogg-042fce976b2bd1b0806c72aa5dd64755.oggvorbisstr"
[deps]
source_file="res://assets/audio/sfx_stance_change.ogg"
dest_files=["res://.godot/imported/sfx_stance_change.ogg-042fce976b2bd1b0806c72aa5dd64755.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c5lduxqgx0vme"
path="res://.godot/imported/sfx_weapon_aim_lock.ogg-649a54b41e9bf6dd8ab414c03bb48cfa.oggvorbisstr"
[deps]
source_file="res://assets/audio/sfx_weapon_aim_lock.ogg"
dest_files=["res://.godot/imported/sfx_weapon_aim_lock.ogg-649a54b41e9bf6dd8ab414c03bb48cfa.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
Binary file not shown.
+19
View File
@@ -0,0 +1,19 @@
[remap]
importer="oggvorbisstr"
type="AudioStreamOggVorbis"
uid="uid://c2bqso5hkljq1"
path="res://.godot/imported/weapon_aim.ogg-0bff40b0820b0f9db4c2b0ae2542bfa0.oggvorbisstr"
[deps]
source_file="res://assets/audio/weapon_aim.ogg"
dest_files=["res://.godot/imported/weapon_aim.ogg-0bff40b0820b0f9db4c2b0ae2542bfa0.oggvorbisstr"]
[params]
loop=false
loop_offset=0
bpm=0
beat_count=0
bar_beats=4
+187
View File
@@ -0,0 +1,187 @@
# UI Microcopy — The Settled Reach v0.1
#
# Ticket: #409 | Sprint: 5
# Author: Mellanie (Copywriter)
# Date: 2026-02-13
#
# All player-facing UI text. Organized by category.
# Voice: Sova Transit District (D-036) — working-class pragmatic, quotidian-with-undertow.
# Diegetic frame: most UI elements are neural insert overlays, not game chrome.
#
# Integration: Stig imports these into client/scripts/constants/ or reads at runtime.
# Keys are stable identifiers. Values are display strings.
# ============================================================
# INTERACTION PROMPTS
# Displayed as "E - {label}" when player is near an interactable.
# Server sends verb kind + label. These are the canonical labels.
# Keep short: max 12 characters. Must read instantly during gameplay.
# ============================================================
interaction_verbs:
talk: "Talk"
examine_npc: "Look"
examine_object: "Examine"
read: "Read"
pick_up: "Pick up"
use: "Use"
open: "Open"
listen: "Listen"
# ============================================================
# RELATIONSHIP STATE DESCRIPTORS
# Shown in knowledge panel when player inspects an entity.
# Maps to RelationshipState enum (server/src/knowledge/types.rs).
# These should feel like insert-generated assessments, not game labels.
# ============================================================
relationship_states:
unknown:
label: "Unknown"
description: "No prior contact."
known:
label: "Recognized"
description: "Seen before. No relationship established."
friendly:
label: "Trusted"
description: "Known contact. Cooperative."
person_of_interest:
label: "Flagged"
description: "Behavioral anomaly noted."
hostile:
label: "Hostile"
description: "Known threat."
# ============================================================
# HUD LABELS
# Top-level HUD elements. Diegetic: these are the insert's status readouts.
# Format mirrors a low-key neural overlay — terse, functional.
# ============================================================
hud:
health: "Condition"
health_values:
full: "Normal"
wounded: "Injured"
critical: "Critical"
time_prefix: "" # No prefix — time displays as raw value (e.g., "14:30")
perception_mode_prefix: "" # No prefix — mode name displays directly
# ============================================================
# PERCEPTION MODE LABELS
# The insert's active sensing mode. Per D-017.
# Smuggler has Baseline. Detective has Standard Professional.
# Labels should feel like the insert's own status readout.
# ============================================================
perception_modes:
standard: "Standard"
analytical: "Analytical"
social: "Social"
surveillance: "Surveillance"
# ============================================================
# NOTIFICATION TEXT
# Brief messages that appear when game state changes.
# Max 40 characters. Must read in under 2 seconds.
# Diegetic: these are insert alerts, not achievement popups.
# ============================================================
notifications:
# Location discovery
location_discovered: "New location logged."
location_revisit: "Returning to {location}."
# NPC knowledge
contact_new: "New contact: {name}."
contact_updated: "Contact updated: {name}."
contact_flagged: "{name} flagged."
# Relationship shifts
relationship_improved: "{name} — trust increased."
relationship_degraded: "{name} — trust decreased."
relationship_hostile: "{name} — hostile."
# Evidence / observation
evidence_found: "Evidence logged."
anomaly_detected: "Anomaly noted."
pattern_recognized: "Pattern flagged."
# System
save_complete: "Progress saved."
connection_lost: "Signal interrupted."
connection_restored: "Signal restored."
# ============================================================
# KNOWLEDGE PANEL LABELS
# When the player opens the knowledge view to review what they know.
# Insert-flavored: reads like a personal filing system, not a game menu.
# ============================================================
knowledge_panel:
tab_contacts: "Contacts"
tab_locations: "Locations"
tab_evidence: "Notes"
section_known_attributes: "Known"
section_last_seen: "Last seen"
section_relationship: "Status"
section_observations: "Observations"
empty_state: "Nothing logged yet."
confidence_high: "Confirmed"
confidence_medium: "Likely"
confidence_low: "Unconfirmed"
confidence_rumor: "Hearsay"
# ============================================================
# TUTORIAL TEXT (DIEGETIC)
# Delivered via internal monologue (D-016), not UI overlay.
# These are NOT traditional tutorial prompts. They're the character
# thinking about what they can do, filtered through their voice.
# Smuggler and detective variants are in the monologue pools.
# These are the GENERIC fallback strings if monologue doesn't fire.
# ============================================================
tutorial_prompts:
move_hint: "Use the directional keys to move."
interact_hint: "Press E near someone to interact."
examine_hint: "Press E to take a closer look."
listen_hint: "Move closer to overhear conversations."
monologue_hint: "Your thoughts appear as text on screen."
perception_hint: "Switch perception modes to see differently."
minimap_hint: "The overlay shows the area around you."
knowledge_hint: "Open your notes to review what you know."
# ============================================================
# DIALOGUE
# Text displayed during dialogue interactions.
# D-063: Confrontation beat monologue — character's internal hesitation.
# ============================================================
dialogue:
confrontation_beat: "This changes things. No taking it back."
# ============================================================
# MENU AND SYSTEM TEXT
# Non-diegetic. Standard game UI. Clean, no flavor text.
# ============================================================
menu:
pause_title: "Paused"
resume: "Resume"
settings: "Settings"
save_game: "Save"
load_game: "Load"
quit_to_menu: "Quit to Menu"
quit_game: "Quit"
confirm_quit: "Unsaved progress will be lost."
confirm_yes: "Yes"
confirm_no: "No"
settings:
audio_volume: "Volume"
text_size: "Text Size"
text_speed: "Text Speed"
fullscreen: "Fullscreen"
language: "Language"
# ============================================================
# CHARACTER SELECTION (if applicable in v0.1)
# ============================================================
character_select:
title: "Choose your perspective."
smuggler_name: "Logistics Operator"
smuggler_tagline: "You know these people. You know the work. You know what's at stake."
detective_name: "Commission Investigator"
detective_tagline: "The manifests don't add up. Someone in this district knows why."
confirm: "Begin"
+19
View File
@@ -20,6 +20,9 @@ config/icon="res://icon.svg"
SimBridge="*res://scripts/autoloads/sim_bridge.gd"
GameState="*res://scripts/autoloads/game_state.gd"
InputMapper="*res://scripts/autoloads/input_mapper.gd"
UIStrings="*res://scripts/autoloads/ui_strings.gd"
FogState="*res://scripts/autoloads/fog_state.gd"
AudioManager="*res://scripts/autoloads/audio_manager.gd"
[display]
@@ -93,8 +96,24 @@ pause={
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
]
}
stance_up={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":67,"key_label":0,"unicode":99,"location":0,"echo":false,"script":null)
]
}
stance_down={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":88,"key_label":0,"unicode":120,"location":0,"echo":false,"script":null)
]
}
bug_report={
"deadzone": 0.5,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194343,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
[rendering]
renderer/rendering_method="gl_compatibility"
renderer/rendering_method.mobile="gl_compatibility"
environment/defaults/default_clear_color=Color(0.05, 0.05, 0.08, 1)
+129 -10
View File
@@ -1,33 +1,152 @@
[gd_scene load_steps=8 format=3 uid="uid://bswrmh7w8dbgm"]
[gd_scene load_steps=19 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"]
[ext_resource type="Script" path="res://scripts/rendering/entity_renderer.gd" id="3_entity"]
[ext_resource type="Script" path="res://scripts/rendering/fog_renderer.gd" id="4_fog"]
[ext_resource type="PackedScene" path="res://ui/hud.tscn" id="5_hud"]
[ext_resource type="PackedScene" path="res://ui/minimap.tscn" id="6_minimap"]
[ext_resource type="PackedScene" path="res://ui/monologue_display.tscn" id="7_monologue"]
[ext_resource type="Script" path="res://scripts/rendering/fog_shader.gd" id="4_fog"]
[ext_resource type="Script" path="res://scripts/rendering/tile_renderer.gd" id="5_tile"]
[ext_resource type="PackedScene" path="res://ui/hud.tscn" id="6_hud"]
[ext_resource type="PackedScene" path="res://ui/minimap.tscn" id="7_minimap"]
[ext_resource type="PackedScene" path="res://ui/monologue_display.tscn" id="8_monologue"]
[ext_resource type="PackedScene" path="res://ui/interaction_prompt.tscn" id="9_prompt"]
[ext_resource type="Script" path="res://scripts/rendering/cursor_renderer.gd" id="10_cursor"]
[ext_resource type="PackedScene" path="res://ui/interaction_list.tscn" id="11_ilist"]
[ext_resource type="PackedScene" path="res://ui/inventory_grid.tscn" id="12_inv"]
[ext_resource type="PackedScene" path="res://ui/stance_indicator.tscn" id="13_stance"]
[ext_resource type="PackedScene" path="res://ui/world_radial.tscn" id="14_radial"]
[ext_resource type="PackedScene" path="res://ui/dialogue_box.tscn" id="15_dialogue"]
[ext_resource type="Script" path="res://scripts/rendering/fog_entities.gd" id="16_fogent"]
[ext_resource type="PackedScene" path="res://ui/gauntlet_hud.tscn" id="17_gauntlet"]
[ext_resource type="PackedScene" path="res://ui/bug_report_dialog.tscn" id="18_bugreport"]
[node name="Game" type="Node2D"]
script = ExtResource("1_main")
; D-049 Z-level rendering pipeline (z-layer-gap-analysis.md)
;
; World scope (z:-200 to z:900, inside FogGroup CanvasGroup):
; z:0 FloorTiles — ground plane
; z:10 FloorObjects — cosmetic floor detail, ground shadows
; z:100 YSortGroup — y-sorted: furniture, entities, wall faces (all z:0 relative)
; z:200 [future] — airborne (projectiles, low-flying objects)
; z:300 Overhead — ceiling edges, semi-transparent occlusion
; z:350 [future] — high airborne (above ceiling, scale > 1.0)
; z:400 [future] — upper floor content (rare with fixed camera)
; z:900 FogOverlay — OUTSIDE FogGroup, fog shader
;
; Y-sort contract: ALL children of YSortGroup that participate in positional
; occlusion MUST have z_index = 0. z_index is PRIMARY sort, y is SECONDARY.
; Entities node is AFTER Furniture node — D-044 entity-wins-ties on same y.
[node name="World" type="Node2D" parent="."]
script = ExtResource("2_world")
[node name="TileMapLayer" type="TileMapLayer" parent="World"]
; --- World content inside CanvasGroup for fog compositing ---
; CanvasGroup captures everything beneath it into one texture.
; FogOverlay (outside) draws the fog shader over this composited texture.
[node name="Entities" type="Node2D" parent="World"]
[node name="FogGroup" type="CanvasGroup" parent="World"]
; z:0 — Floor tiles: zone identity, movement surface
[node name="FloorTiles" type="TileMapLayer" parent="World/FogGroup"]
z_index = 0
script = ExtResource("5_tile")
; z:10 — Floor objects: cosmetic detail, walked over, ground shadows from airborne
; (placeholder — populated when floor object art is added)
[node name="FloorObjects" type="Node2D" parent="World/FogGroup"]
z_index = 10
; z:100 — Y-sorted group: furniture + entities + wall faces interleave by y-position
; ALL children MUST use z_index = 0 (y-sort contract, Godot #62715)
[node name="YSortGroup" type="Node2D" parent="World/FogGroup"]
y_sort_enabled = true
z_index = 100
; Furniture (z:0 relative) — tables, chairs, placed objects
; Placeholder — before Entities in tree order so entities win visual ties (D-044)
[node name="Furniture" type="Node2D" parent="World/FogGroup/YSortGroup"]
y_sort_enabled = true
z_index = 0
; Entities (z:0 relative) — D-033 colored sprites, y-sorted with furniture
; MUST be z_index = 0 for correct y-sort interleaving
[node name="Entities" type="Node2D" parent="World/FogGroup/YSortGroup"]
y_sort_enabled = true
z_index = 0
script = ExtResource("3_entity")
; z:300 — Overhead: ceiling edges, upper floor structure, semi-transparent occlusion
; (placeholder — populated when overhead art is added)
[node name="Overhead" type="Node2D" parent="World/FogGroup"]
z_index = 300
; --- z:900 — Fog of perception (D-059 shader-based) ---
; OUTSIDE FogGroup. ColorRect child with fragment shader composites
; 5-layer fog over the world. fog_shader.gd manages uniforms.
[node name="FogOverlay" type="Node2D" parent="World"]
z_index = 900
script = ExtResource("4_fog")
; --- z:950 — Fog entities (D-059/D-060 cognitive delay visualization) ---
; Between fog (z:900) and InsertOverlay (CanvasLayer 10).
; Sound pings, grey blobs, recognized entity glow + silhouette.
[node name="FogEntities" type="Node2D" parent="World"]
z_index = 950
script = ExtResource("16_fogent")
; --- Camera ---
[node name="Camera2D" type="Camera2D" parent="."]
position_smoothing_enabled = true
position_smoothing_speed = 6.0
zoom = Vector2(2, 2)
; --- Insert overlay (CanvasLayer 10) ---
; Bloom-rendered, NOT affected by fog or camera transform.
; World-anchored elements convert world→screen coords in scripts.
[node name="InsertOverlay" type="CanvasLayer" parent="."]
layer = 10
; InteractionPrompt — v0.1 fallback single-line "E - Talk" display
[node name="InteractionPrompt" parent="InsertOverlay" instance=ExtResource("9_prompt")]
; D-057: Entity interaction vertical list — multi-verb, insert-styled
[node name="InteractionList" parent="InsertOverlay" instance=ExtResource("11_ilist")]
; D-058: World radial menu — right-click, 2 spokes (Observe + Insert)
[node name="WorldRadial" parent="InsertOverlay" instance=ExtResource("14_radial")]
; D-061: Dialogue box — bottom screen, max 20% height, diegetic insert UI
[node name="DialogueBox" parent="InsertOverlay" instance=ExtResource("15_dialogue")]
; --- UI layer (CanvasLayer 20) ---
; HUD, monologue, cursor — always visible, not affected by fog or camera.
[node name="UILayer" type="CanvasLayer" parent="."]
layer = 20
[node name="HUD" parent="UILayer" instance=ExtResource("5_hud")]
[node name="HUD" parent="UILayer" instance=ExtResource("6_hud")]
[node name="Minimap" parent="UILayer" instance=ExtResource("6_minimap")]
[node name="Minimap" parent="UILayer" instance=ExtResource("7_minimap")]
[node name="MonologueDisplay" parent="UILayer" instance=ExtResource("7_monologue")]
[node name="MonologueDisplay" parent="UILayer" instance=ExtResource("8_monologue")]
; D-053: Stance indicator — top-right, color-coded
[node name="StanceIndicator" parent="UILayer" instance=ExtResource("13_stance")]
; #496: Gauntlet HUD — room timer + personal bests, hidden in non-gauntlet mode
[node name="GauntletHUD" parent="UILayer" instance=ExtResource("17_gauntlet")]
; D-065: Inventory grid — 3x3, bottom-right, 40x40px, 1-9 hotkeys
[node name="InventoryGrid" parent="UILayer" instance=ExtResource("12_inv")]
; D-056: Cursor state machine — insert-styled geometric cursor, topmost in UILayer
[node name="CursorRenderer" type="Node2D" parent="UILayer"]
script = ExtResource("10_cursor")
; --- Modal layer (CanvasLayer 30) ---
; Full-screen overlays: pause menu, inventory modal, death screen.
[node name="ModalLayer" type="CanvasLayer" parent="."]
layer = 30
; #495: WRONG button (F12) — bug report capture dialog
[node name="BugReportDialog" parent="ModalLayer" instance=ExtResource("18_bugreport")]
+314
View File
@@ -0,0 +1,314 @@
extends Node
## AudioManager autoload singleton (D-068, D-069, D-073).
## 5-bus architecture with directory-scan registry pattern.
## No-op fallback when audio assets absent (D-038).
## Spatial audio positioning for close-range sounds (D-018).
# --- Bus names (D-068) ---
const BUS_MUSIC := "Music"
const BUS_AMBIENT := "Ambient"
const BUS_WORLD_SFX := "WorldSFX"
const BUS_PLAYER_ACTIONS := "PlayerActions"
const BUS_UI_SOUNDS := "UISounds"
const BUSES := [BUS_MUSIC, BUS_AMBIENT, BUS_WORLD_SFX, BUS_PLAYER_ACTIONS, BUS_UI_SOUNDS]
# D-069: Audio dip profiles — dB offsets relative to player slider setting.
# Mid-range of spec values used (e.g. -6 to -8 → -7).
# NOTE: listening_focus requires 30+ stationary ticks before activation (D-069/D-071).
# The tick gate is the caller's responsibility — AudioManager only manages bus volumes.
const DIP_SPECS := {
"dialogue": {
"ease_in": 0.3, "ease_out": 0.5,
"buses": { "Ambient": -7.0 },
},
"confrontation": {
"ease_in": 0.5, "ease_out": 1.0,
"buses": { "Ambient": -11.0, "WorldSFX": -5.0 },
"filter_hz": 800.0,
},
"listening_focus": {
"ease_in": 0.5, "ease_out": 0.5,
"buses": { "WorldSFX": 2.5 },
},
}
# Low-pass filter default cutoff — effectively bypassed at this value.
const FILTER_CUTOFF_DEFAULT := 20500.0
# Asset registry: filename stem (e.g. "amb_station_base") → AudioStream
var _registry: Dictionary = {}
# Player volume settings per bus (dB). Base for proportional dip calculation.
var _bus_volumes: Dictionary = {}
# Active dip state (D-069)
var _active_dip: String = ""
var _dip_tweens: Array = []
# Low-pass filter on Ambient bus (D-069 confrontation sweep)
var _ambient_filter: AudioEffectLowPassFilter = null
# Ambient loop players keyed by asset_key (D-073 zone crossfade)
var _ambient_players: Dictionary = {}
signal dip_changed(profile: String)
func _ready() -> void:
_setup_buses()
_scan_registry()
# --- Bus setup ---
func _setup_buses() -> void:
for bus_name in BUSES:
if AudioServer.get_bus_index(bus_name) < 0:
AudioServer.add_bus()
var idx := AudioServer.bus_count - 1
AudioServer.set_bus_name(idx, bus_name)
AudioServer.set_bus_send(idx, "Master")
_bus_volumes[bus_name] = 0.0
# Low-pass filter on Ambient bus — cutoff starts high (bypassed).
# Confrontation dip sweeps it down to 800 Hz (D-069).
# Guard: skip if filter already exists (e.g. duplicate _ready in tests).
var amb_idx := AudioServer.get_bus_index(BUS_AMBIENT)
if amb_idx >= 0 and _ambient_filter == null:
_ambient_filter = AudioEffectLowPassFilter.new()
_ambient_filter.cutoff_hz = FILTER_CUTOFF_DEFAULT
AudioServer.add_bus_effect(amb_idx, _ambient_filter)
# --- Asset registry (D-068 directory-scan pattern) ---
func _scan_registry() -> void:
_scan_dir("res://audio/")
print("AudioManager: %d assets registered" % _registry.size())
func _scan_dir(path: String) -> void:
var dir := DirAccess.open(path)
if dir == null:
if path == "res://audio/":
print("AudioManager: res://audio/ not found — all play methods no-op")
return
dir.list_dir_begin()
var file_name := dir.get_next()
while file_name != "":
var full_path := path.path_join(file_name)
if dir.current_is_dir():
_scan_dir(full_path)
elif not file_name.ends_with(".import"):
if file_name.get_extension().to_lower() in ["ogg", "wav", "mp3"]:
var stream := load(full_path) as AudioStream
if stream:
_registry[file_name.get_basename()] = stream
file_name = dir.get_next()
func has_asset(asset_key: String) -> bool:
return _registry.has(asset_key)
func get_registry_size() -> int:
return _registry.size()
# --- Playback: non-spatial ---
## Play a one-shot sound on the given bus. No-ops if asset not in registry.
func play(asset_key: String, bus: String = BUS_UI_SOUNDS) -> void:
var stream := _get_stream(asset_key)
if stream == null:
return
var player := AudioStreamPlayer.new()
player.stream = stream
player.bus = bus
add_child(player)
player.finished.connect(player.queue_free)
player.play()
## Start or replace a looping non-spatial sound. Returns the player or null.
func play_loop(asset_key: String, bus: String = BUS_AMBIENT) -> AudioStreamPlayer:
var stream := _get_stream(asset_key)
if stream == null:
return null
stop_loop(asset_key)
var loop_stream := stream.duplicate() as AudioStream
_enable_loop(loop_stream)
var player := AudioStreamPlayer.new()
player.stream = loop_stream
player.bus = bus
add_child(player)
player.play()
_ambient_players[asset_key] = player
return player
## Stop a looping sound by asset key.
func stop_loop(asset_key: String) -> void:
if _ambient_players.has(asset_key):
var player: AudioStreamPlayer = _ambient_players[asset_key]
if is_instance_valid(player):
player.stop()
player.queue_free()
_ambient_players.erase(asset_key)
## Stop all looping sounds.
func stop_all_loops() -> void:
for key in _ambient_players.keys():
stop_loop(key)
# --- Playback: spatial (D-018 close-range) ---
## Play a one-shot spatial sound at a world position (pixels).
## Added to current_scene for correct AudioListener2D positioning.
func play_at(asset_key: String, world_position: Vector2, bus: String = BUS_WORLD_SFX) -> void:
var stream := _get_stream(asset_key)
if stream == null:
return
var player := AudioStreamPlayer2D.new()
player.stream = stream
player.bus = bus
player.global_position = world_position
var scene := get_tree().current_scene
if scene:
scene.add_child(player)
else:
add_child(player)
player.finished.connect(player.queue_free)
player.play()
# --- Audio dip profiles (D-069) ---
## Apply a dip profile. Interrupts any active dip, restoring unaffected buses.
## Profiles: "dialogue", "confrontation", "listening_focus"
func apply_dip(profile: String) -> void:
if not DIP_SPECS.has(profile):
push_warning("AudioManager: unknown dip profile '%s'" % profile)
return
_kill_tweens()
_active_dip = profile
var spec: Dictionary = DIP_SPECS[profile]
var buses: Dictionary = spec.get("buses", {})
var ease_in: float = spec.get("ease_in", 0.3)
# Tween ALL buses — affected ones get offset, unaffected ones restore to base.
for bus_name in BUSES:
var offset_db: float = buses.get(bus_name, 0.0)
var target_db: float = _bus_volumes.get(bus_name, 0.0) + offset_db
var idx := AudioServer.get_bus_index(bus_name)
if idx < 0:
continue
var current_db := AudioServer.get_bus_volume_db(idx)
if absf(current_db - target_db) < 0.1:
continue
var tween := create_tween()
tween.tween_method(_make_bus_setter(bus_name), current_db, target_db, ease_in)
_dip_tweens.append(tween)
# Confrontation: sweep low-pass filter on Ambient (D-069)
if spec.has("filter_hz") and _ambient_filter:
var tween := create_tween()
tween.tween_property(_ambient_filter, "cutoff_hz", spec["filter_hz"], ease_in)
_dip_tweens.append(tween)
elif _ambient_filter and _ambient_filter.cutoff_hz < FILTER_CUTOFF_DEFAULT - 100.0:
# Leaving confrontation for another dip — restore filter
var tween := create_tween()
tween.tween_property(_ambient_filter, "cutoff_hz", FILTER_CUTOFF_DEFAULT, ease_in)
_dip_tweens.append(tween)
dip_changed.emit(profile)
## Clear the active dip, restoring all bus volumes to player slider settings.
func clear_dip() -> void:
if _active_dip.is_empty():
return
var spec: Dictionary = DIP_SPECS.get(_active_dip, {})
var ease_out: float = spec.get("ease_out", 0.5)
_kill_tweens()
_active_dip = ""
for bus_name in BUSES:
var target_db: float = _bus_volumes.get(bus_name, 0.0)
var idx := AudioServer.get_bus_index(bus_name)
if idx < 0:
continue
var current_db := AudioServer.get_bus_volume_db(idx)
if absf(current_db - target_db) < 0.1:
continue
var tween := create_tween()
tween.tween_method(_make_bus_setter(bus_name), current_db, target_db, ease_out)
_dip_tweens.append(tween)
# Restore low-pass filter
if _ambient_filter and _ambient_filter.cutoff_hz < FILTER_CUTOFF_DEFAULT - 100.0:
var tween := create_tween()
tween.tween_property(_ambient_filter, "cutoff_hz", FILTER_CUTOFF_DEFAULT, ease_out)
_dip_tweens.append(tween)
dip_changed.emit("")
func get_active_dip() -> String:
return _active_dip
# --- Volume control (player sliders, D-069 proportional base) ---
## Set the player's volume preference for a bus. During active dip, immediately
## recalculates effective volume (proportional to new base).
func set_volume(bus: String, volume_db: float) -> void:
_bus_volumes[bus] = volume_db
var idx := AudioServer.get_bus_index(bus)
if idx < 0:
return
if _active_dip.is_empty():
AudioServer.set_bus_volume_db(idx, volume_db)
else:
var spec: Dictionary = DIP_SPECS.get(_active_dip, {})
var buses: Dictionary = spec.get("buses", {})
var offset_db: float = buses.get(bus, 0.0)
AudioServer.set_bus_volume_db(idx, volume_db + offset_db)
func get_volume(bus: String) -> float:
return _bus_volumes.get(bus, 0.0)
# --- Zone crossfade (D-073 stub) ---
## Handle zone transition. Server sends zone_id per tile in ObserverSnapshot.
## Full crossfade implementation deferred to Sprint 9+ (D-073).
## Stub exists so server integration can call it without conditional checks.
func set_zone(_zone_id: String) -> void:
pass
# --- Internal helpers ---
func _get_stream(asset_key: String) -> AudioStream:
return _registry.get(asset_key) as AudioStream
func _make_bus_setter(bus_name: String) -> Callable:
return func(db: float) -> void:
var idx := AudioServer.get_bus_index(bus_name)
if idx >= 0:
AudioServer.set_bus_volume_db(idx, db)
func _kill_tweens() -> void:
for tween in _dip_tweens:
if tween != null and tween.is_valid():
tween.kill()
_dip_tweens.clear()
static func _enable_loop(stream: AudioStream) -> void:
if stream is AudioStreamOggVorbis:
stream.loop = true
elif stream is AudioStreamWAV:
stream.loop_mode = AudioStreamWAV.LOOP_FORWARD
elif stream is AudioStreamMP3:
stream.loop = true
@@ -0,0 +1 @@
uid://cdkut8u7sxwmb
+129
View File
@@ -0,0 +1,129 @@
extends Node
## Fog texture state — visibility/exploration/zone tint images updated from GameState.
## Read by fog_shader.gd for shader uniforms. Not a renderer — pure data.
## Architecture: docs/architecture/fog-shader-spec.md | D-059
# Fog texture byte values — visibility and exploration layers.
# Used by fog shader to distinguish visual treatment per tile.
# Test assertions reference these: assert_that(byte).is_equal(FogState.VIS_FORWARD)
const VIS_HIDDEN: int = 0 # Not in LOS — fully fogged
const VIS_PERIPHERAL: int = 180 # In LOS, peripheral sector — light fog dimming
const VIS_FORWARD: int = 255 # In LOS, forward sector — clear vision
const EXP_UNEXPLORED: int = 0 # Never seen — total darkness
const EXP_EXPLORED: int = 128 # Previously seen, now out of LOS — deep fog
const EXP_VISIBLE: int = 255 # Currently in LOS — clear (written each frame)
var map_bounds: Rect2i = Rect2i(0, 0, 1, 1)
var visibility_texture: ImageTexture
var exploration_texture: ImageTexture
var zone_tint_texture: ImageTexture
var _vis_bytes: PackedByteArray
var _exp_bytes: PackedByteArray
var _vis_image: Image
var _exp_image: Image
var _tint_image: Image
var _width: int = 1
var _height: int = 1
var _prev_visible: Dictionary = {} # Tiles visible last frame (for incremental decay)
func _ready() -> void:
_resize(Rect2i(0, 0, 64, 64))
func _resize(bounds: Rect2i) -> void:
map_bounds = bounds
_width = maxi(bounds.size.x, 1)
_height = maxi(bounds.size.y, 1)
var sz := _width * _height
_vis_bytes = PackedByteArray()
_vis_bytes.resize(sz)
_vis_bytes.fill(VIS_HIDDEN)
_vis_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
visibility_texture = ImageTexture.create_from_image(_vis_image)
_exp_bytes = PackedByteArray()
_exp_bytes.resize(sz)
_exp_bytes.fill(EXP_UNEXPLORED)
_exp_image = Image.create_from_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
exploration_texture = ImageTexture.create_from_image(_exp_image)
# Zone tint — neutral dark for Sprint 6 (zone metadata deferred)
_tint_image = Image.create(_width, _height, false, Image.FORMAT_RGB8)
_tint_image.fill(Color(0.05, 0.05, 0.08))
zone_tint_texture = ImageTexture.create_from_image(_tint_image)
_prev_visible.clear()
func update_from_state() -> void:
# Resize if map bounds changed
var tiles := GameState.visible_tiles
if tiles.size() > 0:
var new_bounds := _compute_bounds(tiles)
if new_bounds != map_bounds:
_resize(new_bounds)
var ox: int = map_bounds.position.x
var oy: int = map_bounds.position.y
var positions: Dictionary = GameState.visible_positions
var sectors: Dictionary = GameState.visibility_sectors
# TODO(v0.2): gradual decay over game-time instead of immediate EXP_VISIBLE→EXP_EXPLORED
# 1. Clear visibility, then write current LOS
_vis_bytes.fill(VIS_HIDDEN)
for pos in 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
var sector: String = sectors.get(pos, "Forward")
_vis_bytes[py * _width + px] = VIS_FORWARD if sector == "Forward" else VIS_PERIPHERAL
_vis_image.set_data(_width, _height, false, Image.FORMAT_R8, _vis_bytes)
visibility_texture.update(_vis_image)
# 2. Exploration: tiles leaving LOS decay to EXP_EXPLORED, visible tiles stay EXP_VISIBLE
# Only touch tiles that changed (O(visible) not O(map_size))
for pos in _prev_visible:
if not positions.has(pos):
var px: int = pos.x - ox
var py: int = pos.y - oy
if px >= 0 and py >= 0 and px < _width and py < _height:
var idx: int = py * _width + px
if _exp_bytes[idx] > EXP_EXPLORED:
_exp_bytes[idx] = EXP_EXPLORED
for pos in positions:
var px: int = pos.x - ox
var py: int = pos.y - oy
if px >= 0 and py >= 0 and px < _width and py < _height:
_exp_bytes[py * _width + px] = EXP_VISIBLE
_exp_image.set_data(_width, _height, false, Image.FORMAT_R8, _exp_bytes)
exploration_texture.update(_exp_image)
# Shallow copy — correct for Dictionary<Vector2i, bool/String> values
_prev_visible = positions.duplicate()
func _compute_bounds(tiles: Array) -> Rect2i:
var min_x := 999999
var min_y := 999999
var max_x := -999999
var max_y := -999999
for tile in tiles:
if not tile is Dictionary or not tile.has("x") or not tile.has("y"):
continue
min_x = mini(min_x, int(tile.x))
min_y = mini(min_y, int(tile.y))
max_x = maxi(max_x, int(tile.x))
max_y = maxi(max_y, int(tile.y))
# Guard: all tiles invalid (no x/y) — sentinels would produce negative Rect2i
if min_x > max_x:
return Rect2i(0, 0, 1, 1)
# Margin for fog gradient bleed at edges
return Rect2i(min_x - 4, min_y - 4, max_x - min_x + 9, max_y - min_y + 9)
@@ -0,0 +1 @@
uid://de816lfn5xeqs
+126 -3
View File
@@ -1,16 +1,48 @@
extends Node
# Updated each frame from ObserverSnapshot data (Protocol format: {tick, entities}).
# Updated each frame from ObserverSnapshot data (Protocol format: {tick, entities, tiles}).
# Entities use Protocol decoded format: {entity_id, x, y, z, kind: {variant, data}}.
# Tiles use format: [{x, y, z, type}].
var current_snapshot: Dictionary = {}
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
# v2 fields (D-015, D-031)
var game_time: Dictionary = {} # {day, time_of_day, day_phase, tick_rate} or empty
var player_facing: String = "North" # 8-directional facing direction
var visibility_sectors: Dictionary = {} # Vector2i -> "Forward"/"Peripheral"
# Player entity ID — the first entity is assumed to be the player (will be
# refined when the server assigns explicit player entity IDs).
var player_entity_id: int = 1
# v4 fields (#404/#405)
var nearby_interactions: Array = [] # [{entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]}]
# v5 fields (#414)
var current_monologue: Variant = null # {id, text, duration_seconds} or null
# v6 fields (#449, D-053, D-065)
var player_stance: String = "Walk" # Sprint/Walk/Careful/Crouch
var player_inventory: Array = [] # [{item_id, name, slot}]
# v7 fields (#435, D-061/D-062)
var current_dialogue: Variant = null # {npc_name, npc_entity_id, speech, options: [{text, response_id, priority}]} or null
# D-064: true while dialogue box is visible or fading out (300ms).
# InputMapper suppresses movement when this is true.
var dialogue_active: bool = false
# v8 fields (#496): Gauntlet mode — room timer + personal bests
var room_id: Variant = null # String room_id from snapshot, null in non-gauntlet mode
var gauntlet_mode: bool = false # true when snapshot includes gauntlet_mode flag
# v7 fields (#431, D-059/D-060)
var pending_recognitions: Array = [] # [{entity_id, x, y, z, remaining_ticks, total_delay_ticks}]
func apply_snapshot(snapshot: Dictionary) -> void:
current_snapshot = snapshot
@@ -19,8 +51,99 @@ func apply_snapshot(snapshot: Dictionary) -> void:
if snapshot.has("entities"):
visible_entities = snapshot.entities
# Derive player position from the player entity
# Derive player position from the entity with kind.variant == "Player"
var found_player := false
for entity in visible_entities:
if entity.has("entity_id") and entity.entity_id == player_entity_id:
if entity.has("kind") and entity.kind is Dictionary and entity.kind.get("variant") == "Player":
player_position = Vector2(entity.x, entity.y)
if entity.has("entity_id"):
player_entity_id = entity.entity_id
found_player = true
break
if not found_player and visible_entities.size() > 0:
push_warning("GameState: no Player entity found in %d entities" % [
visible_entities.size()])
# Tiles for rendering: test mode sends "tiles", live server sends tile data in "visible_tiles"
if snapshot.has("tiles"):
visible_tiles = snapshot.tiles
elif snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
# Live server: visible_tiles now includes type from tile_kind field
var has_type := false
if snapshot.visible_tiles.size() > 0 and snapshot.visible_tiles[0] is Dictionary:
has_type = snapshot.visible_tiles[0].has("type")
if has_type:
visible_tiles = snapshot.visible_tiles
if snapshot.has("visible_positions"):
visible_positions.clear()
for pos in snapshot.visible_positions:
visible_positions[Vector2i(pos.x, pos.y)] = true
# v2: game_time (D-031)
if snapshot.has("game_time") and snapshot.game_time is Dictionary:
game_time = snapshot.game_time
# v2: player_facing (D-015)
if snapshot.has("player_facing") and snapshot.player_facing is String:
player_facing = snapshot.player_facing
# v4: nearby_interactions (#404/#405)
if snapshot.has("nearby_interactions") and snapshot.nearby_interactions is Array:
nearby_interactions = snapshot.nearby_interactions
else:
nearby_interactions = []
# v5: current_monologue (#414)
if snapshot.has("current_monologue") and snapshot.current_monologue is Dictionary:
current_monologue = snapshot.current_monologue
else:
current_monologue = null
# v6: player_stance (#449, D-053)
if snapshot.has("player_stance") and snapshot.player_stance is String:
player_stance = snapshot.player_stance
# v6: player_inventory (#449, D-065)
if snapshot.has("player_inventory") and snapshot.player_inventory is Array:
player_inventory = snapshot.player_inventory
else:
player_inventory = []
# v7: current_dialogue (#434, D-061)
if snapshot.has("current_dialogue") and snapshot.current_dialogue is Dictionary:
current_dialogue = snapshot.current_dialogue
else:
current_dialogue = null
# v7: pending_recognitions (#431, D-059/D-060)
if snapshot.has("pending_recognitions") and snapshot.pending_recognitions is Array:
pending_recognitions = snapshot.pending_recognitions
else:
pending_recognitions = []
# v8: gauntlet mode (#496) — room_id and gauntlet_mode
if snapshot.has("gauntlet_mode") and snapshot.gauntlet_mode == true:
gauntlet_mode = true
else:
gauntlet_mode = false
if snapshot.has("room_id") and snapshot.room_id is String:
room_id = snapshot.room_id
else:
room_id = null
# v2: visible_tiles with visibility sectors
# Derives visible_positions when not explicitly provided (real server mode)
if snapshot.has("visible_tiles") and snapshot.visible_tiles is Array and snapshot.visible_tiles.size() > 0:
visibility_sectors.clear()
var has_explicit_positions := snapshot.has("visible_positions")
if not has_explicit_positions:
visible_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)
if vtile.has("visibility"):
visibility_sectors[pos] = vtile.visibility
if not has_explicit_positions:
visible_positions[pos] = true
+77 -25
View File
@@ -1,55 +1,107 @@
extends Node
# Semantic actions — NO raw key codes cross the bridge
# Diagonal directions registered in project.godot with empty event arrays (intentional).
# Keybindings deferred until input design is finalized — likely numpad or composite WASD.
# Movement uses hold-to-move (polled each frame in _process).
# Discrete actions (interact, stance, etc.) use press events (_unhandled_input).
# Composite diagonals: holding W+D simultaneously → northeast.
#
# Movement throttle: client-side rate limit per stance (D-053).
# Sprint=5/s, Walk=2.5/s, Careful=1.7/s, Crouch=1.25/s.
# Server cooldown is authoritative, but the client throttle prevents
# flooding and gives correct movement feel in test mode.
enum Action {
MOVE_NORTH, MOVE_NORTHEAST, MOVE_EAST, MOVE_SOUTHEAST,
MOVE_SOUTH, MOVE_SOUTHWEST, MOVE_WEST, MOVE_NORTHWEST,
INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE
INTERACT, USE_PERCEPTION_MODE, OPEN_MENU, PAUSE, UNPAUSE,
TOGGLE_STANCE_UP, TOGGLE_STANCE_DOWN,
BUG_REPORT, # #495: F12 WRONG button — client-only, not sent to server
}
var input_queue: Array[Dictionary] = []
# Minimum milliseconds between movement commands, per stance.
# Tuned so Walk feels like walking, Sprint feels fast but readable.
const MOVE_INTERVAL_MS := {
"Sprint": 200, # 5/sec — fast but trackable
"Walk": 400, # 2.5/sec — comfortable walking pace
"Careful": 600, # ~1.7/sec — deliberate, scanning
"Crouch": 800, # 1.25/sec — creeping
}
var _last_move_msec: int = 0
# Hold-to-move: poll held direction keys each frame, throttled by stance.
# Server-side cooldown (D-053) is authoritative; this prevents client flooding.
# D-064: movement suppressed during dialogue (walk-away handled by dialogue_box).
func _process(_delta: float) -> void:
if GameState.dialogue_active:
return
var dir := Vector2i.ZERO
if Input.is_action_pressed("move_north"):
dir.y -= 1
if Input.is_action_pressed("move_south"):
dir.y += 1
if Input.is_action_pressed("move_east"):
dir.x += 1
if Input.is_action_pressed("move_west"):
dir.x -= 1
if dir != Vector2i.ZERO:
var now := Time.get_ticks_msec()
var interval: int = MOVE_INTERVAL_MS.get(GameState.player_stance, 200)
if now - _last_move_msec >= interval:
_last_move_msec = now
var action: Action = _dir_to_action(dir)
input_queue.append({
"action": action,
"timestamp_msec": now,
})
# Discrete actions: fire once on key press (not held).
func _unhandled_input(event: InputEvent) -> void:
var action: Action = -1
# Map input actions to semantic Action enum
# is_action_pressed handles press detection for all input types (key, gamepad, etc.)
if event.is_action_pressed("move_north"):
action = Action.MOVE_NORTH
elif event.is_action_pressed("move_northeast"):
action = Action.MOVE_NORTHEAST
elif event.is_action_pressed("move_east"):
action = Action.MOVE_EAST
elif event.is_action_pressed("move_southeast"):
action = Action.MOVE_SOUTHEAST
elif event.is_action_pressed("move_south"):
action = Action.MOVE_SOUTH
elif event.is_action_pressed("move_southwest"):
action = Action.MOVE_SOUTHWEST
elif event.is_action_pressed("move_west"):
action = Action.MOVE_WEST
elif event.is_action_pressed("move_northwest"):
action = Action.MOVE_NORTHWEST
elif event.is_action_pressed("interact"):
if event.is_action_pressed("interact"):
action = Action.INTERACT
elif event.is_action_pressed("perception_mode"):
action = Action.USE_PERCEPTION_MODE
elif event.is_action_pressed("open_menu"):
action = Action.OPEN_MENU
elif event.is_action_pressed("pause"):
action = Action.PAUSE
var tick_rate = GameState.game_time.get("tick_rate", "Full")
action = Action.UNPAUSE if tick_rate == "Paused" else Action.PAUSE
elif event.is_action_pressed("stance_up"):
action = Action.TOGGLE_STANCE_UP
elif event.is_action_pressed("stance_down"):
action = Action.TOGGLE_STANCE_DOWN
elif event.is_action_pressed("bug_report"):
action = Action.BUG_REPORT
# Queue the action if valid
if action != -1:
input_queue.append({
"action": action,
"timestamp_msec": Time.get_ticks_msec()
"timestamp_msec": Time.get_ticks_msec(),
})
get_viewport().set_input_as_handled()
func flush_queue() -> Array[Dictionary]:
var queue = input_queue.duplicate()
input_queue.clear()
return queue
# Map a direction vector to the corresponding movement Action.
# Handles all 8 directions via composite W+D, W+A, etc.
static func _dir_to_action(dir: Vector2i) -> Action:
match dir:
Vector2i(0, -1): return Action.MOVE_NORTH
Vector2i(1, -1): return Action.MOVE_NORTHEAST
Vector2i(1, 0): return Action.MOVE_EAST
Vector2i(1, 1): return Action.MOVE_SOUTHEAST
Vector2i(0, 1): return Action.MOVE_SOUTH
Vector2i(-1, 1): return Action.MOVE_SOUTHWEST
Vector2i(-1, 0): return Action.MOVE_WEST
Vector2i(-1, -1): return Action.MOVE_NORTHWEST
_: return Action.MOVE_NORTH
+334 -40
View File
@@ -4,15 +4,19 @@ extends Node
enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, ERROR }
var state: ConnectionState = ConnectionState.DISCONNECTED
var test_mode: bool = true # Enable test mode for development without Rust server
var test_mode: bool = OS.get_environment("SR_LIVE") != "1" # SR_LIVE=1 connects to real server
var _test_tick: int = 0
var _test_player_pos: Vector2i = Vector2i(10, 10)
var _test_facing: String = "North"
var _test_input_queue: Array = [] # Queued actions for test mode
var _test_in_dialogue: bool = false # Mock dialogue state (#434)
var _last_snapshot: Variant = null # Most recent decoded snapshot (consumed by poll_snapshot)
var _outbound_buffer: Array[PackedByteArray] = [] # Encoded inputs awaiting transport
var _outbound_buffer: Array[Dictionary] = [] # Raw inputs awaiting batch encode + transport
# Transport layer (non-test mode)
var _bridge: LocalBridge = null
var _server: ServerProcess = null
var server_port: int = 9800
var server_port: int = 9876 # Default matches server's default bind address
var server_path: String = "" # Path to server binary — set before connect_to_sim()
# Connection retry state — handles server startup delay (Critical fix #1)
@@ -27,7 +31,15 @@ signal snapshot_received(snapshot: Dictionary)
func _ready() -> void:
if test_mode:
print("SimBridge: Running in test mode (hardcoded snapshot)")
print("SimBridge: Running in test mode (dynamic snapshot)")
# Reset test state — call before tests that use _test_snapshot()
func reset_test_state() -> void:
_test_tick = 0
_test_player_pos = Vector2i(10, 10)
_test_facing = "North"
_test_input_queue.clear()
_test_in_dialogue = false
# Change connection state and emit signal
func _set_state(new_state: ConnectionState) -> void:
@@ -50,7 +62,8 @@ func connect_to_sim() -> void:
# Spawn server subprocess
if not server_path.is_empty():
_server = ServerProcess.new()
var pid := _server.start(server_path, ["--port", str(server_port)])
# Server reads first positional arg as bind address (e.g. "127.0.0.1:9876")
var pid := _server.start(server_path, ["127.0.0.1:" + str(server_port)])
if pid <= 0:
push_error("SimBridge: failed to start server")
_set_state(ConnectionState.ERROR)
@@ -129,12 +142,19 @@ func _process(delta: float) -> void:
while msg.size() > 0:
receive_bytes(msg)
msg = _bridge.poll_message()
# Send: flush outbound buffer through the bridge
# Send: batch-encode and flush outbound buffer as one frame (Vec<PlayerInput>).
# Inputs are drained before encoding. On encode failure the inputs are
# intentionally dropped — re-queuing would retry the same bad data and
# the server tick has already advanced, making stale inputs invalid.
var outbound := drain_outbound()
for payload in outbound:
var err := _bridge.send_message(payload)
if err != OK:
push_error("SimBridge: failed to send message: %s" % error_string(err))
if outbound.size() > 0:
var encoded := Protocol.encode_player_inputs(outbound)
if encoded.size() > 0:
var err := _bridge.send_message(encoded)
if err != OK:
push_error("SimBridge: failed to send message: %s" % error_string(err))
else:
push_error("SimBridge: failed to batch-encode %d inputs (dropped)" % outbound.size())
StreamPeerTCP.STATUS_CONNECTING:
pass # Should not happen in CONNECTED state
StreamPeerTCP.STATUS_ERROR:
@@ -154,17 +174,24 @@ func send_input(player_input: Dictionary) -> Error:
if state != ConnectionState.CONNECTED:
return ERR_CONNECTION_ERROR
if test_mode:
var action: int = player_input.get("action", -1)
var wire_name: String = _action_enum_to_wire(action)
if not wire_name.is_empty():
_test_input_queue.append(wire_name)
return OK
var action_name := _action_enum_to_wire(player_input.get("action", -1))
if action_name.is_empty():
# _action_enum_to_wire already emits push_warning for invalid actions
return ERR_INVALID_PARAMETER
var tick: int = player_input.get("timestamp_msec", 0)
var encoded := Protocol.encode_player_input(tick, action_name)
if encoded.size() == 0:
push_error("SimBridge: failed to encode player input (action=%s)" % action_name)
return ERR_CANT_CREATE
_outbound_buffer.append(encoded)
# Use the server's current tick so drain_for_tick processes this input immediately.
# The client-side timestamp_msec is only useful for ordering within a frame.
var tick: int = GameState.current_tick
var entry: Dictionary = { "tick": tick, "action_name": action_name }
# Data variants (e.g. UsePerceptionMode) carry payload
var action_data: Variant = player_input.get("action_data")
if action_data != null:
entry["action_data"] = action_data
_outbound_buffer.append(entry)
return OK
# Poll for snapshot from simulation.
@@ -187,20 +214,27 @@ func poll_snapshot() -> Variant:
return null
# Called by transport layer when raw bytes arrive from the server.
# Latest-wins semantics: newer snapshots replace unconsumed ones. This is correct
# for real-time rendering (stale frames are worthless). Upgrade to queue if needed.
# Latest-wins for positional state (stale frames are worthless), but one-shot
# events (monologue, dialogue) are carried forward from overwritten snapshots
# so they aren't silently dropped when server ticks faster than client consumes.
func receive_bytes(bytes: PackedByteArray) -> void:
var snapshot = Protocol.decode_snapshot(bytes)
if snapshot != null:
if _last_snapshot != null:
push_warning("SimBridge: overwriting unconsumed snapshot (tick %s replaced by %s)" % [_last_snapshot.tick, snapshot.tick])
_last_snapshot = snapshot
if snapshot == null:
push_warning("SimBridge: decode_snapshot returned null for %d bytes" % bytes.size())
return
if _last_snapshot != null:
# Carry forward one-shot events the client hasn't consumed yet.
if snapshot.get("current_monologue") == null and _last_snapshot.get("current_monologue") != null:
snapshot["current_monologue"] = _last_snapshot["current_monologue"]
if snapshot.get("current_dialogue") == null and _last_snapshot.get("current_dialogue") != null:
snapshot["current_dialogue"] = _last_snapshot["current_dialogue"]
_last_snapshot = snapshot
# Drain the outbound buffer. Returns encoded messages for transport.
func drain_outbound() -> Array[PackedByteArray]:
var messages = _outbound_buffer.duplicate()
# Drain the outbound buffer. Returns raw input entries for batch encoding.
func drain_outbound() -> Array[Dictionary]:
var inputs = _outbound_buffer.duplicate()
_outbound_buffer.clear()
return messages
return inputs
# Map InputMapper.Action enum values to wire-format action names (matching Rust PlayerAction).
# OPEN_MENU is client-only — no Rust equivalent, not sent over the wire.
@@ -217,27 +251,287 @@ static func _action_enum_to_wire(action: int) -> String:
InputMapper.Action.INTERACT: return "Interact"
InputMapper.Action.USE_PERCEPTION_MODE: return "UsePerceptionMode"
InputMapper.Action.PAUSE: return "Pause"
InputMapper.Action.UNPAUSE: return "Unpause"
InputMapper.Action.TOGGLE_STANCE_UP: return "ToggleStanceUp"
InputMapper.Action.TOGGLE_STANCE_DOWN: return "ToggleStanceDown"
InputMapper.Action.OPEN_MENU:
# Client-only action, not part of wire protocol
push_warning("SimBridge: OPEN_MENU is client-only, not sent to server")
return ""
return "" # Client-only action, not part of wire protocol
InputMapper.Action.BUG_REPORT:
return "" # Client-only action (#495), not part of wire protocol
_:
push_warning("SimBridge: unknown action enum %s" % action)
return ""
# Hardcoded test snapshot matching Protocol format (deterministic per D-010 principle 4).
# Uses the same {tick, entities} schema as Protocol.decode_snapshot() returns.
# Dynamic test snapshot — processes queued inputs to move player, generates
# visibility based on current position. Matches Protocol.decode_snapshot() format.
# NOTE: Test coordinate space (player at 10,10; NPC at 12,9; wall at 12,10)
# is intentionally decoupled from the E2E proof room (player at 16,16; NPC at
# 16,13; wall at 16,14). This ensures standalone tests don't depend on server
# map layout and can exercise the rendering pipeline independently.
func _test_snapshot() -> Dictionary:
_test_tick += 1
# Process queued inputs
for action_name in _test_input_queue:
if action_name == "Interact":
# Mock dialogue trigger (#434): if near NPC, start dialogue
var npc_pos := Vector2i(12, 9)
var dist := absi(_test_player_pos.x - npc_pos.x) + absi(_test_player_pos.y - npc_pos.y)
if dist <= 2 and _test_has_los(_test_player_pos, npc_pos):
_test_in_dialogue = true
continue
var delta := _action_to_delta(action_name)
var new_pos := _test_player_pos + delta
if _test_is_walkable(new_pos):
_test_player_pos = new_pos
if delta != Vector2i.ZERO:
_test_facing = _delta_to_facing(delta)
# Walk-away dismisses dialogue (D-064)
if _test_in_dialogue:
_test_in_dialogue = false
_test_input_queue.clear()
var px := _test_player_pos.x
var py := _test_player_pos.y
# Build entities — player always visible
var entities: Array = [{
"entity_id": 1,
"x": float(px),
"y": float(py),
"z": 0,
"kind": { "variant": "Player", "data": null },
"visibility": "Forward",
}]
# NPC at (12, 9) — visible if within range and not blocked by wall at (12, 10)
var npc_pos := Vector2i(12, 9)
var npc_dist := absi(px - npc_pos.x) + absi(py - npc_pos.y)
if npc_dist <= 4 and _test_has_los(Vector2i(px, py), npc_pos):
var sector: String = "Forward" if npc_pos.y <= py else "Peripheral"
entities.append({
"entity_id": 2,
"x": float(npc_pos.x),
"y": float(npc_pos.y),
"z": 0,
"kind": { "variant": "Npc", "data": null },
"visibility": sector,
})
# v4: nearby_interactions when NPC is nearby and visible (#404/#405)
var nearby: Array = []
if npc_dist <= 2 and _test_has_los(Vector2i(px, py), npc_pos):
nearby.append({
"entity_id": 2,
"entity_type": "Npc",
"distance": npc_dist,
"verbs": [
{"kind": "Talk", "label": "Talk", "priority": 1, "available": true},
{"kind": "ExamineNpc", "label": "Observe", "priority": 2, "available": true},
],
})
# v5: monologue on first tick (#414)
var monologue: Variant = null
if _test_tick == 1:
monologue = {
"id": "test_enter_001",
"text": "Sova Transit District. Population twelve thousand and change.",
"duration_seconds": 5.0,
}
# v7: mock dialogue (#435, D-061/D-062) — triggered by Interact near NPC
# Sustained: dialogue persists across ticks while _test_in_dialogue is true.
# Movement (walk-away) clears it. Client consume-once guards against re-show.
# Options: structured {text, response_id, priority} per #435.
var dialogue: Variant = null
if _test_in_dialogue:
dialogue = {
"npc_name": "Kael",
"npc_entity_id": 2,
"speech": "Haven't seen you around the transit hub before. You new to Sova, or just passing through?",
"options": [
{"text": "Just arrived. Still getting my bearings.", "response_id": "kael_greet_01", "priority": 1, "confrontation": false},
{"text": "Passing through. Know where I can find work?", "response_id": "kael_greet_02", "priority": 2, "confrontation": false},
{"text": "I saw you near the cargo bay last night.", "response_id": "kael_confront_01", "priority": 3, "confrontation": true},
],
}
# v7: mock pending_recognitions (#431, D-059/D-060) — cognitive delay fog entity
# Entity at (13, 12) in fog: starts as grey blob, transitions to recognized over 6 ticks.
# Cycles every 12 ticks: 6 ticks recognizing, 6 ticks off (simulates repeat encounters).
var pending_recs: Array = []
var cycle_pos := _test_tick % 12
if cycle_pos < 6:
var total_delay := 6
var remaining := total_delay - cycle_pos
pending_recs.append({
"entity_id": 100,
"x": 13.5,
"y": 12.5,
"z": 0,
"remaining_ticks": remaining,
"total_delay_ticks": total_delay,
})
return {
"tick": _test_tick,
"entities": [
{
"entity_id": 1,
"x": 10.0,
"y": 10.0,
"z": 0,
"kind": { "variant": "Npc", "data": null },
},
],
"version": Protocol.PROTOCOL_VERSION,
"game_time": {
"day": 0,
"time_of_day": _test_tick * 10,
"day_phase": "Morning",
"tick_rate": "Full",
},
"player_facing": _test_facing,
"player_stance": "Walk",
"player_inventory": [],
"entities": entities,
"tiles": _test_tiles(),
"visible_tiles": _test_visible_tiles(),
"visible_positions": _test_visible_positions(),
"nearby_interactions": nearby,
"current_monologue": monologue,
"current_dialogue": dialogue,
"pending_recognitions": pending_recs,
}
# Generate a small test room: 8x6 room with walls, a door, and floor
func _test_tiles() -> Array:
var tiles: Array = []
var room_x := 7
var room_y := 7
var room_w := 8
var room_h := 8
for x in range(room_x, room_x + room_w):
for y in range(room_y, room_y + room_h):
var is_edge := (x == room_x or x == room_x + room_w - 1
or y == room_y or y == room_y + room_h - 1)
var tile_type: String
if is_edge:
# Door on the south wall, center
if y == room_y + room_h - 1 and x == room_x + room_w / 2:
tile_type = "door"
else:
tile_type = "wall"
else:
tile_type = "floor"
tiles.append({"x": x, "y": y, "z": 0, "type": tile_type})
# Corridor south of the door
var door_x := room_x + room_w / 2
for y in range(room_y + room_h, room_y + room_h + 4):
tiles.append({"x": door_x - 1, "y": y, "z": 0, "type": "wall"})
tiles.append({"x": door_x, "y": y, "z": 0, "type": "floor"})
tiles.append({"x": door_x + 1, "y": y, "z": 0, "type": "wall"})
return tiles
# Test visible tiles with visibility sectors (v2 format)
# Tiles ahead of the player are Forward, others Peripheral.
func _test_visible_tiles() -> Array:
var vtiles: Array = []
var px := _test_player_pos.x
var py := _test_player_pos.y
var radius := 4
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:
var sector: String = "Forward" if y <= py else "Peripheral"
vtiles.append({"x": x, "y": y, "z": 0, "visibility": sector})
return vtiles
# Test visibility: tiles within radius 4 of player, inside room bounds
func _test_visible_positions() -> Array:
var positions: Array = []
var px := _test_player_pos.x
var py := _test_player_pos.y
var radius := 4
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
# -- Test mode helpers --
const _TEST_WALLS: Array = [
# Room walls (8x8 room from (7,7) to (14,14))
Vector2i(7,7), Vector2i(8,7), Vector2i(9,7), Vector2i(10,7),
Vector2i(11,7), Vector2i(12,7), Vector2i(13,7), Vector2i(14,7),
Vector2i(7,14), Vector2i(8,14), Vector2i(9,14), Vector2i(10,14),
Vector2i(11,14), Vector2i(12,14), Vector2i(13,14), Vector2i(14,14),
Vector2i(7,8), Vector2i(7,9), Vector2i(7,10), Vector2i(7,11),
Vector2i(7,12), Vector2i(7,13),
Vector2i(14,8), Vector2i(14,9), Vector2i(14,10), Vector2i(14,11),
Vector2i(14,12), Vector2i(14,13),
# Interior wall blocking NPC
Vector2i(12, 10),
]
func _test_is_walkable(pos: Vector2i) -> bool:
return not _TEST_WALLS.has(pos)
# Simple LOS check — blocked if a wall tile sits between start and end
func _test_has_los(from: Vector2i, to: Vector2i) -> bool:
# Bresenham-lite: check tiles along the line
var dx := absi(to.x - from.x)
var dy := absi(to.y - from.y)
var sx := 1 if from.x < to.x else -1
var sy := 1 if from.y < to.y else -1
var err := dx - dy
var cx := from.x
var cy := from.y
while true:
if cx == to.x and cy == to.y:
return true
if Vector2i(cx, cy) != from and not _test_is_walkable(Vector2i(cx, cy)):
return false
var e2 := 2 * err
if e2 > -dy:
err -= dy
cx += sx
if e2 < dx:
err += dx
cy += sy
return true
static func _action_to_delta(action_name: String) -> Vector2i:
match action_name:
"MoveNorth": return Vector2i(0, -1)
"MoveNortheast": return Vector2i(1, -1)
"MoveEast": return Vector2i(1, 0)
"MoveSoutheast": return Vector2i(1, 1)
"MoveSouth": return Vector2i(0, 1)
"MoveSouthwest": return Vector2i(-1, 1)
"MoveWest": return Vector2i(-1, 0)
"MoveNorthwest": return Vector2i(-1, -1)
_: return Vector2i.ZERO
static func _delta_to_facing(delta: Vector2i) -> String:
match delta:
Vector2i(0, -1): return "North"
Vector2i(1, -1): return "Northeast"
Vector2i(1, 0): return "East"
Vector2i(1, 1): return "Southeast"
Vector2i(0, 1): return "South"
Vector2i(-1, 1): return "Southwest"
Vector2i(-1, 0): return "West"
Vector2i(-1, -1): return "Northwest"
_: return "North"
+92
View File
@@ -0,0 +1,92 @@
extends Node
# UIStrings — loads UI display text from data/ui-strings.yaml.
# Access: UIStrings.get_text("hud.mode_label") -> "Mode"
# Falls back to the key itself if not found, with a push_warning.
const STRINGS_PATH: String = "res://data/ui-strings.yaml"
var _strings: Dictionary = {}
var _loaded: bool = false
func _ready() -> void:
_load_strings()
func _load_strings() -> void:
if not FileAccess.file_exists(STRINGS_PATH):
push_warning("UIStrings: file not found: %s" % STRINGS_PATH)
return
var file := FileAccess.open(STRINGS_PATH, FileAccess.READ)
if file == null:
push_warning("UIStrings: failed to open: %s" % STRINGS_PATH)
return
var text := file.get_as_text()
file.close()
_strings = _parse_yaml(text)
_loaded = true
print("UIStrings: loaded %d strings from %s" % [_strings.size(), STRINGS_PATH])
## Get a UI string by dotted key. Returns the key itself if not found.
func get_text(key: String) -> String:
if _strings.has(key):
return _strings[key]
push_warning("UIStrings: missing key '%s'" % key)
return key
## Check if a key exists.
func has_key(key: String) -> bool:
return _strings.has(key)
## Get all keys.
func get_all_keys() -> PackedStringArray:
return PackedStringArray(_strings.keys())
## Reload from disk (useful for hot-reload during development).
func reload() -> void:
_strings.clear()
_loaded = false
_load_strings()
## Parse YAML with arbitrary nesting depth.
## Returns flat Dictionary with dotted keys: { "section.sub.key": "value" }.
static func _parse_yaml(text: String) -> Dictionary:
var strings := {}
var stack: Array = [] # [[indent, key], ...]
for line in text.split("\n"):
var stripped := line.strip_edges(false, true)
if stripped.is_empty() or stripped.begins_with("#"):
continue
var indent := line.length() - line.lstrip(" ").length()
var content := stripped.strip_edges()
var colon_pos := content.find(":")
if colon_pos < 0:
continue
var key := content.substr(0, colon_pos).strip_edges()
var val := content.substr(colon_pos + 1).strip_edges()
# Trailing comment without a value — treat as section header
if val.begins_with("#"):
val = ""
# Pop sections at same or deeper indent
while stack.size() > 0 and stack.back()[0] >= indent:
stack.pop_back()
if val.is_empty():
# Section header — push onto stack
stack.push_back([indent, key])
else:
# Leaf value — extract from quotes or strip inline comment
if val.begins_with("\""):
var end_quote := val.find("\"", 1)
if end_quote > 0:
val = val.substr(1, end_quote - 1)
else:
val = val.substr(1)
else:
var comment_pos := val.find(" #")
if comment_pos >= 0:
val = val.substr(0, comment_pos).strip_edges()
var dotted_key := ""
for entry in stack:
dotted_key += entry[1] + "."
dotted_key += key
strings[dotted_key] = val
return strings
@@ -0,0 +1 @@
uid://xm2hswjm3pss
+82
View File
@@ -0,0 +1,82 @@
class_name Constants
# Shared constants used across rendering, game state, and protocol layers.
## Tile size in pixels — all renderers and coordinate conversions use this.
const TILE_SIZE: int = 32
# D-049 Z-level rendering pipeline — three-scope architecture.
# Full spec: docs/architecture/z-layer-gap-analysis.md
#
# WORLD SCOPE (z:-200 to z:900, inside FogGroup CanvasGroup)
# All world content composited into one texture, then fog drawn over it.
# Y-sort contract: all children of YSortGroup MUST use z_index = 0.
# z_index is PRIMARY sort, y-position is SECONDARY (Godot #62715).
const Z_FLOOR: int = 0 # Floor tiles — ground plane
const Z_FLOOR_OBJECTS: int = 10 # Floor objects — cosmetic, ground shadows
# z:20-99 reserved: liquid surface (z:110), surface effects
const Z_YSORT: int = 100 # YSortGroup — furniture + entities + walls, all z:0 relative
# z:110 reserved: liquid surface occlusion (alpha by depth)
# z:150-199 reserved: ground VFX (smoke origins, gas pools, sparks)
const Z_AIRBORNE: int = 200 # Projectiles, low-flying objects (scale ~1.0)
# z:250-299 reserved: mid-air VFX (rising smoke, floating particles)
const Z_OVERHEAD: int = 300 # Ceiling edges, upper structure, semi-transparent
# z:325-349 reserved: ceiling VFX (smoke through ceiling)
const Z_HIGH_AIRBORNE: int = 350 # Above-ceiling flying (scale 1.03-1.30, alpha fades)
const Z_UPPER_CONTENT: int = 400 # Upper-floor entities (rare with fixed camera)
# z:500-899 reserved: edge cases
const Z_FOG: int = 900 # FogOverlay — OUTSIDE FogGroup, fog shader
const Z_FOG_ENTITIES: int = 950 # FogEntities — cognitive delay blobs/pings (D-059/D-060)
# Lower floors: z:-100 per floor (floor-1: z:-100 to z:-1, floor-2: z:-200 to z:-101)
# z:-75 to z:-51 reserved: lower floor VFX
#
# INSERT SCOPE (CanvasLayer 10)
# Bloom-rendered, not affected by fog or camera transform.
const CANVAS_INSERT: int = 10 # CanvasLayer number for InsertOverlay
#
# UI SCOPE (CanvasLayer 20)
# HUD, monologue, cursor — always visible.
const CANVAS_UI: int = 20 # CanvasLayer number for UILayer
#
# MODAL SCOPE (CanvasLayer 30)
# Full-screen overlays: pause, inventory modal, death screen.
const CANVAS_MODAL: int = 30 # CanvasLayer number for ModalLayer
#
# Rendering ceiling: 10 floors (25m) above current floor.
# Above this: no sprites, ground shadows + environmental effects only.
const RENDER_CEILING_FLOORS: int = 10
# Visible floor window (looking down): current - 2 floors (detailed + parallax).
const VISIBLE_FLOOR_DEPTH: int = 2
# D-033: Entity relationship color palette
# Color represents the player's RELATIONSHIP to the entity, not an objective property.
# Phase 1: default colors mapped by entity kind (Player/Npc/Object/Terrain).
# Phase 2 (#361): colors derived from RelationshipState via the knowledge graph.
const ENTITY_COLOR_UNKNOWN: Color = Color("#4a9ebb") # Unknown/Neutral — cool teal
const ENTITY_COLOR_FRIENDLY: Color = Color("#6bc9a6") # Known/Friendly — soft green
const ENTITY_COLOR_POI: Color = Color("#e8c547") # Person of Interest — warm amber
const ENTITY_COLOR_HOSTILE: Color = Color("#d45d5d") # Hostile/Dangerous — muted red
const ENTITY_COLOR_OBJECT: Color = Color("#8b8ba0") # Static objects — muted grey
const ENTITY_COLOR_PLAYER: Color = Color("#e0e8ff") # Player character (detective)
# D-033 color lookup by entity kind (Phase 1: defaults, Phase 2 #361: relationship-based)
static func color_for_entity_kind(entity_data: Dictionary) -> Color:
var kind_variant: String = entity_data.get("kind", {}).get("variant", "")
match kind_variant:
"Player": return ENTITY_COLOR_PLAYER
"Npc": return ENTITY_COLOR_UNKNOWN
"Object", "Terrain": return ENTITY_COLOR_OBJECT
_: return ENTITY_COLOR_OBJECT
# D-048/D-056: Insert-styled UI color palette
# Used by dialogue box, interaction list, radial menu, and other diegetic insert UI.
const INSERT_COLOR_TEXT: Color = Color("#c8d0e0") # Default insert text — white-blue
const INSERT_COLOR_HOVER: Color = Color("#e8c547") # Hover/highlight — amber POI
const INSERT_COLOR_ACTIVE: Color = Color("#6bc9a6") # Active/pressed — friendly green
# D-015: Peripheral vision dimming
const PERIPHERAL_ALPHA: float = 0.5
# Facing direction indicator
const FACING_INDICATOR_SIZE: float = 6.0
const FACING_INDICATOR_OFFSET: float = 14.0
+1
View File
@@ -0,0 +1 @@
uid://wmbu7ivynu5d
+194 -4
View File
@@ -1,30 +1,220 @@
extends Node2D
@onready var world_renderer = $World
@onready var fog_entities = $World/FogEntities # D-059/D-060: cognitive delay fog visualization
@onready var camera = $Camera2D
@onready var hud = $UILayer/HUD
@onready var monologue_display = $UILayer/MonologueDisplay
@onready var interaction_prompt = $InsertOverlay/InteractionPrompt # v0.1 single-line fallback
@onready var interaction_list = $InsertOverlay/InteractionList # D-057: z-layer 6
@onready var world_radial = $InsertOverlay/WorldRadial # D-058: z-layer 6
@onready var dialogue_box = $InsertOverlay/DialogueBox # D-061: z-layer 6
@onready var inventory_grid = $UILayer/InventoryGrid # D-065: z-layer 7
@onready var stance_indicator = $UILayer/StanceIndicator # D-053: z-layer 7
@onready var cursor_renderer = $UILayer/CursorRenderer # D-056: z-layer 7
@onready var gauntlet_hud = $UILayer/GauntletHUD # #496: room timer + personal bests
@onready var bug_report_dialog = $ModalLayer/BugReportDialog # #495: F12 WRONG button
var _last_dialogue_npc_id: int = -1 # D-064: NPC entity_id for WalkAway input
var _camera_anchored: bool = false
var _last_monologue_tick: int = -1 # Prevent re-consuming monologue when same tick polled twice
var _last_dialogue_tick: int = -1
func _ready() -> void:
print("The Settled Reach — client initialized")
# Connect to simulation (will use test mode initially)
# Disable camera smoothing during init. Camera2D's position_smoothing
# lerps an internal smoothed_camera_pos toward global_position each frame.
# That smoothed position initializes at (0,0) — the Camera2D's default in
# the .tscn. Even after we set global_position to the player coords,
# smoothing causes the viewport to still show (0,0) on the first rendered
# frame because the lerp hasn't converged. With smoothing OFF, the viewport
# uses global_position directly. Re-enabled in _process() after anchor.
camera.position_smoothing_enabled = false
# Connect to simulation (test mode sets CONNECTED immediately)
SimBridge.connect_to_sim()
# Camera anchor: snap to player position before the first frame renders.
# In test mode poll_snapshot() returns synchronously — position is set
# immediately. In live mode the snapshot isn't available yet — _process
# handles it. No reset_smoothing() needed: smoothing is OFF.
var first_snapshot: Variant = SimBridge.poll_snapshot()
if first_snapshot != null:
GameState.apply_snapshot(first_snapshot)
camera.global_position = GameState.player_position * Constants.TILE_SIZE
_camera_anchored = true
# D-061: Connect dialogue box signals
if dialogue_box:
dialogue_box.option_selected.connect(_on_dialogue_option_selected)
dialogue_box.dialogue_dismissed.connect(_on_dialogue_dismissed)
dialogue_box.confrontation_monologue.connect(_on_confrontation_monologue)
# #496: Print gauntlet session summary on disconnect
if gauntlet_hud:
SimBridge.connection_state_changed.connect(_on_connection_state_changed)
func _process(_delta: float) -> void:
# Main game loop: poll snapshot, apply state, flush input
var snapshot = SimBridge.poll_snapshot()
var snapshot: Variant = SimBridge.poll_snapshot()
if snapshot != null:
GameState.apply_snapshot(snapshot)
# Late anchor: live mode — first snapshot arrives during _process.
# Smoothing is already OFF (disabled in _ready), so setting
# global_position takes effect immediately with no lerp.
if not _camera_anchored:
camera.global_position = GameState.player_position * Constants.TILE_SIZE
_camera_anchored = true
# Update renderers with new state
if world_renderer and world_renderer.has_method("update_from_state"):
world_renderer.update_from_state()
# Track camera to player position (D-015)
camera.position = GameState.player_position * 32 # tile-space to pixel-space
# D-057: Update interaction list from game state
# Suppress during dialogue — player is in conversation, verb list is noise
if interaction_list and interaction_list.has_method("update_from_state"):
if dialogue_box and dialogue_box.is_dialogue_active():
if interaction_list.is_showing():
interaction_list._hide()
else:
interaction_list.update_from_state()
# D-065: Update inventory grid
if inventory_grid and inventory_grid.has_method("update_from_state"):
inventory_grid.update_from_state()
# D-053: Update stance indicator
if stance_indicator and stance_indicator.has_method("update_from_state"):
stance_indicator.update_from_state()
# D-059/D-060: Update fog entity visualization (#431)
if fog_entities and fog_entities.has_method("update_from_state"):
fog_entities.update_from_state()
# #496: Update gauntlet HUD (room timer + personal bests)
if gauntlet_hud and gauntlet_hud.has_method("update_from_state"):
gauntlet_hud.update_from_state()
# Show monologue if server sent one this tick (#414)
_consume_monologue()
# D-061: Show dialogue if server sent one this tick (#434)
_consume_dialogue()
# Track camera to player position every frame (D-015: locked, no panning)
if _camera_anchored:
camera.global_position = GameState.player_position * Constants.TILE_SIZE
# Re-enable smoothing after the first anchored frame. The frame that just
# rendered used smoothing=OFF (correct viewport from frame one). Now we
# turn smoothing back on and sync its internal state so subsequent frames
# get smooth camera tracking during gameplay.
if _camera_anchored and not camera.position_smoothing_enabled:
camera.position_smoothing_enabled = true
camera.reset_smoothing()
# Send queued input to simulation
var inputs = InputMapper.flush_queue()
for input in inputs:
# #495: F12 WRONG button — client-only, trigger bug report capture
if input.action == InputMapper.Action.BUG_REPORT:
if bug_report_dialog and not bug_report_dialog.is_active():
bug_report_dialog.start_capture()
continue
if input.action == InputMapper.Action.INTERACT:
# D-057: prefer interaction list (multi-verb), fall back to prompt (v0.1)
var target_id: int = -1
var verb: String = ""
if interaction_list and interaction_list.has_method("get_interaction_target"):
target_id = interaction_list.get_interaction_target()
verb = interaction_list.get_selected_verb()
if target_id < 0 and interaction_prompt:
target_id = interaction_prompt.get_interaction_target()
verb = interaction_prompt.get_selected_verb()
# Always send struct form for Interact (#415) — server expects named fields
if target_id >= 0:
input["action_data"] = {
"target_entity_id": target_id,
"verb": verb,
}
else:
input["action_data"] = {
"target_entity_id": null,
"verb": null,
}
SimBridge.send_input(input)
# Consume-once per tick: show monologue text, then clear.
# Tick guard prevents re-triggering when the same tick is polled multiple
# times (client FPS > sim tick rate).
func _consume_monologue() -> void:
if GameState.current_monologue == null or not monologue_display:
return
if GameState.current_tick == _last_monologue_tick:
return
_last_monologue_tick = GameState.current_tick
var mono: Dictionary = GameState.current_monologue
monologue_display.show_monologue(mono.get("text", ""), mono.get("duration_seconds", 5.0))
GameState.current_monologue = null
# Consume-once per tick with ID tracking: show dialogue, then clear.
# Tick guard + is_dialogue_active check prevent re-triggering.
func _consume_dialogue() -> void:
if GameState.current_dialogue == null or not dialogue_box:
return
if GameState.current_tick == _last_dialogue_tick:
return
if dialogue_box.is_dialogue_active():
GameState.current_dialogue = null
return
_last_dialogue_tick = GameState.current_tick
var dlg: Dictionary = GameState.current_dialogue
_last_dialogue_npc_id = dlg.get("npc_entity_id", -1)
dialogue_box.show_dialogue(
dlg.get("npc_name", ""),
dlg.get("speech", ""),
dlg.get("options", [])
)
GameState.current_dialogue = null
# D-061: Handle dialogue option selection → send to server
func _on_dialogue_option_selected(response_id: String, text: String) -> void:
SimBridge.send_input({
"action": InputMapper.Action.INTERACT,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {
"target_entity_id": null,
"verb": "DialogueResponse",
"response_id": response_id,
},
})
# D-063: Handle confrontation beat monologue → show on monologue display (layer 7)
func _on_confrontation_monologue(text: String, duration: float) -> void:
if monologue_display:
monologue_display.show_monologue(text, duration)
# D-064: Handle walk-away → send WalkAway{npc_id} to server
func _on_dialogue_dismissed() -> void:
SimBridge.send_input({
"action": InputMapper.Action.INTERACT,
"timestamp_msec": Time.get_ticks_msec(),
"action_data": {
"target_entity_id": _last_dialogue_npc_id if _last_dialogue_npc_id >= 0 else null,
"verb": "WalkAway",
},
})
# #496: Finalize gauntlet stats on disconnect
func _on_connection_state_changed(old_state: SimBridge.ConnectionState, new_state: SimBridge.ConnectionState) -> void:
if new_state == SimBridge.ConnectionState.DISCONNECTED and gauntlet_hud:
gauntlet_hud.finalize()
+254 -8
View File
@@ -9,11 +9,17 @@ class_name Protocol
## Unit enum variants (no data) → bare strings ("MoveNorth", "Npc")
## Data enum variants → single-element maps ({"UsePerceptionMode": "thermal"})
## Protocol version — must match server PROTOCOL_VERSION in bridge/types.rs.
## Reject snapshots where version != this value.
const PROTOCOL_VERSION: int = 8
# -- Decode: bytes from server → GDScript types --------------------------------
## Decode an ObserverSnapshot from MessagePack bytes.
## Returns { "tick": int, "entities": Array[Dictionary] } or null on error.
## Returns decoded snapshot Dictionary or null on error.
## v2 fields (version, game_time, player_facing, visible_tiles) default to null/empty
## when decoding v1 snapshots for backward compatibility.
static func decode_snapshot(bytes: PackedByteArray) -> Variant:
var result = Messagepack.decode(bytes)
if result.status != null:
@@ -25,6 +31,12 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
push_error("Protocol: snapshot missing required fields")
return null
# Version check: reject snapshots from incompatible server
var version: Variant = raw.get("version")
if version != PROTOCOL_VERSION:
push_error("Protocol: version mismatch (got %s, expected %s). Server and client are out of sync." % [version, PROTOCOL_VERSION])
return null
var entities: Array[Dictionary] = []
var raw_entities: Array = raw["entities"]
var dropped := 0
@@ -41,10 +53,148 @@ static func decode_snapshot(bytes: PackedByteArray) -> Variant:
# GDScript int is signed 64-bit. Rust tick is u64 but will not exceed 2^63
# in any realistic scenario (would require ~29 billion years at 10 ticks/game-minute per D-031).
var tick: int = raw["tick"]
# version already checked above; game_time for HUD display
var game_time: Variant = raw.get("game_time")
# player_facing: FacingDirection is a unit enum → bare string in rmp_serde
var player_facing: Variant = null
var raw_facing: Variant = raw.get("player_facing")
if raw_facing is String:
player_facing = raw_facing
# visible_tiles: Array of {x, y, z, visibility, tile_kind}
var visible_tiles: Array = []
var raw_vtiles: Variant = raw.get("visible_tiles")
if raw_vtiles is Array:
for raw_tile in raw_vtiles:
if raw_tile is Dictionary and raw_tile.has("x") and raw_tile.has("y") and raw_tile.has("z"):
var tile_entry := {
"x": int(raw_tile["x"]),
"y": int(raw_tile["y"]),
"z": int(raw_tile["z"]),
}
var vis: Variant = raw_tile.get("visibility")
if vis is String:
tile_entry["visibility"] = vis
# tile_kind: Floor/Wall/Door/Object — map to client tile type strings
var kind: Variant = raw_tile.get("tile_kind")
if kind is String:
tile_entry["type"] = kind.to_lower()
else:
tile_entry["type"] = "floor"
visible_tiles.append(tile_entry)
# v4: nearby_interactions (#404/#405)
var nearby_interactions: Array = []
var raw_interactions: Variant = raw.get("nearby_interactions")
if raw_interactions is Array:
for raw_ni in raw_interactions:
var ni = _decode_nearby_interaction(raw_ni)
if ni != null:
nearby_interactions.append(ni)
# v5: current_monologue (#414)
var current_monologue: Variant = null
var raw_monologue: Variant = raw.get("current_monologue")
if raw_monologue is Dictionary and raw_monologue.has("text"):
current_monologue = {
"id": str(raw_monologue.get("id", "")),
"text": str(raw_monologue["text"]),
"duration_seconds": float(raw_monologue.get("duration_seconds", 5.0)),
}
# v6: player_stance (#449, D-053) — unit enum → bare string
var player_stance: String = "Walk"
var raw_stance: Variant = raw.get("player_stance")
if raw_stance is String:
player_stance = raw_stance
# v6: player_inventory (#449, D-065) — array of {item_id, name, slot}
var player_inventory: Array = []
var raw_inventory: Variant = raw.get("player_inventory")
if raw_inventory is Array:
for raw_item in raw_inventory:
if raw_item is Dictionary and raw_item.has("item_id") and raw_item.has("name"):
player_inventory.append({
"item_id": int(raw_item["item_id"]),
"name": str(raw_item["name"]),
"slot": int(raw_item.get("slot", 0)),
})
# v7: pending_recognitions (#431, D-059/D-060) — cognitive delay fog entities
# Bounded: server sends at most ~50 pending recognitions per snapshot (practical limit
# given perception range). Excessive arrays are truncated to prevent allocation abuse.
const MAX_PENDING_RECOGNITIONS: int = 64
var pending_recognitions: Array = []
var raw_recognitions: Variant = raw.get("pending_recognitions")
if raw_recognitions is Array:
var count := 0
for raw_pr in raw_recognitions:
if count >= MAX_PENDING_RECOGNITIONS:
push_warning("Protocol: pending_recognitions truncated at %d entries" % MAX_PENDING_RECOGNITIONS)
break
if raw_pr is Dictionary and raw_pr.has("entity_id") and raw_pr.has("x") and raw_pr.has("y"):
pending_recognitions.append({
"entity_id": int(raw_pr["entity_id"]),
"x": float(raw_pr["x"]),
"y": float(raw_pr["y"]),
"z": int(raw_pr.get("z", 0)),
"remaining_ticks": int(raw_pr.get("remaining_ticks", 0)),
"total_delay_ticks": int(raw_pr.get("total_delay_ticks", 1)),
})
count += 1
# v7: current_dialogue (#435, D-061/D-062) — NPC speech + player response options
# Options carry response_id for server round-trip and priority for display ordering.
# Locked options are invisible (D-062): server filters before sending.
var current_dialogue: Variant = null
var raw_dialogue: Variant = raw.get("current_dialogue")
if raw_dialogue is Dictionary and raw_dialogue.has("speech"):
var dialogue_options: Array = []
var raw_options: Variant = raw_dialogue.get("options")
if raw_options is Array:
for raw_opt in raw_options:
if raw_opt is Dictionary and raw_opt.has("text"):
dialogue_options.append({
"text": str(raw_opt["text"]),
"response_id": str(raw_opt.get("response_id", "")),
"priority": int(raw_opt.get("priority", 0)),
"confrontation": bool(raw_opt.get("confrontation", false)),
})
current_dialogue = {
"npc_name": str(raw_dialogue.get("npc_name", "")),
"npc_entity_id": int(raw_dialogue.get("npc_entity_id", -1)),
"speech": str(raw_dialogue["speech"]),
"options": dialogue_options,
}
# v8: dialogue_response (#305, D-028) — NPC's spoken dialogue line from server
# Separate from current_dialogue (client-side rich dialogue state).
var dialogue_response: Variant = null
var raw_dr: Variant = raw.get("dialogue_response")
if raw_dr is Dictionary and raw_dr.has("text"):
dialogue_response = {
"line_id": str(raw_dr.get("line_id", "")),
"text": str(raw_dr["text"]),
"speaker_entity_id": int(raw_dr.get("speaker_entity_id", -1)),
}
return {
"tick": tick,
"entities": entities,
"decode_errors": dropped,
"version": version,
"game_time": game_time,
"player_facing": player_facing,
"player_stance": player_stance,
"player_inventory": player_inventory,
"visible_tiles": visible_tiles,
"nearby_interactions": nearby_interactions,
"current_monologue": current_monologue,
"current_dialogue": current_dialogue,
"dialogue_response": dialogue_response,
"pending_recognitions": pending_recognitions,
}
@@ -56,12 +206,81 @@ static func _decode_entity(raw: Dictionary) -> Variant:
return null
var entity_id: int = raw["entity_id"]
# v2: visibility sector (Forward/Peripheral). null for v1 entities.
var visibility: Variant = null
var raw_vis: Variant = raw.get("visibility")
if raw_vis is String:
visibility = raw_vis
# v4: relationship (D-033) and observation state
var relationship: String = "Unknown"
var raw_rel: Variant = raw.get("relationship")
if raw_rel is String:
relationship = raw_rel
var observation: Variant = "Visible"
var raw_obs: Variant = raw.get("observation")
if raw_obs is String:
observation = raw_obs
elif raw_obs is Dictionary:
observation = _decode_enum_variant(raw_obs)
return {
"entity_id": entity_id,
"x": float(raw["x"]),
"y": float(raw["y"]),
"z": int(raw["z"]),
"kind": _decode_enum_variant(raw["kind"]),
"visibility": visibility,
"relationship": relationship,
"observation": observation,
}
## Decode a NearbyInteraction from a raw msgpack map.
## Returns {entity_id, entity_type, distance, verbs: [{kind, label, priority, available}]} or null.
static func _decode_nearby_interaction(raw) -> Variant:
if not raw is Dictionary:
return null
if not raw.has("entity_id") or not raw.has("verbs"):
push_warning("Protocol: nearby_interaction missing required fields: %s" % str(raw.keys()))
return null
var verbs: Array[Dictionary] = []
var raw_verbs: Variant = raw.get("verbs")
if raw_verbs is Array:
for rv in raw_verbs:
var verb = _decode_verb_option(rv)
if verb != null:
verbs.append(verb)
if verbs.is_empty():
return null
var entity_type: String = ""
var raw_et: Variant = raw.get("entity_type")
if raw_et is String:
entity_type = raw_et
return {
"entity_id": int(raw["entity_id"]),
"entity_type": entity_type,
"distance": int(raw.get("distance", 0)),
"verbs": verbs,
}
## Decode a VerbOption from a raw msgpack map.
## Returns {kind, label, priority, available} or null.
static func _decode_verb_option(raw) -> Variant:
if not raw is Dictionary or not raw.has("label"):
return null
var kind: String = ""
var raw_kind: Variant = raw.get("kind")
if raw_kind is String:
kind = raw_kind
return {
"kind": kind,
"label": str(raw["label"]),
"priority": int(raw.get("priority", 0)),
"available": bool(raw.get("available", true)),
}
@@ -86,13 +305,7 @@ static func _decode_enum_variant(raw) -> Dictionary:
## "Interact", "UsePerceptionMode", "Pause", "Unpause"
## action_data: null for unit variants, String for UsePerceptionMode
static func encode_player_input(tick: int, action_name: String, action_data: Variant = null) -> PackedByteArray:
var action_value: Variant
if action_data != null:
# Data variant → single-element map
action_value = { action_name: action_data }
else:
# Unit variant → bare string
action_value = action_name
var action_value: Variant = _encode_action(action_name, action_data)
var input := {
"tick": tick,
@@ -107,6 +320,39 @@ static func encode_player_input(tick: int, action_name: String, action_data: Var
return result.value
## Encode an array of PlayerInputs to MessagePack bytes (Vec<PlayerInput> wire format).
## Server expects one framed message per tick containing all inputs as a msgpack array.
## Each entry: { "tick": int, "action_name": String, "action_data": Variant (optional) }
static func encode_player_inputs(inputs: Array) -> PackedByteArray:
var wire_inputs: Array = []
for input in inputs:
var action_name: String = input["action_name"]
var action_data: Variant = input.get("action_data")
wire_inputs.append({
"tick": input["tick"],
"action": _encode_action(action_name, action_data),
})
var result = Messagepack.encode(wire_inputs)
if result.status != null:
push_error("Protocol: msgpack encode failed: %s" % result.status)
return PackedByteArray()
return result.value
## Encode a PlayerAction for the wire.
## Struct variants (Interact) always need their fields even when null.
## Unit variants (MoveNorth, Pause, etc.) encode as bare strings.
static func _encode_action(action_name: String, action_data: Variant) -> Variant:
if action_data != null:
return { action_name: action_data }
# Interact is a struct variant — server expects named fields, not a bare string
if action_name == "Interact":
return { "Interact": { "target_entity_id": null, "verb": null } }
return action_name
## Decode a PlayerInput from MessagePack bytes (used in tests / echo scenarios).
## Returns { "tick": int, "action": { "variant": String, "data": Variant } } or null.
static func decode_player_input(bytes: PackedByteArray) -> Variant:
+312
View File
@@ -0,0 +1,312 @@
class_name CursorRenderer
extends Node2D
## Cursor state machine — 4 geometric states, 150ms linear transitions (D-056).
## Insert-styled cursor on z-layer 7 (UILayer CanvasLayer).
## Detects entity hover via world-space proximity to visible entities.
enum State { DEFAULT, ENTITY_HOVER, OBJECT_HOVER, WEAPON_AIM }
# --- Public ---
var current_state: State = State.DEFAULT
var hovered_entity_id: int = -1
var weapon_mode_active: bool = false
signal state_changed(new_state: State)
signal hovered_entity_changed(entity_id: int)
# D-056 colors
const COLOR_DEFAULT := Color("#c8d0e0")
const COLOR_OBJECT := Color("#8b8ba0")
const COLOR_OBJECT_FLAGGED := Color("#e8c547")
const COLOR_WEAPON := Color("#f0e8d8")
const TRANSITION_SEC := 0.15 # 150ms linear (D-056)
const HOVER_RADIUS_PX := 16.0 # World pixels — ~half a tile
# Bracket geometry (screen pixels — sized for 24px entity at 2x zoom)
const BRACKET_HALF := 26.0
const BRACKET_ARM := 8.0
# --- Transition state ---
var _target: State = State.DEFAULT
var _t: float = 1.0
var _from: Dictionary = {}
var _time: float = 0.0
var _mouse_inside: bool = true
var _shift_held: bool = false
# Hover tracking
var _hover_color: Color = COLOR_DEFAULT
var _hover_offset: Vector2 = Vector2.ZERO
# Interpolated draw params
var _gap: float = 4.0
var _len: float = 6.0
var _rot: float = 0.0
var _thick: float = 1.0
var _color: Color = COLOR_DEFAULT
var _bloom: float = 0.4
var _bracket_a: float = 0.0
var _alpha: float = 0.45
func _ready() -> void:
Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
_from = _snapshot()
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_MOUSE_EXIT:
_mouse_inside = false
visible = false
elif what == NOTIFICATION_WM_MOUSE_ENTER:
_mouse_inside = true
visible = true
func _process(delta: float) -> void:
if not _mouse_inside:
return
_time += delta
position = get_viewport().get_mouse_position()
_detect_hover()
if _t < 1.0:
_t = minf(_t + delta / TRANSITION_SEC, 1.0)
_interpolate()
if _t >= 1.0:
current_state = _target
queue_redraw()
# --- Hover detection (automatic, from GameState.visible_entities) ---
func _detect_hover() -> void:
var prev_id := hovered_entity_id
if weapon_mode_active:
var found := _find_nearest_entity()
hovered_entity_id = found.id
_hover_color = found.color
_hover_offset = found.offset
_set_target(State.WEAPON_AIM)
if hovered_entity_id != prev_id:
hovered_entity_changed.emit(hovered_entity_id)
return
var found := _find_nearest_entity()
hovered_entity_id = found.id
_hover_color = found.color
_hover_offset = found.offset
var new_state := State.DEFAULT
if found.id >= 0:
new_state = State.ENTITY_HOVER if found.kind == "Npc" else State.OBJECT_HOVER
_set_target(new_state)
if hovered_entity_id != prev_id:
hovered_entity_changed.emit(hovered_entity_id)
func _find_nearest_entity() -> Dictionary:
var xform := get_viewport().get_canvas_transform()
var mouse_screen := get_viewport().get_mouse_position()
var mouse_world: Vector2 = xform.affine_inverse() * mouse_screen
var best_dist := INF
var result := { id = -1, kind = "", color = COLOR_DEFAULT, offset = Vector2.ZERO }
for entity in GameState.visible_entities:
if not entity.has("entity_id") or not entity.has("x") or not entity.has("y"):
continue
if entity.entity_id == GameState.player_entity_id:
continue
var center := Vector2(
floorf(entity.x) * Constants.TILE_SIZE + Constants.TILE_SIZE * 0.5,
floorf(entity.y) * Constants.TILE_SIZE + Constants.TILE_SIZE * 0.5
)
var dist := mouse_world.distance_to(center)
if dist < HOVER_RADIUS_PX and dist < best_dist:
best_dist = dist
result.id = entity.entity_id
result.kind = entity.get("kind", {}).get("variant", "")
result.color = Constants.color_for_entity_kind(entity)
result.offset = (xform * center) - mouse_screen
return result
# --- State transitions ---
func _set_target(new_state: State) -> void:
if new_state == _target:
return
_target = new_state
_from = _snapshot()
# D-056: weapon aim is "hard transition" — skip interpolation
if new_state == State.WEAPON_AIM:
_t = 1.0
_apply_params(_params_for(State.WEAPON_AIM))
current_state = State.WEAPON_AIM
else:
_t = 0.0
state_changed.emit(new_state)
func _snapshot() -> Dictionary:
return { gap = _gap, len = _len, rot = _rot, thick = _thick,
color = _color, bloom = _bloom, bracket_a = _bracket_a, alpha = _alpha }
func _params_for(state: State) -> Dictionary:
match state:
State.ENTITY_HOVER:
return { gap = 10.0, len = 6.0, rot = 0.0, thick = 1.0,
color = _hover_color, bloom = 0.5, bracket_a = 1.0, alpha = 1.0 }
State.OBJECT_HOVER:
return { gap = 4.0, len = 6.0, rot = PI / 4.0, thick = 1.0,
color = _hover_color, bloom = 0.3, bracket_a = 0.0, alpha = 0.8 }
State.WEAPON_AIM:
return { gap = 12.0, len = 9.0, rot = 0.0, thick = 2.0,
color = COLOR_WEAPON, bloom = 0.0, bracket_a = 0.0, alpha = 1.0 }
_:
return { gap = 4.0, len = 6.0, rot = 0.0, thick = 1.0,
color = COLOR_DEFAULT, bloom = 0.4, bracket_a = 0.0, alpha = 0.45 }
func _apply_params(p: Dictionary) -> void:
_gap = p.gap; _len = p.len; _rot = p.rot; _thick = p.thick
_color = p.color; _bloom = p.bloom; _bracket_a = p.bracket_a; _alpha = p.alpha
func _interpolate() -> void:
var p := _params_for(_target)
_gap = lerpf(_from.gap, p.gap, _t)
_len = lerpf(_from.len, p.len, _t)
_rot = lerp_angle(_from.rot, p.rot, _t)
_thick = lerpf(_from.thick, p.thick, _t)
_color = _from.color.lerp(p.color, _t)
_bloom = lerpf(_from.bloom, p.bloom, _t)
_bracket_a = lerpf(_from.bracket_a, p.bracket_a, _t)
_alpha = lerpf(_from.alpha, p.alpha, _t)
# --- Drawing ---
func _draw() -> void:
# _hover_offset is computed in _detect_hover() during _process(), not recalculated
# here. This is safe because Camera2D (tree sibling, earlier in scene order) applies
# its smoothing transform before CursorRenderer._process() reads canvas_transform.
# Nothing modifies the canvas transform between _process() and _draw().
# Bloom pass — wider, semi-transparent glow
if _bloom > 0.01:
var bloom_mod := 1.0
# D-056: ~10% bloom pulse during entity hover
if current_state == State.ENTITY_HOVER:
bloom_mod += sin(_time * 4.0) * 0.1
var bloom_a := _alpha * _bloom * 0.6 * bloom_mod
_draw_ticks(Color(_color, bloom_a), _thick + 3.0)
if _bracket_a > 0.01:
_draw_brackets(_hover_offset, Color(_color, _bracket_a * _bloom * 0.4))
# Crisp pass
_draw_ticks(Color(_color, _alpha), _thick)
if _bracket_a > 0.01:
_draw_brackets(_hover_offset, Color(_color, _bracket_a * _alpha))
func _draw_ticks(color: Color, thickness: float) -> void:
var dirs: Array[Vector2] = [Vector2.UP, Vector2.RIGHT, Vector2.DOWN, Vector2.LEFT]
for dir: Vector2 in dirs:
var d := dir.rotated(_rot)
draw_line(d * _gap, d * (_gap + _len), color, thickness, true)
func _draw_brackets(offset: Vector2, color: Color) -> void:
for sx in [-1.0, 1.0]:
for sy in [-1.0, 1.0]:
var corner := offset + Vector2(sx * BRACKET_HALF, sy * BRACKET_HALF)
draw_line(corner, corner + Vector2(-sx * BRACKET_ARM, 0), color, 1.0, true)
draw_line(corner, corner + Vector2(0, -sy * BRACKET_ARM), color, 1.0, true)
# --- Test-friendly API (expected by test_cursor_states.gd) ---
func get_state() -> String:
match current_state:
State.DEFAULT: return "Default"
State.ENTITY_HOVER: return "EntityHover"
State.OBJECT_HOVER: return "ObjectHover"
State.WEAPON_AIM: return "WeaponAim"
_: return "Default"
func set_hover_target(data: Dictionary) -> void:
var kind: String = data.get("kind", "")
# D-056: cursor changes require LOS
if not data.get("in_los", true):
return
hovered_entity_id = data.get("entity_id", -1)
if kind == "Npc":
var rel: String = data.get("relationship", "Unknown")
match rel:
"Friendly": _hover_color = Constants.ENTITY_COLOR_FRIENDLY
"PersonOfInterest": _hover_color = Constants.ENTITY_COLOR_POI
"Hostile": _hover_color = Constants.ENTITY_COLOR_HOSTILE
_: _hover_color = Constants.ENTITY_COLOR_UNKNOWN
_target = State.ENTITY_HOVER
_t = 1.0
_apply_params(_params_for(State.ENTITY_HOVER))
current_state = State.ENTITY_HOVER
elif kind == "Object" or kind == "Terrain":
_hover_color = COLOR_OBJECT_FLAGGED if data.get("flagged", false) else COLOR_OBJECT
_target = State.OBJECT_HOVER
_t = 1.0
_apply_params(_params_for(State.OBJECT_HOVER))
current_state = State.OBJECT_HOVER
func clear_hover_target() -> void:
hovered_entity_id = -1
_hover_color = COLOR_DEFAULT
_target = State.DEFAULT
_t = 1.0
_apply_params(_params_for(State.DEFAULT))
current_state = State.DEFAULT
func set_weapon_mode(active: bool) -> void:
weapon_mode_active = active
if active:
_target = State.WEAPON_AIM
_t = 1.0
_apply_params(_params_for(State.WEAPON_AIM))
current_state = State.WEAPON_AIM
else:
_target = State.DEFAULT
_t = 1.0
_apply_params(_params_for(State.DEFAULT))
current_state = State.DEFAULT
func set_shift_held(held: bool) -> void:
_shift_held = held
func get_transition_duration() -> float:
return TRANSITION_SEC
func get_cursor_color() -> Color:
return _color
func get_z_layer() -> int:
return Constants.CANVAS_UI # D-049/D-056
func should_show_interactions() -> bool:
return not weapon_mode_active or _shift_held
func get_interaction_range() -> int:
return 2 # D-056: ~2 sim tiles
@@ -0,0 +1 @@
uid://bkbfcalfo1kk8
+111 -21
View File
@@ -1,25 +1,56 @@
class_name EntityRenderer
extends Node2D
# Entity renderer — manages entity sprites under the Entities node
# Creates/updates/removes Sprite2D children based on entity data
# Creates/updates/removes ColorRect children based on entity data
# Entity format (from Protocol v2): {entity_id, x, y, z, kind: {variant, data}, visibility}
#
# Position lerping: entity sprites smoothly slide between tiles instead of snapping.
# The server moves entities in discrete tile steps; the lerp makes this look fluid.
# Speed is tuned so Sprint feels snappy and Walk/Careful/Crouch feel deliberate.
#
# D-033 colors: Phase 1 defaults by entity kind. Phase 2 (#361) will derive
# color from RelationshipState via the knowledge graph.
const TILE_SIZE: int = 32
const TILE_SIZE: int = Constants.TILE_SIZE
const ENTITY_SIZE: int = 24
const ENTITY_OFFSET: float = (TILE_SIZE - ENTITY_SIZE) / 2.0 # center within tile
var entity_nodes: Dictionary = {} # id -> Node2D mapping
# Lerp speed — framerate-independent exponential smoothing.
# At 12.0: ~70% there after 0.1s, ~95% after 0.25s.
# Fast enough for Sprint snappiness, slow enough for Walk to show sliding.
const LERP_SPEED: float = 12.0
var entity_nodes: Dictionary = {} # entity_id -> Node2D
var _entity_targets: Dictionary = {} # entity_id -> Vector2 (target pixel position)
func _ready() -> void:
print("EntityRenderer: Initialized")
func _process(delta: float) -> void:
# Lerp all entity visual positions toward their targets each frame.
# Uses framerate-independent exponential smoothing.
var weight := 1.0 - exp(-LERP_SPEED * delta)
for entity_id in entity_nodes.keys():
if not _entity_targets.has(entity_id):
continue
var node = entity_nodes[entity_id]
var target: Vector2 = _entity_targets[entity_id]
if not node.position.is_equal_approx(target):
node.position = node.position.lerp(target, weight)
# Update entities from snapshot data
func update_entities(entities: Array) -> void:
var active_ids: Array = []
# Create or update entities
for entity_data in entities:
if not entity_data.has("id"):
if not entity_data.has("entity_id"):
continue
var entity_id = entity_data.id
var entity_id = entity_data.entity_id
active_ids.append(entity_id)
# Create entity node if it doesn't exist
@@ -37,38 +68,62 @@ func update_entities(entities: Array) -> void:
for entity_id in ids_to_remove:
_remove_entity_node(entity_id)
# Create a new entity node (placeholder visual)
# Create a new entity node with D-033 color and optional facing indicator
func _create_entity_node(entity_id: int, entity_data: Dictionary) -> void:
var entity_node = ColorRect.new()
entity_node.name = "Entity_" + str(entity_id)
entity_node.size = Vector2(32, 32)
entity_node.pivot_offset = Vector2(16, 16)
entity_node.size = Vector2(ENTITY_SIZE, ENTITY_SIZE)
entity_node.pivot_offset = Vector2(ENTITY_SIZE / 2.0, ENTITY_SIZE / 2.0)
# Color based on type
if entity_data.get("type") == "npc":
entity_node.color = Color(0.3, 0.6, 0.9) # Blue for NPCs
else:
entity_node.color = Color(0.8, 0.8, 0.8) # Gray for unknown
# D-033 color by entity kind (Phase 1 default)
# TODO(#361): derive from RelationshipState via knowledge graph
entity_node.color = _color_for_kind(entity_data)
add_child(entity_node)
entity_nodes[entity_id] = entity_node
# Add facing indicator for the player entity
if entity_id == GameState.player_entity_id:
_add_facing_indicator(entity_node)
# Snap to initial position (no lerp on first appearance)
if entity_data.has("x") and entity_data.has("y"):
var target := Vector2(
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
)
entity_node.position = target
_entity_targets[entity_id] = target
_update_entity_node(entity_id, entity_data)
# Update an existing entity node
# Update an existing entity node (target position, visibility dimming, facing)
func _update_entity_node(entity_id: int, entity_data: Dictionary) -> void:
if not entity_nodes.has(entity_id):
return
var entity_node = entity_nodes[entity_id]
# Update position
if entity_data.has("position"):
var pos = entity_data.position
if pos is Array and pos.size() >= 2:
entity_node.position = Vector2(pos[0] * TILE_SIZE, pos[1] * TILE_SIZE)
else:
push_warning("EntityRenderer: malformed position for entity %s" % entity_id)
# Update target position — the lerp in _process() will smoothly move there.
# Server sends tile-center coords (tile 16 → 16.5), floor to get tile index.
if entity_data.has("x") and entity_data.has("y"):
_entity_targets[entity_id] = Vector2(
floorf(entity_data.x) * TILE_SIZE + ENTITY_OFFSET,
floorf(entity_data.y) * TILE_SIZE + ENTITY_OFFSET
)
# v2: Peripheral vision dimming (D-015)
# null visibility (v1 backward compat) defaults to full alpha
var visibility: Variant = entity_data.get("visibility")
var target_alpha := Constants.PERIPHERAL_ALPHA if visibility == "Peripheral" else 1.0
if not is_equal_approx(entity_node.modulate.a, target_alpha):
entity_node.modulate.a = target_alpha
# v2: Update facing indicator rotation (player entity only)
if entity_id == GameState.player_entity_id:
var indicator = entity_node.get_node_or_null("FacingIndicator")
if indicator != null:
indicator.rotation = _facing_to_rotation(GameState.player_facing)
# Remove an entity node
func _remove_entity_node(entity_id: int) -> void:
@@ -78,3 +133,38 @@ func _remove_entity_node(entity_id: int) -> void:
var entity_node = entity_nodes[entity_id]
entity_node.queue_free()
entity_nodes.erase(entity_id)
_entity_targets.erase(entity_id)
# D-033 color by entity kind — delegates to Constants.color_for_entity_kind
static func _color_for_kind(entity_data: Dictionary) -> Color:
return Constants.color_for_entity_kind(entity_data)
# Add a facing direction indicator triangle to the player entity
func _add_facing_indicator(parent_node: Control) -> void:
var indicator := Polygon2D.new()
indicator.name = "FacingIndicator"
var s := Constants.FACING_INDICATOR_SIZE
var offset := Constants.FACING_INDICATOR_OFFSET
# Triangle pointing up (North), offset from center. Rotates around (0,0).
indicator.polygon = PackedVector2Array([
Vector2(0, -offset - s),
Vector2(-s * 0.6, -offset + s * 0.4),
Vector2(s * 0.6, -offset + s * 0.4),
])
indicator.color = Constants.ENTITY_COLOR_PLAYER
# Position at center of parent ColorRect — rotation around this point
indicator.position = Vector2(ENTITY_SIZE / 2.0, ENTITY_SIZE / 2.0)
parent_node.add_child(indicator)
# Convert facing direction string to rotation in radians (0 = North/up)
static func _facing_to_rotation(facing: String) -> float:
match facing:
"North": return 0.0
"Northeast": return PI / 4.0
"East": return PI / 2.0
"Southeast": return 3.0 * PI / 4.0
"South": return PI
"Southwest": return 5.0 * PI / 4.0
"West": return 3.0 * PI / 2.0
"Northwest": return 7.0 * PI / 4.0
_: return 0.0
+178
View File
@@ -0,0 +1,178 @@
class_name FogEntities
extends Node2D
## Fog entity visualization — renders entities undergoing cognitive delay recognition
## in the fog layer. Between FogOverlay (z:900) and InsertOverlay (CanvasLayer 10).
##
## Three visual states per D-059/D-060:
## 1. Sound pings: 2-3 thin concentric expanding rings (sonar-style)
## 2. Unrecognized entity: grey blob #555566, 0.8s breathing pulse
## 3. Recognized entity: D-033 color glow + silhouette + breathing pulse + position drift
##
## Cognitive delay transition: grey blob → color + silhouette over ~0.3s,
## driven by remaining_ticks / total_delay_ticks ratio from server.
##
## All drawing happens in _draw() — no child scripts needed.
const TILE_SIZE: int = Constants.TILE_SIZE
# D-059: Fog entity colors
const COLOR_UNRECOGNIZED := Color("#555566") # Neutral grey blob
# Intentionally matches Constants.INSERT_COLOR_TEXT / cursor default (#c8d0e0).
# Sonar pings are insert-generated — same visual language as cursor and HUD overlays.
const COLOR_PING := Color("#c8d0e0") # Insert white-blue (D-048/D-056)
# Animation timing
const PULSE_PERIOD: float = 0.8 # Breathing pulse cycle (seconds)
const PULSE_MIN_ALPHA: float = 0.4 # Min alpha during breathing
const PULSE_MAX_ALPHA: float = 0.8 # Max alpha during breathing
const PING_DURATION: float = 1.5 # Sound ping expand + fade (seconds)
const PING_MAX_RADIUS: float = 24.0 # Max ring expand radius (pixels)
const PING_RING_COUNT: int = 3 # Concentric rings per ping
const PING_RING_WIDTH: float = 1.5 # Ring line width (pixels)
const DRIFT_RANGE: float = 0.5 # ±0.5 tile position drift (D-059)
const DRIFT_PERIOD: float = 2.0 # Seconds per drift wander
# Entity blob rendering
const BLOB_RADIUS: float = 10.0
const SILHOUETTE_SCALE: float = 1.3 # Silhouette slightly larger than blob
# Transition thresholds: recognition progress mapped from remaining_ticks/total_delay_ticks.
# Color transition starts at 50% to compress the visual change into ~0.3s (D-060).
const COLOR_TRANSITION_START: float = 0.5
# Silhouette (body shape hint) appears after 80% progress — recognition nearly complete.
const SILHOUETTE_APPEAR_THRESHOLD: float = 0.3
# Silhouette size: approximate torso-sized rectangle in pixels at TILE_SIZE=32 scale.
const SILHOUETTE_SIZE := Vector2(6.0, 10.0)
# Internal state — no child nodes, pure data + _draw()
var _entities: Dictionary = {} # entity_id -> {pos, drift_offset, drift_target, drift_timer, progress}
var _pings: Array = [] # [{pos: Vector2, elapsed: float}]
var _time: float = 0.0
func _process(delta: float) -> void:
# Early return when idle — skip queue_redraw() when nothing to draw
if _entities.is_empty() and _pings.is_empty():
return
_time += delta
# Animate drifts
for eid in _entities.keys():
var e: Dictionary = _entities[eid]
e.drift_timer += delta
if e.drift_timer >= DRIFT_PERIOD:
e.drift_timer = 0.0
e.drift_offset = e.drift_target
e.drift_target = _random_drift()
var t: float = e.drift_timer / DRIFT_PERIOD
e.drift_offset = e.drift_offset.lerp(e.drift_target, t)
# Advance pings, remove expired
var i := _pings.size() - 1
while i >= 0:
_pings[i].elapsed += delta
if _pings[i].elapsed >= PING_DURATION:
_pings.remove_at(i)
i -= 1
queue_redraw()
## Called from main.gd each frame after GameState.apply_snapshot()
func update_from_state() -> void:
var recognitions: Array = GameState.pending_recognitions
var active_ids: Array = []
for rec in recognitions:
var eid: int = rec.entity_id
active_ids.append(eid)
if not _entities.has(eid):
# New entity — spawn with random drift and trigger sonar ping
_entities[eid] = {
"pos": Vector2(rec.x, rec.y),
"drift_offset": _random_drift(),
"drift_target": _random_drift(),
"drift_timer": 0.0,
"progress": 0.0,
}
_pings.append({"pos": Vector2(rec.x, rec.y), "elapsed": 0.0})
# Update server data
var e: Dictionary = _entities[eid]
e.pos = Vector2(rec.x, rec.y)
var total: int = rec.total_delay_ticks
var remaining: int = rec.remaining_ticks
if total > 0:
e.progress = clampf(1.0 - float(remaining) / float(total), 0.0, 1.0)
else:
e.progress = 1.0
# Remove entities no longer pending
var to_remove: Array = []
for eid in _entities.keys():
if eid not in active_ids:
to_remove.append(eid)
for eid in to_remove:
_entities.erase(eid)
func _draw() -> void:
# Breathing pulse — shared across all entities
var pulse_t := fmod(_time, PULSE_PERIOD) / PULSE_PERIOD
var pulse_alpha := lerpf(PULSE_MIN_ALPHA, PULSE_MAX_ALPHA,
0.5 + 0.5 * sin(pulse_t * TAU))
# Draw entity blobs
for eid in _entities.keys():
var e: Dictionary = _entities[eid]
var world_pos: Vector2 = (e.pos + e.drift_offset) * TILE_SIZE
# Color transition: grey → D-033 teal based on recognition progress
# Transition begins at 50% progress (D-060: ~0.3s visual transition within delay window)
var progress: float = e.progress
var color_t: float = clampf((progress - COLOR_TRANSITION_START) / (1.0 - COLOR_TRANSITION_START), 0.0, 1.0)
var blob_color: Color = COLOR_UNRECOGNIZED.lerp(Constants.ENTITY_COLOR_UNKNOWN, color_t)
blob_color.a = pulse_alpha
# Blob — unrecognized: plain circle. Recognized: larger glow + inner shape
if color_t < 1e-3:
# Pure unrecognized: grey blob, no silhouette (D-059)
draw_circle(world_pos, BLOB_RADIUS, blob_color)
else:
# Transitioning / recognized: outer glow + inner silhouette hint
var glow_color := blob_color
glow_color.a *= 0.4
draw_circle(world_pos, BLOB_RADIUS * SILHOUETTE_SCALE, glow_color)
draw_circle(world_pos, BLOB_RADIUS, blob_color)
# Faint silhouette: body shape rectangle emerges late in recognition
if color_t > SILHOUETTE_APPEAR_THRESHOLD:
var sil_color := blob_color
sil_color.a *= 0.6
var sil_size := SILHOUETTE_SIZE * color_t
draw_rect(Rect2(world_pos - sil_size * 0.5, sil_size), sil_color)
# Draw sound pings — concentric expanding rings
for ping in _pings:
var pos: Vector2 = ping.pos * TILE_SIZE
var t: float = ping.elapsed / PING_DURATION
var fade: float = 1.0 - t # Linear fade out
for ring_i in range(PING_RING_COUNT):
# Stagger rings: each starts slightly later
var ring_t: float = t - ring_i * 0.15
if ring_t < 0.0 or ring_t > 1.0:
continue
var radius: float = ring_t * PING_MAX_RADIUS
var ring_fade: float = (1.0 - ring_t) * fade
var ring_color := COLOR_PING
ring_color.a = ring_fade * 0.7
draw_arc(pos, radius, 0.0, TAU, 32, ring_color, PING_RING_WIDTH)
func _random_drift() -> Vector2:
return Vector2(
randf_range(-DRIFT_RANGE, DRIFT_RANGE),
randf_range(-DRIFT_RANGE, DRIFT_RANGE)
)
@@ -0,0 +1 @@
uid://cw154tgjdyhe1
-16
View File
@@ -1,16 +0,0 @@
extends Node2D
# Fog renderer — manages fog of war overlay
# Controls visibility based on player position and fog radius
func _ready() -> void:
print("FogRenderer: Initialized")
# Update fog visibility (stub for now)
func update_fog(fog_data: Dictionary, player_pos: Vector2) -> void:
# TODO: Implement fog of war rendering
# This will control what the player can see based on:
# - fog_data.radius (visibility radius)
# - player_pos (center of visible area)
# - Perception mode state (affects visibility)
pass
+71
View File
@@ -0,0 +1,71 @@
extends Node2D
## Fog overlay controller — manages ColorRect + shader uniforms for D-059 fog.
## Reads textures from FogState autoload, positions rect to cover viewport.
## Architecture: docs/architecture/fog-shader-spec.md
var _fog_rect: ColorRect
var _shader_mat: ShaderMaterial
const TILE_SIZE := float(Constants.TILE_SIZE)
func _ready() -> void:
# Create the fog overlay ColorRect — transparent fallback so a shader failure
# doesn't paint solid white over the entire world (z:900 covers everything).
_fog_rect = ColorRect.new()
_fog_rect.name = "FogRect"
_fog_rect.color = Color.TRANSPARENT
add_child(_fog_rect)
# Load shader and create material
var shader := load("res://shaders/fog.gdshader") as Shader
if shader == null:
push_error("FogShader: failed to load res://shaders/fog.gdshader — fog disabled")
return
_shader_mat = ShaderMaterial.new()
_shader_mat.shader = shader
_fog_rect.material = _shader_mat
# Create seamless noise texture for fog animation
var noise := FastNoiseLite.new()
noise.noise_type = FastNoiseLite.TYPE_PERLIN
noise.frequency = 0.03
var noise_tex := NoiseTexture2D.new()
noise_tex.noise = noise
noise_tex.width = 256
noise_tex.height = 256
noise_tex.seamless = true
_shader_mat.set_shader_parameter("noise_tex", noise_tex)
_shader_mat.set_shader_parameter("tile_size", TILE_SIZE)
print("FogShader: Initialized (D-059 5-layer)")
func update_fog() -> void:
if _shader_mat == null:
return # Shader failed to load — fog disabled
# 1. Update FogState textures from GameState
FogState.update_from_state()
# 2. Position ColorRect to cover the current viewport.
# Use the camera's actual position (not player position) so the fog rect
# tracks the smoothed camera and never desynchronizes during smooth pan.
var vp_size := get_viewport().get_visible_rect().size
var cam := get_viewport().get_camera_2d()
var zoom := cam.zoom if cam else Vector2(2.0, 2.0)
var camera_pos := cam.global_position if cam else GameState.player_position * TILE_SIZE
var half_view := vp_size / (2.0 * zoom)
_fog_rect.position = camera_pos - half_view
_fog_rect.size = vp_size / zoom
# 3. Update shader uniforms
_shader_mat.set_shader_parameter("visibility_tex", FogState.visibility_texture)
_shader_mat.set_shader_parameter("exploration_tex", FogState.exploration_texture)
_shader_mat.set_shader_parameter("zone_tint_tex", FogState.zone_tint_texture)
_shader_mat.set_shader_parameter("rect_pos", _fog_rect.position)
_shader_mat.set_shader_parameter("rect_sz", _fog_rect.size)
_shader_mat.set_shader_parameter("map_offset", Vector2(FogState.map_bounds.position))
_shader_mat.set_shader_parameter("map_size", Vector2(FogState.map_bounds.size))
_shader_mat.set_shader_parameter("time", Time.get_ticks_msec() / 1000.0)
@@ -0,0 +1 @@
uid://dje4vilxh5xxu
+94
View File
@@ -0,0 +1,94 @@
class_name TileRenderer
extends TileMapLayer
# Tile renderer — draws map tiles from ObserverSnapshot tile data
# Uses a programmatic TileSet with placeholder colored rectangles (D-014)
#
# Tile types (atlas coords in the programmatic source):
# (0,0) = floor — dark gray
# (1,0) = wall — lighter gray
# (2,0) = door — brown
# (3,0) = object — teal
const TILE_SIZE: int = Constants.TILE_SIZE
enum TileType { FLOOR = 0, WALL = 1, DOOR = 2, OBJECT = 3 }
# Wire-format string to TileType mapping
const TILE_TYPE_MAP: Dictionary = {
"floor": TileType.FLOOR,
"wall": TileType.WALL,
"door": TileType.DOOR,
"object": TileType.OBJECT,
}
var _initialized: bool = false
func _ready() -> void:
_setup_tileset()
_initialized = true
print("TileRenderer: Initialized")
# Build a programmatic TileSet with colored placeholder tiles
func _setup_tileset() -> void:
var ts := TileSet.new()
ts.tile_size = Vector2i(TILE_SIZE, TILE_SIZE)
# Create an atlas source backed by a programmatic image
var source := TileSetAtlasSource.new()
var img := Image.create(TILE_SIZE * 4, TILE_SIZE, false, Image.FORMAT_RGBA8)
# Floor (0,0) — dark gray
_fill_tile(img, 0, Color(0.18, 0.18, 0.22))
# Wall (1,0) — lighter gray with subtle border
_fill_tile_with_border(img, 1, Color(0.4, 0.4, 0.45), Color(0.25, 0.25, 0.3))
# Door (2,0) — brown
_fill_tile_with_border(img, 2, Color(0.5, 0.35, 0.2), Color(0.35, 0.25, 0.15))
# Object (3,0) — teal
_fill_tile(img, 3, Color(0.2, 0.45, 0.45))
var tex := ImageTexture.create_from_image(img)
source.texture = tex
source.texture_region_size = Vector2i(TILE_SIZE, TILE_SIZE)
# Create tile entries in the atlas
for i in range(4):
source.create_tile(Vector2i(i, 0))
var source_id := ts.add_source(source)
tile_set = ts
# Update tiles from snapshot data
# tiles: Array of {x: int, y: int, z: int, type: String}
func update_tiles(tiles: Array) -> void:
if not _initialized:
return
clear()
for tile_data in tiles:
if not tile_data.has("x") or not tile_data.has("y") or not tile_data.has("type"):
continue
var tile_type_str: String = tile_data.type
if not TILE_TYPE_MAP.has(tile_type_str):
push_warning("TileRenderer: unknown tile type '%s' at (%d, %d)" % [
tile_type_str, tile_data.x, tile_data.y])
continue
var atlas_x: int = TILE_TYPE_MAP[tile_type_str]
var coords := Vector2i(tile_data.x, tile_data.y)
set_cell(coords, 0, Vector2i(atlas_x, 0))
# Fill a tile region with a solid color
func _fill_tile(img: Image, tile_index: int, color: Color) -> void:
var rect := Rect2i(tile_index * TILE_SIZE, 0, TILE_SIZE, TILE_SIZE)
img.fill_rect(rect, color)
# Fill a tile region with a color and a 1px border
func _fill_tile_with_border(img: Image, tile_index: int, fill: Color, border: Color) -> void:
var x_offset := tile_index * TILE_SIZE
# Border (full tile)
img.fill_rect(Rect2i(x_offset, 0, TILE_SIZE, TILE_SIZE), border)
# Fill (inset by 1px)
img.fill_rect(Rect2i(x_offset + 1, 1, TILE_SIZE - 2, TILE_SIZE - 2), fill)
@@ -0,0 +1 @@
uid://d2evnkstnqtpb
+31 -7
View File
@@ -2,19 +2,43 @@ extends Node2D
# World renderer — manages all visual representation from GameState
# Attached to the World node in main.tscn
#
# D-049 Z-level rendering pipeline (z-layer-gap-analysis.md):
# FogGroup (CanvasGroup) composites world content:
# FloorTiles (TileMapLayer) — z:0 ground plane
# FloorObjects (Node2D) — z:10 cosmetic detail (placeholder)
# YSortGroup (Node2D, y_sort) — z:100 furniture + entities (all z:0 relative)
# Furniture (Node2D, y_sort) — z:0 placed objects (placeholder)
# Entities (Node2D, y_sort) — z:0 entity sprites (D-033 colors)
# Overhead (Node2D) — z:300 ceiling/upper structure (placeholder)
# FogOverlay (Node2D) — z:900 fog shader (OUTSIDE FogGroup)
@onready var entity_renderer = $Entities
@onready var tile_renderer = $FogGroup/FloorTiles
@onready var fog_renderer = $FogOverlay
@onready var entity_renderer = $FogGroup/YSortGroup/Entities
var _last_tick: int = -1
func _ready() -> void:
print("WorldRenderer: Initialized")
print("WorldRenderer: Initialized (D-049 z-stack)")
# Called each frame to update visuals from game state
# Called each frame to update visuals from game state.
# Uses tick-based invalidation — re-renders all layers when a new snapshot arrives.
func update_from_state() -> void:
var tick := GameState.current_tick
if tick == _last_tick:
return
_last_tick = tick
# Update tiles (may include new chunks or modified tiles)
if tile_renderer and tile_renderer.has_method("update_tiles"):
if GameState.visible_tiles.size() > 0:
tile_renderer.update_tiles(GameState.visible_tiles)
# Update fog overlay — shader-based, reads from FogState autoload
if fog_renderer and fog_renderer.has_method("update_fog"):
fog_renderer.update_fog()
# Update entity sprites
if entity_renderer and entity_renderer.has_method("update_entities"):
entity_renderer.update_entities(GameState.visible_entities)
# Update fog overlay (fog data will come in D-020 expansion)
if fog_renderer and fog_renderer.has_method("update_fog"):
fog_renderer.update_fog({}, GameState.player_position)
+73
View File
@@ -0,0 +1,73 @@
shader_type canvas_item;
// D-059: 5-layer fog shader. Composites over world content (layers 0-4).
// Layer 1: Clear (vision cone) — transparent, soft gradient edge
// Layer 2: Light fog (peripheral) — desaturated + dim + animated noise, 8-10s cycle
// Layer 3: Deep fog (explored) — near-monochrome + zone tint + breathing, 15-20s cycle
// Layer 4: Unexplored + maps — wireframe (Sprint 6: deferred, treated as Layer 5)
// Layer 5: Unexplored, no maps — solid near-black #12141a
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;
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)
uniform vec2 map_offset; // map_bounds.position (tiles)
uniform vec2 map_size; // map_bounds.size (tiles)
uniform float tile_size; // Pixels per sim tile
uniform float time; // Seconds since start
// D-059 fog layer colors
const vec3 UNEXPLORED_COLOR = vec3(0.071, 0.078, 0.102); // #12141a
const vec3 DARK_OVERLAY = vec3(0.02, 0.02, 0.05);
// D-059 thresholds (after bilinear filtering)
// Forward tiles = 1.0, Peripheral = 0.706 (180/255), not-visible = 0.0
const float CLEAR_THRESHOLD = 0.85; // Above this: fully clear
const float PERIPHERAL_LOW = 0.55; // Below this: transition to deep/unexplored
void fragment() {
// Map UV (0-1 across ColorRect) to world pixels, then to tile coordinates
vec2 world_px = rect_pos + UV * rect_sz;
vec2 tile = world_px / tile_size;
// Map tile coordinate to texture UV
vec2 tex_uv = (tile - map_offset) / map_size;
// Outside known map → unexplored
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);
} else {
float vis = texture(visibility_tex, tex_uv).r;
float explored = texture(exploration_tex, tex_uv).r;
if (vis > PERIPHERAL_LOW) {
// In or near vision cone
if (vis > CLEAR_THRESHOLD) {
// Layer 1: Clear — soft edge gradient
float edge = smoothstep(CLEAR_THRESHOLD, 1.0, vis);
COLOR = vec4(0.0, 0.0, 0.0, 1.0 - edge);
} else {
// Layer 2: Light fog (peripheral + forward edge)
// D-059: animated Perlin noise, 8-10s cycle
float noise_val = texture(noise_tex, tile * 0.03 + vec2(time * 0.11, time * 0.07)).r;
float coverage = smoothstep(PERIPHERAL_LOW, CLEAR_THRESHOLD, vis);
// Blend from heavy fog (alpha ~0.55) to lighter fog near clear edge
float alpha = mix(0.55, 0.25, coverage) + noise_val * 0.1;
COLOR = vec4(DARK_OVERLAY, alpha);
}
} else if (explored > 0.3) {
// Layer 3: Deep fog (previously explored, no longer in LOS)
// D-059: near-monochrome, ~10% zone temperature tint, 15-20s breathing cycle
vec3 zone_tint = texture(zone_tint_tex, tex_uv).rgb;
float noise_val = texture(noise_tex, tile * 0.015 + vec2(time * 0.045, time * 0.03)).r;
vec3 tint_color = mix(vec3(0.04), zone_tint, 0.1);
float alpha = mix(0.78, 0.90, noise_val); // Fog breathes
COLOR = vec4(tint_color, alpha);
} else {
// Layer 5: Unexplored, no maps — information zero
COLOR = vec4(UNEXPLORED_COLOR, 1.0);
}
}
}
+1
View File
@@ -0,0 +1 @@
uid://bcc1wwpk3moh1
Binary file not shown.
@@ -0,0 +1 @@

@@ -0,0 +1 @@
̀
@@ -0,0 +1 @@
����
Binary file not shown.
@@ -0,0 +1 @@
��
Binary file not shown.
@@ -0,0 +1 @@
��
Binary file not shown.
@@ -0,0 +1 @@
�����
Binary file not shown.
@@ -0,0 +1 @@
���
Binary file not shown.
@@ -0,0 +1 @@
��������
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.

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