Compact MEMORY.md: drop CHANGELOG/DONE duplication
Full build / Import + headless tests (push) Successful in 44s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-03 19:40:50 +01:00
1 parent 215b843b67
commit 0aacee6518
1 file changed
+149 -755
+149 -755
View File
@@ -1,754 +1,156 @@
# MEMORY
Session memory for durable decisions, state, and gotchas. Keep concise, remove obsolete entries.
Session memory for durable decisions, gotchas, and commands. Implementation history lives in
`DONE.md`, user-facing changes in `CHANGELOG.md`, the live task list in `TODO.md`, and the
canonical implemented-features list in `docs/development-status.md`.
## Decisions
- **DuckDuckGo collection tool (2026-11-03):** `tools/refcollect.py` wraps the `duckduckgo_search`
package (installed locally; renamed `ddgs` upstream). All outbound requests share one
`TokenBucket` (`--rate` 0.5 req/s, `--burst` 3) and a polite retry routine: full-jitter
exponential backoff, `Retry-After` honoured, permanent HTTP failures fail fast, six attempts
maximum. Requests send a complete Firefox header set, not just the UA. DDG rate-limits by IP for
a while after probing, so the per-venue collections should still be run in small spaced batches.
Downloads are `local_only`/`unchecked-local` and never committed.
- **Comedy Store multi-model concepts (2026-11-03):** four scene pairs (facade, street, strip,
visitor) now live in `docs/screens/concepts/joke_shop/`, each anchored on a local-only
`tmp/research/comedy-store/` photo with one `google/gemini-2.5-flash-image` and one
`openai/gpt-5-image-mini` frame (`.refgen.json` records `unchecked`). Gotcha: DDG photo 07.jpg
was titled "Ramada hotel" but is actually an interior stage shot — Gemini refused it with a
text-only message; the hotel/parking ramp reference is still missing from the local set. The
map-recreation plan is tracked in `TODO.md` §3D model and local map re-evaluation → K. Comedy Store map recreation.
- **Jimmy Kimmel Live = starting point (2026-11-03):** the building is the game's starting
point: the Hollywood first shift and the late-night TV Studio arrival both begin at the El
Capitan Entertainment Centre housing the Kimmel stage/studio. Four photo-anchored entrance
concepts (day + night × gemini + gpt) are in `docs/screens/concepts/hollywood_boulevard/`,
and the fact is stated in `docs/hollywood-boulevard-map.md` and `docs/gameplay.md`.
- **Jimmy Kimmel Live research (2026-11-03):** three new local-only DDG sets —
`tmp/research/jimmy-kimmel-stage/` (15 frames, stage/audience), `tmp/research/jimmy-kimmel-entrance/`
(12 frames, the El Capitan entrance with the Kimmel marquee/signs) and
`tmp/research/hollywood-block/` (11 frames, the Masonic Temple/El Capitan block). The Kimmel
entrance frames confirm the starting-point building is the El Capitan Entertainment Centre with
"Jimmy Kimmel Live!" signage over the same six-column facade. Some DDG frames arrive as AVIF
mislabeled `.png`/`.jpg`; `refcollect.py sheet` now skips unreadable files and the AVIF frames
were converted with `ffmpeg` for the contact sheets.
- **Street-view limitation (2026-11-03):** true Google Street View panoramas are not fetchable
without a Google API key or a manual browser capture, so the local reference sets use DDG
street-level photos (facades, corners, opposite frontages) instead. If real Street View capture
is ever needed, it requires the owner to provide an API key or take screenshots.
- **Local-only reference override (2026-11-03, owner decision):** for development-only photo
research the licence gate does **not** apply. Downloads for this track live in `tmp/research/`
(gitignored) and may come from DuckDuckGo image search, street-view frames, neighbouring
properties and buildings across the street — never committed, never shipped, never used as a
committed asset. The moment a frame is promoted to `docs/screens/refs/<venue>/` it passes the
sidecar licence + privacy gate and `CREDITS.md`; the committed boards and the deferred Gaussian
splatting gate are unchanged. Tracked in `TODO.md` §3D model and local map re-evaluation → A.
- **Godot 4.7.2 `--import` gotcha (2026-11-03):** with a stale `.godot/` cache, `--import`
(and `--editor --quit-after N`) scans new image files but never imports them — no `.import`
files are created, and `preload()` of the new texture then fails to compile every dependent
script (the confusing `Could not resolve external class member "spec"` comes from
`game_manager.gd`, not the texture). A brand-new file still not importing after several runs
is the signature. Workaround that works: run the import in a scratch copy with `.godot/`
removed (`rsync -a --exclude .godot --exclude tmp --exclude .git ./ /tmp/proj-copy/` then
`godot --headless --import` there), and copy back the generated `.png.import` plus the
matching `.godot/imported/*.ctex/.md5`. CI is unaffected (it imports fresh and `.import`
files are committed).
- **Facade-realism pass complete (2026-11-03):** the masonic facade now uses the photo-anchored
`limestone_ashlar` field (see the ashlar decision below), the event banners carry a warm
self-light (`0.14` base, `0.28` night / `0.028` day via `_marquee_emissive`), and the marquee
sign face + bulbs are test-verified to brighten at night (`test_marquee_emissive_balanced_at_night`).
The marquee canopy/sign box stay `black_metal` by design — it is the fictional "GIG LINE"
marquee over the Masonic Temple, not the red El Capitan Theatre marquee next door (which is
represented by the blade-sign neighbour). Day and night captures promoted; both three-up boards
rebuilt.
- **Image-recitation filter (2026-11-03):** Gemini image models (`google/gemini-2.5-flash-image`)
reject a reference image with `finish_reason: content_filter` /
`native_finish_reason: IMAGE_RECITATION` when the photo shows a well-known branded facade (the
Comedy Store wall) — the same photo is fine as documentation evidence, just not as a generation
anchor. Workaround: anchor on a less-branded frame of the same block (the hillside photo worked),
or generate the texture text-only when the reference adds no geometric detail (the dark stucco
tile is text-only for this reason).
- **Ashlar limestone pick (2026-11-03):** the masonic facade field uses
`limestone_ashlar[_normal].png`, generated reference-anchored on the 2009 CC BY facade photo,
768 px tile ≈ 4.6 m of wall (~167 px/m, inside the 40–400 band). `stone_steps` stays for trim;
facade tint warm-shifted to `Color(0.85, 0.81, 0.74)`. Night emissive balance and
banner/canopy materials are still open in the facade-realism item.
- **OpenRouter reference-anchored generation (2026-11-03):** the `/v1/images` endpoint takes no
input image; the working image-to-image path is `POST /v1/chat/completions` with a
`content` array (`{"type": "image_url", "image_url": {"url": "data:...;base64,..."}}`).
`google/gemini-2.5-flash-image` honours reference geometry well (kept the El Capitan column
rhythm, motto band, banner slots and blade sign from a photo); response images arrive as
`message.images[].image_url.url` (data URL) — the parser also falls back to a data URI inside
the message text. Implemented in `generate_textures.generate_image(reference_path=...)` and
`scene_concepts.py generate --reference`; licence gate + `.refgen.json` provenance sidecar.
- **Recent-photo research, gitignored (2026-11-03):** Commons geosearch around the El Capitan block
found usable last-12-months imagery (Dolby billboards 2026-09-30 CC0 x6, Dolby street + fresh star
2026-07-17 CC0, El Capitan frontage with an impersonator 2026-02-25 CC BY-SA 4.0, Celia Cruz
marker and a star series 2025-09-28 CC0, Thunderbolts premiere 2025-04-28 CC BY-SA 2.0). Eight
candidates downloaded to `tmp/research/elcapitan-2026/` with `meta.json` — **never commit these**;
committed references still go through the sidecar licence gate. Finding: no recent photo frames
the El Capitan facade straight-on in daylight; the 2009 CC BY facade frame stays the geometry
reference. Geosearch (`generator=geosearch`, `ggscoord`, radius, `ggsnamespace=6`) is the reliable
"photos of this exact location" query; keyword search is not. Plan for reference-anchored
generation (`--reference`) is in TODO before any tool code is written.
- **TV Studio shares the Hollywood location (2026-11-03, owner clarification):** the Late Night TV
Studio venue is **the same** El Capitan Entertainment Centre block at 6840 Hollywood Blvd (the
building houses *Jimmy Kimmel Live!*), restaged for a late-night arrival with a bulb canopy, black
sedan and barrier line. `VENUE_SPECS["tv_studio"]` is a 30.0 m variant of the 29.6 m Hollywood
facade. One reference board (`docs/screens/refs/hollywood/`) serves both venues; do not create a
separate TV Studio photo board or treat it as a distinct real site.
- **Hollywood reference board (2026-11-03):** five Wikimedia photos live in
`docs/screens/refs/hollywood/` with `SIDECAR.md` (licences: CC BY 2.0 x2, CC BY 3.0, PD x2),
cropped/re-encoded to <=1600 px JPEG q85 with EXIF stripped, foreground crowd cropped out of the
facade shot. The board is linked from `docs/hollywood-boulevard-map.md` and the CC BY files are
credited in `CREDITS.md`. Gotchas: the Commons API needs a descriptive User-Agent and rate-limits
rapid thumbnail downloads (HTTP 429) - fetch all metadata in one `titles=A|B|C` query and space
the downloads. `check_docs` requires each image to be linked from its folder `README.md`, so each
venue folder needs both `README.md` (board) and `SIDECAR.md` (provenance). The board grew to nine
photos on 2026-11-03 (El Capitan Theatre neighbour CC BY 2.0, terrazzo + star close-up CC0, night
street context CC BY-SA 3.0). The CC BY-SA frame is redistributed unmodified for study only with
no derived asset, and that review is written into the sidecar; a licensed night photo of the hero
facade is still the top missing reference.
- **Joke Shop reference board removed (2026-11-03):** the committed Comedy Store photo board was removed in favour of the local-only `tmp/research/comedy-store/` research set (owner override: development reference, no licence gate, never committed). The three-up board's photo panel remains a historical public-domain frame; the block's current reference lives in the gitignored research folder.
- **Local-only reference override (2026-11-03, owner):** development-only photo research ignores
the licence gate. Downloads live in `tmp/research/` (gitignored): DDG image search, street-view
frames, neighbouring buildings — never committed, never shipped. Promotion into
`docs/screens/refs/` always passes the sidecar licence + privacy gate and `CREDITS.md`.
- **TV Studio shares the Hollywood location (owner):** the Late Night TV Studio is the same El
Capitan Entertainment Centre block at 6840 Hollywood Blvd (which houses *Jimmy Kimmel Live!*),
restaged for a late-night arrival. `VENUE_SPECS["tv_studio"]` is a 30.0 m variant of the 29.6 m
Hollywood facade. One reference board serves both venues; never create a separate TV Studio board.
- **Jimmy Kimmel Live = starting point:** the Hollywood first shift and the TV Studio arrival both
begin at the El Capitan block. Stated in `docs/hollywood-boulevard-map.md` and `docs/gameplay.md`.
- **Street-view limitation:** Google Street View panoramas need a Google API key or manual browser
capture, so the local reference sets use DDG street-level photos instead.
- **Image-recitation filter:** Gemini image models reject well-known branded facades (Comedy Store
wall) as generation anchors (`IMAGE_RECITATION`). Workaround: anchor on a less-branded frame of
the same block, or generate text-only when the reference adds no geometry.
- **OpenRouter image-to-image:** `/v1/images` is text-to-image only; the working path is
`POST /v1/chat/completions` with a `content` array carrying a base64 `image_url`. Response
images arrive as `message.images[].image_url.url` (fallback: a data URI inside the message text).
Implemented in `generate_textures.generate_image(reference_path=...)` and
`scene_concepts.py generate/anchor`; provenance sidecars are `.refgen.json`.
- **Facade/venue material picks (2026-11-03):** the Masonic/El Capitan facade uses the
photo-anchored `limestone_ashlar` field (warm tint `Color(0.85, 0.81, 0.74)`) with `stone_steps`
for trim; the Joke Shop uses `dark_stucco`; the marquee canopy/sign box stay `black_metal` by
design (fictional "GIG LINE" marquee, not the red El Capitan Theatre marquee next door). The
TV Studio reuses `masonic_facade`. Committed Comedy Store refs were removed in favour of the
local-only `tmp/research/comedy-store/` set; the three-up board's photo panel is a historical
public-domain frame.
- **Godot 4.7.2 `--import` gotcha:** with a stale `.godot/` cache, new image files never import
(no `.import` created) and `preload()` of the new texture fails every dependent script with the
misleading `Could not resolve external class member "spec"`. Fix: import in a scratch copy with
`.godot/` removed, then copy back the `.png.import` and `.godot/imported/*.ctex/.md5`.
- **Blender skills pack (local aid):** MIT `blender-skills` at `.agents/skills/blender-skills/`
(94 skills + shared `references/`), gitignored, kept out of Godot's scanner by
`.agents/.gdignore`. Keep the pack flat when updating (`cp -a <pack>/.claude/skills/. ...`).
Pi gates project `.agents/skills` behind project trust.
- **NPC model sourcing (ADR-0015):** CC0 acceptable for shipped assets; CC-BY only with
attribution in `CREDITS.md`; no service assets with redistribution restrictions. The procedural
generator is the primary path. Quaternius and MakeHuman were evaluated as references only, not
shipped bases (Quaternius: 62-bone T-pose rig mismatch; MakeHuman: needs retopology + custom
rig + clothing).
- **Gaussian splatting (unversioned):** offline/development-only until an ADR proves licence,
privacy, Compatibility/Web cost and mesh collision. Accepted photo channels: team captures,
permissioned user/volunteer photos, clearly licensed downloads; every set needs a
provenance/consent sidecar. No runtime splat dependency in 0.10.0.
- **0.10.0 balance protocol:** `docs/balance-acceptance.md` defines targets/denominators for the
36 venue/difficulty/phase cells, seeds 1–100, and access/emergency error rates. Gotcha:
`_read_npc_truth()` is type-derived, not behaviour-derived; the emergency-fairness review must
account for that oracle mismatch.
- **Character mesh cap:** eight library variants use 8,000 tris / 8,000 glTF vertices
(`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in `tools/blender_assets.py`); the frozen `npc_base` fallback
keeps 6,000/4,000. Measure after any geometry change; never silently lower the cap back to 5,000.
- **Engine baseline:** Godot 4.7.2 stable (official editor `~/.local/bin/godot`). The system's
`godot-runner` cannot load arbitrary project paths (compiled without path overrides); use the
official editor binary.
- **GDScript/architecture:** cross-file references use explicit `preload()` constants (not
`class_name`) so scripts compile in headless `--script` runs; the test runner is a custom minimal
harness (not GUT, which needs the editor plugin); no autoload signal bus — `ShiftCoordinator`
owns the `EventBus` instance passed to subsystems.
- **Skinned NPCs (v0.6.0):** `NpcAvatar._build_skinned_body()` loads `npc_base.glb`; procedural
`_build_body()` is the fallback. Bone pose rotation is the **absolute local orientation**, not an
offset — compose with `get_bone_rest(i).basis` or the body snaps to a broken pose. Five tint
slots via `SurfaceMaterials.npc_slot()`; keep `_rig.scale` uniform on the skinned path.
- **NPC facing:** the avatar's visual front is **+Z**; Godot `look_at()` aims **-Z** (made NPCs
walk backwards). Use `NpcAvatar.yaw_towards(dir)` and set `rotation.y`; never `look_at()` on
avatars. Head-turn offsets use `+basis.z` for the same reason.
- **Release blockers (still open):** macOS unsigned, AI image provider terms/commercial rights not
cleared, storefront/rating approval pending, external playtests and full hardware matrix pending.
Recorded in `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`, `docs/known-limitations.md`.
- **Blender Skills Pack installed locally (2026-11-03):** the MIT `blender-skills` pack
(v1.3.0, 94 skills + a shared `references/` folder) is installed at
`.agents/skills/blender-skills/` for Blender asset work. `.agents/` is gitignored and
`.agents/.gdignore` keeps Godot's scanner out, so it never ships, exports or affects tests.
Skills link shared docs as `../references/<file>.md`, so keep the pack flat when updating:
`cp -a <pack>/.claude/skills/. .agents/skills/blender-skills/`. Pi gates project
`.agents/skills` behind project trust, so the trust prompt must be accepted before the
skills load.
- **Priority order (2026-11-03):** `TODO.md` now leads with the unversioned **3D model and
local map re-evaluation** track, then **Character development**, then the versioned 0.10.0
release candidate and later releases. Local map workstreams are H–M inside the same 3D
track. Do not attach a placeholder
version to an unversioned track; assign one only when its scope enters a release. Character
development is generator-first (ADR-0015); the map work evaluates OSM/3D data, licensed
reference imagery, AI-assisted composition and development-only Gaussian splatting.
- **Commit/build discipline (2026-11-03):** commit each coherent verified change as it lands, not
only at milestone end, and push shareable commits. Every major, minor and patch version requires
a `CHANGELOG.md` entry, a local `tools/build_releases.sh` build and artifact/checksum
verification before tagging. CI tag builds are the second gate, not a substitute.
- **Gaussian splatting / photo-derived maps (unversioned local track):** expanded in
`TODO.md` "Gaussian splatting evaluation". Accepted photo channels are team captures,
permissioned user/volunteer photos and clearly licensed downloads; every set needs a
provenance/consent sidecar. Sparse web photos are reference/training supplements, not a splat
source. Five enhancement paths are ranked (reference/QA, texture bake, mesh proxy, distant
backdrop, single-facade user-photo supplement). Splats stay offline/development-only until the
ADR proves licence, privacy, Godot Compatibility/Web cost and mesh-based collision; no runtime
splat dependency in 0.10.0.
- **NPC model variety decision (2026-11-03, ADR-0015):** CC0 is acceptable for shipped
assets; CC-BY only with attribution in `CREDITS.md`; no service assets with redistribution
restrictions. The **procedural generator is the primary path**. Four crowd-variety specs were
prototyped with the existing builder (`npc_crowd_a` bun/slim/sunglasses, `npc_crowd_b`
bald/broad/beard/shoulder bag, `npc_crowd_c` coat/umbrella, `npc_crowd_d` hood/cup); all audit
clean at 4,550–5,258 tris / 4,356–5,086 verts. Prototype assets stay in
`tmp/npc-research/generator_proto/`; do not add them to the current release candidate.
**Quaternius is a reference, not a shipped base:** welding brings `Casual_Hoodie` from 12,518
to 3,199 verts and a 0.75 decimate to 2,423 verts / 4,654 tris, and its Idle/Walk clips read
well, but the T-pose 62-bone realistic rig/proportions do not match the arm-down 13-bone
contract. A contract rebuild is a no-go without a body proportion warp; a second
rig/animation path needs its own ADR. **MakeHuman is deferred** to a separate local pipeline:
the CC0 base is 19,158 verts / 36,972 tris and decimates to 3,780 / 6,468 at 0.35, but needs
retopology, a custom rig and project clothing. Research previews are committed under
`docs/blender_previews/sheet_npc_{sources_research,crowd_prototypes}.jpg`; the CC-BY entries
(CesiumMan/RiggedFigure, © Cesium) are attributed in `CREDITS.md` and are development-only.
- **0.10.0 balance protocol:** `docs/balance-acceptance.md` defines targets and
denominator/cohort rules for the 36 venue/difficulty/phase cells, seeds 1–100,
access/emergency error rates and matched calm vs emergency-first comparisons.
These are acceptance criteria, not measured results; sweeps/playtests remain open.
Inspection found `_read_npc_truth()` is type-derived, not behavior-derived;
the pending emergency fairness review must cover this oracle mismatch rather
than treating internal truth as independent evidence.
- The visual-alignment path/loader/panel/HUD work and the realism lighting pass are complete and
recorded in `DONE.md`; remaining visual items are the realism follow-ups in `TODO.md`.
- **Character mesh cap (2026-11-02):** the eight library variants now use **8,000 triangles /
8,000 glTF vertices** per character (`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in
`tools/blender_assets.py`), up from 6,000/5,000. The frozen `npc_base` fallback keeps its
6,000/4,000 contract. This headroom is specifically for the remaining face, hand-grip and
clothing realism work; re-run the 24-NPC and Web/Compatibility budgets as geometry lands. Do not
add detail without measuring, and do not silently lower the cap back to 5,000.
- `PLAN.md` is a compact product/design document, not an implementation log. Historical control
and v0.3.0 specs were folded into its Presentation and Controls section; put new step-by-step
work in `TODO.md`, not the plan.
- Editable Blender sources under `assets/models/blender_src/` are gitignored and untracked
(46 `.blend` files removed from the index on 2026-11-01); the `.gdignore` marker stays tracked.
- `screen_designs.py judge` default `google/gemini-2.5-flash-lite` worked on 2026-11-01
(7 match / 4 review / 0 diff); `--model` remains available.
- `capture_realism.gd` parses `--portrait-type=N`; it was documented but ignored before the gear
pass. Portrait captures need the requested NPC type to have spawned, otherwise the stage can
finish with an empty frame; use the Blender character previews (`render --only npc_coat`) for
deterministic gear checks.
- Headgear: the firefighter helmet was lifted 2 cm after the brow/face pass; cap and hood were
visually checked on `npc_vest`/`npc_hoodie`. The beanie is unused and shares the cap shell.
- Gear fit is measured with `python3 tools/blender_assets.py fit` (stdlib, reads the GLBs):
head width uses skin vertices weighted to Head in y 1.55–1.81; clothing radius is the cloth
skirt in y 0.30–0.52 with a pants hip fallback. The command reports per-character gear boxes,
head-band status and x/radial skirt clearance; `test_blender_assets` covers its shape.
- Post-realism work is split by theme, not by date: finished items are archived in `DONE.md`, and
`TODO.md` carries the 0.x gap register and milestone roadmap. New work belongs there, not in
a revived historical plan document.
- Graphics v0.6.0 plan (completed 2026-11-01; ADR-0013): NPCs move to Blender-authored skinned meshes. `tools/npc_pipeline.py` builds
`assets/models/generated/npc_base.glb`; `NpcAvatar` keeps the procedural `_build_body()`
fallback. Preserve `Rig/Head`, the limb node names, and the seed-deterministic
`outfit_for`/`layer_for`/`hand_item_for` helpers. Blender = flatpak 5.2.0 LTS, sandbox cwd
`/app/blender` → absolute paths for `--python`. CI stays offline (committed assets).
- Engine: Godot 4.7.2 stable (official editor build, `~/.local/bin/godot`). The system's preinstalled `godot-runner` cannot load projects from arbitrary paths because it was compiled without path overrides; use the official editor binary instead.
- Language: GDScript. All cross-file references use explicit `preload()` constants (not `class_name` globals) so scripts compile in headless `--script` runs without the editor's class cache.
- Test framework deviation: the project uses a custom minimal headless test runner (`src/test/test_runner.gd`) instead of GUT, because GUT needs the editor plugin. CI runs it with `--headless`.
- Eventing: no autoload signal bus. `ShiftCoordinator` owns an `EventBus` instance (`src/core/event_bus.gd`) passed to subsystems; subsystems emit only when a bus is assigned. Autoloads at parse time are unreliable under `--script`.
- Static signals are not available in Godot GDScript ("Expected func or var after static") — do not use them.
- GDScript lambdas capture local variables **by value**; mutating a captured local inside a lambda does not affect the outer variable. Use member functions for accumulators in tests.
- Shift length is the scenario's `shift_duration_s` (900 s in all three venues). Tests and the screenshot tool override elapsed time; they do not change the default.
- RNG: NPC reactions and event scheduling are seedable for deterministic tests.
- **Mouse look (v0.7.3):** guard look is handled in `GuardController._input()` (before GUI), not `_unhandled_input`; focus/click-back recaptures the cursor and the HUD is made mouse-transparent recursively. Regression tests cover yaw/pitch and HUD filters.
- **v0.9.0 production pass:** CI/release, local dev and project now use Godot 4.7.2; the version-adaptive light falloff remains for older editors. Audio settings are `master_volume`/`sfx_volume`/`ambience_volume`; accessibility has `reduce_motion` and `captions_enabled` (HUD log is the caption surface). Budget smoke is 24 NPCs (6.94 ms headless). Coat/dress stride is scaled to 0.76 to reduce hem clipping. Localization remains English-table based; pseudo-locale/VO/music are post-0.10.
- **Radio submit fix (v0.8.1):** `RadioPanel.action_button` now connects `pressed` for normal reports; `button_down/up` remains exclusive to emergency hold-to-confirm. Regression test covers both modes.
- **Campaign/v0.8.0:** `Campaign` owns Hollywood → Joke Shop → TV Studio progression. `campaign.json`, `shift_results.json`, `settings.json` and versioned `shift_state.json` persist separately; pause saves a checkpoint, review Continue advances, Settings resets with confirmation. Difficulty keys are `easy/normal/hard` (`ai_difficulty`); scripted QA bypasses tutorial unless `--tutorial` is passed. Help is searchable and generated from `InputBindings.REMAPPABLE`.
- **Tutorial (0.8.0 work):** `docs/tutorial.md` is the eight-beat design; `src/game/tutorial.gd` is the pure state machine, `TutorialOverlay` is the non-blocking card, and `GameManager` wires it with a safe visitor, suppressed spawns, persisted completion/skip and Settings replay. Scripted test/capture runs bypass it unless `--tutorial` is passed; `smoke_tutorial.gd` covers completion/replay/skip.
- **Pre-build NPC revisit (2026-11-02):** the final-context harness is
`src/test/capture_npc_review.gd` (8 library models, 3 distances, 2 venues,
day/night). It found two P1 gear-fit issues: the lanyard badge was hidden
under the jacket and the firefighter helmet brim sat over the eyes. Both are
fixed at runtime with `NpcAvatar.GEAR_OFFSETS` (`lanyard` forward 4 cm,
`firefighter_helmet` up 3 cm); the authored GLBs and the fit report remain
asset-space measurements. The guard model is staged through the review-only
`character_override` because it has no spawned NPC role. **Signed off
2026-11-02:** 96 final-context captures, no P0; lanyard/helmet P1 fixes; the
backpack straps, caps, low-poly hands/faces and optional close-range
flashlight wash were accepted or deferred. The 24-NPC budget is 6.94 ms, the
gallery is 13 good / 0 bad, the screen-design judge is 13/13 and a clean
portrait measured a 0.732 vision gap against the photoreal concept. No
release build, version bump or tag was created at this gate.
- **NPC texture resolution (0.9.5, 2026-11-02):** `tools/npc_pipeline.py` now generates 256²
albedo/normal/ORM maps instead of 128² (AI shirt/trousers are resized to 256). The base and all
eight library GLBs plus AO maps were rebuilt; texture files total ~0.5 MB, eight GLBs ~5.75 MB,
and the 24-NPC smoke is 6.94 ms.
- **0.9.5 texture/UI audit (2026-11-02):** all 21 screen-art textures are
1280×720 and referenced by their screens; access cards are 256×128 and HUD
icons 64×64, with no oversized or unused runtime texture. The 24-NPC smoke now
reports **168 meshes / 131,616 triangles / 6.94 ms** (50 ms budget). Compressed
payloads are picks **24.55 MB**, UI art **22.67 MB**, NPC maps **0.48 MB**.
Legacy root textures remain only for `generate_textures.py` and the asset test.
- **Environment surface audit (0.9.5, 2026-11-02):** `capture_realism.gd` gained
`surfaces` (Hollywood pavers/asphalt/limestone/facade) and `surfaces_joke`
(neighbour awning, canopy, pavers) stages with the HUD hidden. The Walk of Fame
band now uses `T_CONCRETE` as dark terrazzo; the neighbour awning uses a neutral
tint (`0.62`) and 4× UV so the red/cream stripes survive; the skyline shader
fades its lower 42 m toward a per-phase haze colour. Brick/asphalt picks needed
no replacement. No seams or broken UV scales found.
- **NPC fabric UV/AO (0.9.5, 2026-11-02):** cloth/pants use
`Materials.NPC_FABRIC_UV_SCALE = Vector3(4,4,1)`, and `NpcAvatar` skips the baked per-character
AO on those two slots. The 256² AO was magnifying into large blotches; the fabric now reads as a
fine weave. Skin/hair/gear keep baked AO. Test `test_fabric_uv_scale_avoids_the_baked_ao_pattern`
guards this. Current suite is 276/276.
- **NPC visual baseline (0.9.4, 2026-11-02):** `src/test/capture_npc_baseline.gd` is the
neutral-studio 8-variant × 6-view baseline (front, three-quarter, profile, back, full body,
crowd). It uses a fixed orbit camera, mid-grey studio backdrop, key/fill lights, no HUD/crowd/animation;
raw frames stay in `tmp/`, and `tools/npc_baseline.py sheet` builds the committed
`sheet_npc_baseline.jpg`. Criteria and measurements are in `docs/npc-visual-review.md`. The first
ranked gaps were: tourist photo prop duplicates the backpack silhouette; shallow/faceted face;
backpack straps read as rigid bars. The tourist prop now hides outside photo/film poses and the
backpack straps were refit as flat forward bands (GLB + AO rebuilt); the face remains P1. Before/
after sheet: `sheet_npc_baseline_compare.jpg`.
- **NPC proportion audit (0.9.4, 2026-11-02):** `tools/npc_proportions.py` measures the exported
GLBs (height, skin-head size, upper-chest shoulder silhouette, pants hip width, Spine-to-shoulder
torso, Spine-to-ground leg, shoulder-to-hand arm, shoe foot length). All eight variants pass the
stylized guide in `docs/npc-proportion-audit.md`; head ~0.190 (upper 0.22) and broad-build
shoulders ~0.370/hip ~0.225 are near the upper bounds, not outliers. No skeleton/mesh rescale was
needed; run the report after any body change.
- **NPC 0.9.4 visual passes (2026-11-02, consolidated):** the neck/shoulder, face-depth,
torso/hip, footwear, face-detail and hand passes all landed and are baked into
`tools/npc_pipeline.py` / `NpcAvatar`. The durable bits are the `NpcAvatar.HAND_ITEM_FORWARD =
Vector3(0,0,0.04)` offset (hand gear is authored around the wrist proxy) and the zero-area-triangle
cleanup after the neck branch (see the Blender-tooling note in Known Issues). Exact per-pass
values, review sheets and current per-variant counts live in `docs/3d-model-review.md` and the
code; the library cap is 8,000/8,000. Open: true finger curl/thumb contact and umbrella/sign grips.
- **UI flow smoke (2026-11-02):** `smoke_ui_flows.gd` boots the real scene and
exercises inspection allow/deny, normal vs emergency radio, Help filtering,
wand allow/deny and review return-to-menu. Keep this smoke in the 0.9.6
handoff; it complements the unit tests that only exercise panels.
- **Destructive actions (2026-11-02):** `SettingsPanel._open_destructive_dialog()`
owns one confirmation instance for reset/replay; repeated presses focus the
existing dialog, OK emits once, Cancel is inert. Emergency send remains the
RadioPanel hold path; `_holding` clears before submit so button-up cannot
double-send. Tests cover both.
- **Font/layout limits (2026-11-02):** HUD scale is clamped to 0.75–1.75 and only
applies to HUD labels. `Hud.set_target_label()` measures the scaled font, widens
the chip to 180–420 px and re-measures after a scale change; top labels clip.
`UiStyles.apply_button()` sets `clip_text` + ellipsis so long localized labels
cannot force a panel past the viewport. Do not replace these with Control.scale.
- **Modal close (2026-11-02):** `GameManager._close_top_modal()` is the single
Esc/right-click path for radial, inspection, wand, radio, timeline, Help,
Settings, Pause and review. Settings remembers whether it came from the menu and
restores it on apply/cancel; `_refresh_freeze()` recaptures the mouse and
`_show_start_menu()` keeps it visible. Do not add a new modal without adding it
to this close path.
- **UI focus (2026-11-02):** `src/game/ui/ui_focus.gd` links visible/enabled
controls into a wrapping ring; each modal sets default focus on open. Start menu
uses its own focus-target list so Continue, venue rows and footer actions share
one ring. `GuardController._ensure_gamepad_bindings()` adds A/B to
`ui_accept`/`ui_cancel` so pad-only users can confirm/cancel; hidden Continue and
disabled timeline buttons never enter the ring.
- **Button states (2026-11-02):** `src/game/ui/ui_styles.gd` owns the five action-button
states and font colours for the dark session/UI shell. Use `UiStyles.apply_button()`
for every new `Button`; CheckButton/OptionButton keep the engine theme. Timeline
Previous/Next disable at the ends; F1 tutorial skip is keyboard-only by design.
- **Version and roadmap renumber (2026-11-02):** source was **0.9.3** at the UI
polish milestone, **0.9.4** after the NPC pass, now **0.9.5** after the texture
and material pass, and **0.9.6** as the integration checkpoint. Next work is
the **0.10.0** minor release candidate. `TODO.md` plans only 0.x patch/minor increments; no
major-version scope is defined yet. The `v0.9.6` tag is the release trigger; no
tag was created for 0.9.3.
- **Priority change (2026-11-02):** the map optimization, pre-build NPC revisit,
0.9.3 UI polish, 0.9.4 NPC visual optimization, 0.9.5 texture/material pass and
0.9.6 integration are complete and in `DONE.md`. 0.10.0 release-candidate work
is next. The source version is 0.10.0-rc.1.
- **0.10.0-rc.1 build (2026-11-03):** clean worktree `a9658ec` produced five
artifacts (Linux 161.5 MB, Windows 197.2 MB, macOS 148.0 MB, Web 97.8 MB, HQ
241.6 MB); Web/HQ include player docs, SHA-256 manifests are local, Linux/HQ
launch headless and Web files serve HTTP 200. Not published: external playtest,
AI/legal approval, macOS signing and the Windows/macOS matrix are still open.
- **Release/legal blockers (0.10.0 work, 2026-11-03):** macOS remains unsigned
(quarantine instructions only); AI image provider terms/commercial rights and
external storefront/rating approval are not cleared; external playtests and the
full hardware matrix are pending. `CREDITS.md`, `PRIVACY.md`, `docs/accessibility.md`
and `docs/known-limitations.md` record these explicitly rather than claiming
approval. Do not tag 0.10.0 as final until these are accepted or cleared.
- **Save migration (0.10.0 work, 2026-11-03):** fixtures in `src/test/fixtures/saves/`
cover 0.8 settings/campaign/history and a 0.9.0 checkpoint. `_load_json` uses
`JSON.new().parse()` so corrupt player files fall back silently instead of
logging a parse error; atomic writes leave the old file intact when the temp
path cannot be written. The migration suite is 9 tests; total 276.
- **0.9.6 integration (2026-11-03):** no gameplay/content changes. A clean
detached worktree at `f5b8ef2` rebuilt Linux/Windows/macOS/Web/HQ without the
dirty tree's ignored `tmp/`/`build/` payload; the in-repo build was 1.6–3.1 GB
per artifact because those folders were packed, so always build release
artifacts from a clean checkout/worktree. Verification: 263/263 tests,
101/101 Python, 14 smokes, 24-NPC 6.90 ms / 168 meshes / 131,616 triangles,
gallery 21/0, screen judge 21/21, realism judge overall 2.1 / gap 0.291.
- **Release v0.9.4/v0.9.5/v0.9.6 (2026-11-02/03):** tags `v0.9.4` (`a57b86c`),
`v0.9.5` (`4bc6390`) and `v0.9.6` (`4bb2896`, the 0.10.0 integration checkpoint) each
published seven assets (Linux, Windows, macOS, NSIS setup, AppImage, Web, Desktop HQ) with
full CI runs; release pages live under `releases/tag/`. Never reuse a tag — future fixes
need a new patch tag. Full per-version records stay in `CHANGELOG.md`.
- **Hollywood landmark map (2026-11-02):** the default venue is the real El Capitan
Entertainment Centre block at 6840 Hollywood Blvd, not a generic brick entrance. Its
reference/mapping notes live in `docs/hollywood-boulevard-map.md`; the map-budget test
(`test_hollywood_geometry_stays_inside_the_map_budget`) currently measures 58,884 triangles
across 409 meshes. The facade uses the shared smooth limestone (`stone_steps`) texture; the
noisier granite pick was removed.
- **Light falloff gotcha (2026-11-02):** in Godot 4.7 `omni_attenuation = 0.0` is constant
falloff, not physical falloff. It made the night facade a flat yellow wall and the street
lamps wash the set. All local lights now use `1.0`; the Hollywood facade was retuned with
0.50/0.28 washes, 1.3 sconces and 2.2 street lamps; the Joke Shop uses 1.8 sconces,
2.8 canopy downlights, 3.0 entrance, 1.35 ground uplights and 2.2 sign. Flashlight spot
attenuation is also `1.0`. Do not reintroduce `0.0` as a "modern" branch.
- **Joke Shop map (2026-11-02):** the venue is now a 27 m Sunset Strip club based on The
Comedy Store at 8433 Sunset Blvd, not the old brick alley. `docs/joke-shop-map.md` carries
the OSM/Commons references and simplifications; `docs/screens/concepts/joke_shop/` holds the
new day/night/across-street frames. The map-budget test measures 22,680 triangles / 454
meshes. The hill behind the club is a custom hand-built ramp with an unshaded, fog-disabled
shader: a rotated `PlaneMesh` was edge-on and scene fog erased it from the street camera.
Local light names are `JokeSign`, `JokeCanopy_*`, `JokeEntrance`, `JokeSconce_*`,
`JokeGround_*`; `set_night_mode()` owns their energies and the hill tint.
The latest local realism judge reports a 0.454 reference gap on the night entrance;
the concept is photoreal while the map keeps stylized materials, so texture/foliage
detail stays with the 0.9.5 texture pass rather than the map milestone.
- **Docs validation (2026-11-02):** `python3 tools/check_docs.py` requires a README
only in directories containing tracked files, verifies local links and requires
every tracked image in a folder to be linked from its README. Gitignored
generated files (notably `docs/screens/realism/`) are exempt; the tool also has
unit tests in `tools/test_check_docs.py`.
- **Documentation ownership (2026-11-01):** `docs/architecture.md` §12 defines the source-of-truth map and consolidation rules. Obsolete/historical plan documents are removed rather than kept; current work belongs in `TODO.md`. README/development-status/Blender AO/Forward+ stale claims were corrected. The 0.x gap register and roadmap live in `TODO.md`.
- **Vision judge (v0.7.2):** `tools/openrouter_judge.py` reads `/api/v1/models`, filters image-input models with `response_format`, and ranks by advertised prompt/image/completion prices. `--model auto` (default) uses the cheapest (`z-ai/glm-5.3-flash` on 2026-11-01) with fallbacks; explicit `--model`/`GIG_LINE_JUDGE_MODEL` wins. Auth errors abort, provider/model errors fall through. Full screen judge 13/13 match, 0 diff; A/B judge ~$0.006.
- **Desktop HQ preset (v0.7.1):** `packaging/desktop_hq/override.cfg` switches a
copied executable to Forward+; `Materials.forward_plus_active()` gates
volumetric fog/SDFGI and `light_volumetric_fog_energy`, and
`Materials.modern_light_falloff()` compensates for Godot 4.7's much harsher
local-light falloff. LATE volumetric density is 0.020 with flashlight
volumetric energy 3.0. `tools/build_releases.sh` packages the Linux/Windows
binaries + override into `gig-line-desktop-hq-<version>.zip`;
`publish_release.sh` uploads it when present. A/B report:
`docs/screens/realism/2026-11-01-forward-plus/` (entrance gap 1.568→1.187).
- **Godot toolchain (updated 2026-11-01):** local `~/.local/bin/godot`, CI and
`project.godot` now use Godot 4.7.2; 4.3 remains only as a local backup binary.
Export templates for 4.7.2 are installed under
`~/.local/share/godot/export_templates/4.7.2.stable`. Godot 4.7 writes
`*.gd.uid` files; they are kept out of this clone's status via
`.git/info/exclude`. CI downloads the binary/templates directly; Gitea
Actions cache is currently unreliable (see CI cache evaluation below).
- **Blender invocation (v0.6.0):** `flatpak run org.blender.Blender --background --python
/run/media/system/DATA/Documents/06-games/security-game/tools/npc_pipeline.py -- build`.
The flatpak cannot read `/tmp`; use absolute repo paths. `assets/models/npc/.gdignore` is
required, otherwise Godot's editor tries to import the committed `.blend` and hangs during
`--import` (also add `.gdignore` to local build/squashfs folders when scanning is slow).
- **Skinned NPC integration (v0.6.0):** `NpcAvatar._build_skinned_body()` loads
`npc_base.glb` and keeps `_build_body()` as fallback (`use_skinned_model=false` in tests).
Bone pose rotation is the **absolute local orientation**, not an offset from rest: compose with
`get_bone_rest(i).basis` or the whole body snaps to a broken pose. Proxies named Hip/Knee/
Shoulder/Elbow/Hand + Head marker preserve the old animation math; `Rig/Head` is the marker.
Five material slots are tinted via `SurfaceMaterials.npc_slot()`; keep `_rig.scale` uniform on
the skinned path. Budget: 16 avatars = **144 meshes**, ~6.9 ms headless.
- **NPC facing (fixed after v0.5.0):** the avatar's visual front is **+Z** (procedural eyes
and the Blender model's face). Godot `look_at()` aims **-Z**, which made every NPC walk
backwards. Use `NpcAvatar.yaw_towards(dir)` and set `rotation.y`; do not call `look_at()` on
avatars. Head-turn offsets use `+basis.z` for the same reason. Covered by
`test_yaw_faces_the_movement_direction`.
- **Image root (ADR-0014, 2026-11-01):** `assets/images/` is the runtime root for composed
interface/scene art (`ui/`, `scenes/`); tiled PBR textures stay under `assets/textures/`;
`docs/screens/designs/` and `docs/screens/concepts/` stay reference-only; QA captures stay in
`docs/screenshots/`. When a mockup becomes runtime art, copy/process it into `assets/images/`
with a new name (no overwrites) and keep a fallback in code.
**Mockup copies:** 21 design references live in `assets/images/ui/`; `docs/screens/designs/`
remains the reference-only copy. `tools/screen_designs.py` reads/writes the runtime root.
## UI / Presentation Decisions
- Runtime UI/scene art is loaded through `src/game/ui/art.gd` (`assets/images/ui/` then `scenes/`); missing files return null so panels keep procedural fallbacks.
- Screen mockups with baked-in controls are used as backdrops plus an opaque `ArtPanel` cover, not as clickable full-screen skins.
- Radio, wand, inspection, and body-camera timeline panels use their reference art as backdrops; forms sit on transparent, padded panels over the covered mockup controls.
- HUD target names use a boxed chip under the crosshair; escalation has a five-segment bar below the text label. The body-camera REC/clock block starts at y=92 on the right so it clears the shift/rep/crowd text.
- Staged captures in `docs/screenshots/` are committed and were regenerated on 2026-11-03 with the full `tools/check_screenshots.sh` run (21 good / 0 bad, 13 PNGs changed). Re-run the whole script, never a partial capture, before replacing them. The README, `docs/README.md` quick preview, `docs/screenshots/README.md` and `docs/blender_previews/README.md` now carry captioned galleries; the previews README is curated by hand (all tracked sheets linked, local per-asset PNGs described as untracked).
- `tools/check_screenshots.sh` is the repeatable capture+compare smoke; both capture scripts accept `--out=res://docs/screenshots`. Judge verdicts on the staged set were 7 match / 4 review / 0 diff.
- Venue selection used to leave `guard.input_locked` true and the mouse visible; `_start_shift()` must call `_refresh_freeze()` after starting the clock.
## Current State
- **v0.3.0 pass (2026-10-31, historical; shipped):** rendering, generated prop models, screen
catalogue, aim-free selection. See ADR-0011 (selection) and ADR-0012 (materials/props).
- `src/game/materials.gd` (`SurfaceMaterials`) is now the single source of surface
recipes + `configure_environment()`. `GameManager.PHASE_LIGHTING` gained
`exposure/glow/fog_color/fog_density/fog_energy/contrast/saturation/ambient_energy`.
Renderer is **GL Compatibility**, so SSAO/SSIL are unavailable; glow+fog+ACES are used
instead and unsupported settings are ignored rather than gated in code.
- World: scaled brick (uv 22,10), framed windows with per-window night emission
(`i % 3 != 2` lit), stone base course, door trim/handles, curb, lane dashes (the old
loop had `Vector3(i * 0.0, …)` and stacked every dash at x=0), street lamps use the
metal profile. The two flat planters that clipped the steps were removed.
- `tools/model_pipeline.py` builds `assets/models/generated/*.glb` (bollard, bench,
trash_bin, planter_box, newspaper_box, fire_hydrant) from box/cylinder specs;
`WorldBuilder._add_prop_model()` loads them with a procedural fallback and records
`loaded_models` / `fallback_props`. **Gotcha:** glTF `baseColorFactor` is LINEAR —
writing sRGB made every prop pastel; `srgb_to_linear()` is applied in `build_glb`.
**Gotcha:** a new/changed `.glb` needs `godot --headless --path . --import` before
`ResourceLoader.exists()` / `load()` sees it (test_generated_props fails otherwise).
- `model_pipeline.py concept` (OpenRouter, reads repo `.env`) writes per-prop concept
sheets to `assets/models/concepts/`; `dominant_colors()` centre-crops and weights by
saturation (a grey studio backdrop otherwise wins), `assign_roles()` maps colours by
hue/value. Delete a concept file to fall back to the catalogue palette.
- NPC avatars: `outfit_for(type, seed)` (pure) gives shirt/trousers/hair/skin +
accessory (cap, hood, backpack, sunglasses, lanyard) and height scale; `_arms` and
`_legs` now swing (`limb_swing`) while walking. Keep `Rig/Head` named for focus.
- Selection: `NpcFocus.rank_indices` (angle then distance) — `pick_index` = first.
`GuardController.cycle_target(direction)` walks nearest-first `nearby_candidates()` and
sets `target_pinned`, which makes `_update_target()` skip the cone. `clear_target()`
sets `focus_suppress_s = 0.75` so the cone does not instantly re-acquire the person.
New input actions `walk_slow` (Shift), `cycle_target_next` / `cycle_target_prev`
(wheel down / up). Right click with nothing open clears focus (Esc still pauses).
- HUD: `nearby_label` + `set_nearby_hint(count)` show `hud.nearby`; `hud.controls` and
`hud.targeting` mention the wheel and Shift.
- `tools/screen_designs.py`: catalogue now 19 screens (added wand_screening,
joke_shop_queue, studio_arrival, crowd_panic, responder_arrival,
body_camera_timeline, escalation_hud), new `list` command, `generate --only/--force`
(existing designs are kept by default so a draft run cannot clobber AI mockups),
and `judge --dry-run` no longer raises `KeyError: 'class'`.
- Screenshot tool leak: `capture_screenshots.gd` intermittently prints
`ObjectDB instances leaked at exit`. Verified against a `git worktree` of HEAD~ — the
baseline leaks more often (3/3 vs 1/3), so it is a pre-existing quit-timing artifact,
not a regression. Normal boots are clean.
- Access artifacts: `src/core/access_loadout.gd` builds from event `override_access` + venue policy.
Spawn signal is now `(profile, intent, npc_id, event)`. Recognition is not auto-attached to every
guest. `AccessVerifier.verify(..., already_screened=false)` so Joke Shop scoring uses
`controller.screened`. Joke Shop/Studio gained `sidewalk`/`steps_end` waypoints. Wand venues roll
`carried_items` per spawn (seeded). Tests in `src/test/tests/test_access_loadout.gd`.
- Wand shortcut is **`C`**, not `W` (`W` is walk). `radio_emergency` input map no longer includes W.
`hud.controls` / `hud.screen_first` updated.
- Control pass (historical; smoke-tested): cone focus via `src/game/npc_focus.gd`,
accel/decel, gravity only airborne, `input_locked` freeze, Esc no longer on the guard.
`NpcAvatar` in group `npc`; neck/hair/eyes/shoes; blue focus circle + `Label3D` name; `set_focus_visual`.
`src/game/ui/action_radial.gd` doughnut (LMB/Space open, A/D/arrows, Space/click confirm, Esc/RMB
cancel). Crosshair CanvasLayer. Emergency still `R` only. Number keys 1–6 when the wheel is closed.
- Named input actions now exist and are used: `action_menu`, `confirm`, `cancel` (Esc + RMB),
`inspect` (E), `wand` (C). The old unused `interact` action was removed.
- Runtime fixes from the smoke: `Head` is +1.6 above the guard root (root sits on the floor); guard
spawns in front of the door facing the street; building volume moved to z ∈ [-8,-4] so the door,
awning, and marquee are visible; step treads rise toward the door; `ShiftCoordinator` pulls the
event scheduler once per simulated second (was every physics frame → >1200 NPCs in seconds);
`GameManager.MAX_LIVE_NPCS = 16` caps live avatars.
- **Session shell (2026-10-31, historical):** the game boots into `StartMenu`
(`src/game/ui/start_menu.gd`, layer 20) instead of a live shift. `GameManager._on_venue_selected()`
builds the scenario and calls `_start_shift()`; `_shift_running` gates input and `_refresh_freeze()`.
Esc with nothing open now pauses (`PauseOverlay`, layer 18) → resume / restart / settings /
quit-to-menu. `SettingsPanel` (layer 22) writes `look_sensitivity` + `hud_font_scale` through
`Save.save_settings()` → `user://settings.json`; `Save.load_settings()` merges over
`Save.DEFAULT_SETTINGS` on boot and `GameManager._apply_settings()` pushes them into the guard/HUD.
Shift review "Run another shift" now calls `_show_start_menu()` (not a silent restart). `V` stays a
debug venue bind. Smoke: `src/test/smoke_session_shell.gd` (menu → shift → pause → settings → menu).
- **World follow-ups closed (2026-10-31):** skyline backdrop restored with `T_SKYLINE`
(`assets/textures/skyline.png`, regenerated RGBA with a transparent sky) and an
`TRANSPARENCY_ALPHA_SCISSOR` unshaded material (`_skyline_mat`); steps + landing are a
`StaticBody3D` with per-tread `BoxShape3D` colliders; marquee `font_size` 160 / outline 24; focus
circle (blue disc in front of the face) + name tag 48px; `NpcAvatar.ephemeral`/`lifetime_s` despawns routine traffic
(authored events stay). Guard spawn height verified on the landing (guard_y=0.84).
- **PLAN verbs (2026-10-31, historical):** the radial has 12 slices
(`Stop, Look, Point, No, Ask, Inspect, Wand, Warn, Block, Reason, Recognize, Radio`).
`eye_contact` reuses the Level 1 reaction table via `_apply_interaction`. Recognition is a player
decision: `NPCController.player_recognized` + `IncidentRecorder.add_access_evidence`, and
`GameManager._artifacts_for()` prepends a `RECOGNITION` artifact only after the player declares
it. The body camera is a real flag (`GameManager.body_camera_on`, `Hud.set_body_camera`), off by
default, `B` toggles it, and a formal warning arms it and writes a `body_camera` incident event.
Blocking also calls `_station_between()` to physically place the guard 0.9 m from the NPC toward
the entrance. `_request_reason()` uses the authored `reason` context (falling back to seeded `dialogue_lines`) and stores the pick for the timeline.
- **NPC navigation was dead (fixed 2026-10-31):** avatars never moved in the live game, for three
stacked reasons. (1) `agent.target_position` was re-assigned every physics frame, which
invalidates the path so `get_next_path_position()` keeps returning the current point — only set
it when the target changes. (2) Nav path points sit on the navmesh plane (y=0) while bodies stand
at y≈0.5, so a 3D "are we there" test always looked 0.5 m away and the horizontal direction was
zero — compare horizontally. (3) `path[0]` is the old start point, so picking any point "far
enough away" can pick the point behind you and oscillate — pick the first point that is
**closer to the goal** than the current position. `_nav_try_frames`/`_move_direct` fallback is
still there.
- **Crowd reactions listen on the EventBus:** `CrowdSystem.apply_event()` emits
`event_bus.crowd_changed`; its own `changed` signal is never emitted. `GameManager._connect_crowd()`
therefore connects to `coordinator.event_bus.crowd_changed` (reconnect after every
`coordinator.setup()`, which recreates both crowd and event bus).
- **Crowd presentation:** `NpcAvatar.start_rubberneck(at)` (idle bystanders away from the door stop
and face the incident for 12 s) and `start_flee(from)` (panic: walk away at 2.4 m/s, then free
themselves). Someone `awaiting_interaction` within 3 m of the door is "engaged" and does not
rubberneck. Use `position` (not `global_position`) for that distance check so it also works in
headless tests outside the tree.
- **Perf:** a busy 16-NPC entrance measured ~1.2 ms/frame (824 fps with vsync off, 800 samples),
so LOD/culling is not warranted at `MAX_LIVE_NPCS = 16`. Re-measure before raising the cap.
- **Incident recording was dead:** `IncidentRecorder.start_incident()` was never called, so every
`add_event`/`add_access_evidence` silently no-opped and the review always showed 0 incidents.
`_begin_incident()` now starts one per focused NPC (`has_open_incident()` guard), verbs call
`_ensure_incident()`, and `ShiftCoordinator._finish_shift()` closes leftovers as `unresolved`
before building the review.
- **Editor-ready venues (2026-10-31):** `tools/export_venues.gd` (a SceneTree script) saves the
three factory scenarios to `content/venues/<id>.tres` with `ResourceSaver`; the `.tres` files
round-trip through `ResourceLoader` (events, profiles, policy, shift phases, door lines,
locations). `test_venue_resources.gd` also fails if the exported files are missing or stale
relative to the factory, so re-run the export after changing `ScenarioFactory`.
- **Vision judge (2026-10-31):** `python3 tools/screen_designs.py judge` sends the capture plus
its concept mockup to a vision model (`DEFAULT_JUDGE_MODEL = google/gemini-2.5-flash-lite`,
override with `--model`; `openai/gpt-4o-mini` also works). Reads the repo `.env` via
`_load_env_file()` (never overrides real env). Verdicts are classed `match` / `review`
(mismatch below `--fail-confidence`, default 0.6) / `diff` (confident mismatch → exit 1) /
`unknown`. `--dry-run`, `--json`, `--only` supported. The prompt explicitly ignores art style,
because the references are stylised mockups; the live run gave 3 match, 3 review, 0 diff.
Python tests: `cd tools && python3 -m unittest test_asset_pipeline test_screen_designs`.
- **String newlines (2026-11-02):** `content/strings.en.json` needs one escaped
newline (`\n`), not two. Two backslashes reached the HUD as literal `\n` in
the shift status and wand panel; `test_strings_format` now guards this.
- **String formatting safety (2026-10-31):** `Strings.placeholder_count()` counts `%` specs
(ignoring `%%`), `fmt()` warns and returns the template when the arg count does not match
(instead of raising an engine error), and `validate()` lints the table for trailing `%`,
unsupported conversions, and mixed positional/sequential placeholders. Tests cover all three
plus a formatting pass over every placeholder key with type-correct dummy args.
- **Authored scenario content (2026-10-31):** events can now carry `hint` (a string key logged
when the avatar spawns), `extra_spawns` (celebrity arrivals pull in tourists/influencers via
`_spawn_npc` with `kind: routine`), and `refuses_screening` (copied to
`NPCController.refuses_screening`; `_toggle_wand_screening` logs `wand.refused`, records a
`wand_refused` event, applies a small prop hit, and does not open the panel). `film_boundary`
is a new intent that maps to `steps_end` so the influencer stands past the posted line.
- **Accessibility (2026-10-31):** escalation text is `"<name> [###--] L3/5"` via
`Hud.escalation_meter(level)` (pure, tested) so the cue survives grayscale. HUD font scale uses
`add_theme_font_size_override` on the core labels (`set_font_scale`), **not** `Control.scale`,
which pushed the right-anchored clock label off-screen. Settings load before `_build_ui()`, so
`_ready()` re-applies player settings after the HUD exists.
- **Audio (2026-10-31):** `AudioFeedback.start_crowd_bed()` builds a 4 s looping `AudioStreamWAV`
(one-pole-filtered noise + 68/103 Hz drone, seeded) and `set_crowd_level(mood)` maps 0..1 to
-34..-12 dB. `play_radio_open/close` push short filtered-noise bursts. All procedural; with a
dummy (headless) audio server the calls are no-ops. `_free_player()` returns null when `_ready`
has not run (tests), and callers handle null.
- **NPC theater (2026-10-31):** `NpcAvatar.theater_pose()` maps `intent` + `arrived` to
idle/sit/photo/film/perform. Pure helpers `crouch_for_pose`, `prop_lift_for_pose`,
`sway_for_pose` keep the math testable. Sit lowers the rig by 0.34 and swings the legs forward
(negative X pitch; positive looked like kneeling). `_photo_prop` is set for influencer/tourist
cameras, `_sign_prop` for protester/performer boards; their base Y values are captured at build
time so the animation offsets from rest.
- **Body-camera timeline (2026-10-31):** `src/game/ui/event_timeline.gd` flattens
`review.incidents.incidents[].events` into one time-ordered list (`flatten`), formats each row
with `describe`, and steps with Left/Right or the buttons. `ShiftCoordinator.started_at_ms`
gives the review an absolute start for mm:ss stamps. `GameManager._open_timeline` hides the
review dim (two stacked overlays made the screen nearly black) and `_timeline_from_review`
restores it on close. Smoke: `src/test/smoke_timeline.gd`.
- **Radio dispatch was dead after the first shift:** `coordinator.setup()` builds a new
`RadioSystem`, so the `responder_arrived` connection made in `_ready` was lost on every venue
switch/restart (responders never resolved anything). `_connect_radio()` now re-wires it next to
`_connect_crowd()` after every setup. Same class of bug as the crowd signal.
- **Emergency aftermath:** `ShiftCoordinator.responder_outcomes` / `investigations` feed the review.
`evacuation` (emergency call, low danger, no weapon) locks the door, makes every NPC flee, marks
them `leave_requested`, and applies a `security_failure` hit. An emergency placed while
`current_npc_truth` has no weapon and `danger_level <= 1` appends an investigation note, records
`investigation_opened`, and applies `false_report` -6.
- **Skyline backdrop (2026-10-31):** `PlaneMesh` defaults to a **horizontal** XZ plane — set
`mesh.orientation = PlaneMesh.FACE_Z` or the "backdrop" renders as a band overhead. The skyline
is now a ring of four 400x60 planes at 200 m (center y=30, so building bases meet the ground),
`TEXTURE_FILTER` default, `TRANSPARENCY_ALPHA_SCISSOR` with threshold **0.5** (0.3 left faint
mip dashes across the sky), unshaded with a lifted albedo (`Color(2.6,2.7,3.0)`) so it is not
pure black. Ground is 500x500 so no sky gap shows under the buildings. The `skyline.png` import
needs `mipmaps/generate=true`.
- **Asset gotcha:** `assets/textures/game_ready/` is *not* imported (no `.import`). Use the
imported `assets/textures/skyline.png` path. Untracked duplicate folder should not be referenced.
- Host sign-in follow-up is **done**: `REQUIRE_SIGNIN_VIEW = false` in `/etc/gitea/app.ini`
(backup `app.ini.bak-*` alongside), Gitea restarted; anonymous repo page/API return 200 and
release assets download in full. Reverse by setting it back to `true` and restarting.
- **Release publishing (fixed 2026-10-31):** `publish_release.sh` falls back to the local git
extraheader that `actions/checkout` writes (`git config --local --get-regexp
'^http\..*\.extraheader$'`) because this Gitea/act runner provides no `GITEA_TOKEN`. Always
build release artifacts from a clean checkout/worktree (the dirty tree's ignored `tmp/`/`build/`
payload blew artifact size to 1.6–3.1 GB once). Per-version records live in `CHANGELOG.md`.
- NSIS gotchas (validated in a local ubuntu:24.04 docker repro): makensis uses `-D` defines
(never `/D`), resolves relative `-DICON`/`-DARTIFACT` against the **script directory** so CI
passes `$PWD`-absolute paths; installed exe must be space-free (`gig-line.exe`); ICO must be
classic BMP entries (ImageMagick `icon:auto-resize=48,32,16`; PIL emits PNG-compressed ICOs NSIS
cannot load).
- appimagetool runs system `appstreamcli` if present (strict, exit 3 on warnings) — project
metadata now passes `--pedantic`.
- `~/.git-credentials` holds the Gitea creds (basic-auth usable); `.env` only holds
`OPENROUTER_API_KEY`.
- NPC avatars now walk to per-intent waypoints (`VenueScenario.waypoint_for_intent`): enter/trespass/deliver/arrive → entrance, photo/film/perform/wander/roam → sidewalk, sit → steps_end. `NpcAvatar.setup` takes an explicit `door_position`.
- Returning-trespasser behavior: `NpcProfile.return_probability` (trespasser 0.6, mental-crisis 0.4, others 0). `NPCController._maybe_schedule_return()` moves a directed-away NPC to `LOITERING_DISTANT`; `tick_return(delta)` fires after 20-60s and transitions back to `APPROACH`. `NpcAvatar._advance_fsm` re-targets the door on return.
- Perception pass: `NPCController.perceive_guard(distance)` returns `{noticed, band, reaction}` with bands `unaware` (>6m), `aware` (2-6m), `near` (<2m). Near + temperament ≥0.6 → ARGUE, else COMPLY. `GameManager._perception_pass()` feeds real guard distance each physics tick and logs first-time notice.
- Responder dispatch: `RadioSystem` tracks `pending_calls` and decrements `arrival_delay_s` in `tick`; emits `responder_arrived` when a call's delay elapses. `RadioSystem.responder_outcome(call)` maps danger/weapons → command/restraint/evacuation/arrest. `GameManager._on_responder_arrived` spawns a `RESPONDER` avatar at the entrance and logs the outcome. HUD shows an emergency-radio cooldown label via `_update_radio_cooldown`.
- Wand screening (Joke Shop): `WandScreener` classifies carried items as prohibited vs false-alarm vs clear. `NpcProfile.carried_items` is filled at spawn on wand venues. `GameManager` binds **`C`** to `_toggle_wand_screening` and `A`/`D` to `decide_wand` while inspect/wand is open. `decide_access` still blocks admission until `controller.screened` is true, and `verify` now takes that flag. Door-staff lines log through GameManager.
- Design change (2026-10-31): the selectable dialogue tone-of-voice system was **removed**.
`Rules.Tone`/`TONE_NAMES`, `NpcProfile.tone_modifiers`, and the tone param on
`NPCController.roll_reaction/apply_player_action` no longer exist; `NpcProfile.dialogue_lines`
is a flat `Array[String]`; `GameManager` uses `_apply_interaction(level, action_name)`;
the `T` key is gone. HUD `tone_label` removed and `hud.tone_prefix` string deleted from JSON.
- Polish/accessibility: venue cycling (`V` cycles `VENUE_ORDER` and restarts the shift), aim-free
targeting (`Tab` cycles nearby NPCs within `NEARBY_RADIUS`), color-coded escalation indicator
(`Hud.set_escalation`), procedural audio (`AudioFeedback` node generates sine tones for
confirm/radio/escalation/deny), and a centralized `Strings` table (`src/core/strings.gd`, `lookup`/`fmt`)
backed by `content/strings.en.json`. NOTE: `Strings.get` was renamed to `Strings.lookup` because
`get` collides with `Object.get`.
- Newly added: LATE shift phase (start 700s) with a reachability test; PROTESTER archetype + events + colors; CALL_SOMEONE reaction; `DoorState` core class + `L` binding + visual indicator; flashlight (`G`, SpotLight3D, perception bonus at evening/late); persistence wired (`Save.save_shift_result` on shift end, history + best score on the review screen); `NavigationRegion3D` flat navmesh + `NavigationAgent3D` NPC paths (steps now visual-only); strings moved to `res://content/strings.en.json` (41 keys, loader keeps `lookup`/`fmt`); `export_presets.cfg` (Linux/Windows/macOS) + `tools/build_releases.sh` + `docs/RELEASING.md`; README Downloads section.
- Run tests: `godot --headless --path . --script res://src/test/test_runner.gd`
- Boot check: `godot --headless --path . --quit-after 120`
- Screenshot tools: `godot --path . -s res://src/test/take_screenshot.gd` (single shot to `user://screenshot.png`); `godot --path . -s res://src/test/capture_screenshots.gd` (staged README gallery shots to `user://screenshots/`, copied to `docs/screenshots/`).
- Release build verified locally: export templates for 4.3 installed at `~/.local/share/godot/export_templates/4.3.stable/`; `./tools/build_releases.sh` produced Linux/Windows/macOS artifacts for 0.1.0 in `build/` and the Linux binary passed a headless smoke test. Publishing to Gitea still needs a token (see `tools/publish_release.sh`, `docs/RELEASING.md`).
- CI: the repo now carries a **Gitea Actions** workflow at `.gitea/workflows/build.yml` (checks on
push/PR; full export + artifact upload + optional release publish on tags and `workflow_dispatch`).
A repo secret `GITEA_TOKEN` is required for tag releases. The old `.github/workflows/ci.yml` was
removed so hosts scanning both paths do not double-run.
- Venue cycling: `V` switches Hollywood Blvd → The Joke Shop → TV Studio and restarts the shift.
- Venue door-staff dialogue: `VenueScenario.door_staff_line(context, rng)` and GameManager logging
(wristband, ID/ticket, wand-check, refusal, prohibited-item). Hollywood uses a wristband line,
the Joke Shop uses ID/ticket/wand lines, and the Studio uses ticket/credential/guest-list lines.
- CI cache evaluation: Gitea Actions cache was tested for the Godot binary and
trimmed 4.7.2 templates. Restores were inconsistent and saves timed out in
`CreateCacheEntry` or were rejected as reserved by another job. The cache steps
are disabled until the host cache service is healthy; keep direct downloads.
`GODOT_SILENCE_ROOT_WARNING=1` is set on jobs.
Logs live in `.gitea/logs/` after each run.
- Visual polish (v0.2.0): phase lighting (GameManager.PHASE_LIGHTING + ProceduralSkyMaterial),
street lamps + night mode in WorldBuilder.set_night_mode, marquee Label3D, crosswalk/planters/
trees, NPC per-seed variation. capture_screenshots.gd shoots day + fast-forwarded LATE night
(6 shots in docs/screenshots/). Version bumped to 0.2.0; binaries rebuilt.
- Assets pipeline: `python3 tools/generate_textures.py` produces all authored PNGs under
`assets/textures/` (≤1024px, pow2); regenerate after changing the script, then run
`godot --headless --import` before tests. 57 headless tests guard the pipeline.
- General image pipeline: `python3 tools/asset_pipeline.py` defaults to
`openai/gpt-5-image-mini`, requests supported 1:1/3:2 sources, then fits each role's target.
It provides crop, resize, tile edge blending, alpha-glyph recoloring, and role-aware audits.
Screenshot auditing checks 1280x720 minimum, 16:9 aspect, and contrast; visual composition
remains a manual check. Python tools require Pillow from `requirements-tools.txt`.
- AI texture mode: `python3 tools/generate_textures.py --ai` generates textures via the
OpenRouter image API (`POST https://openrouter.ai/api/v1/images`, model
`bytedance-seed/seedream-4.5` by default, base64 PNG in `data[].b64_json`). Requires
`OPENROUTER_API_KEY` (a gitignored `.env` in the repo root holds it locally; source it
with `set -a && . ./.env && set +a`). `--prompt`/`--name`/`--model`/`--size` override the
default batch. Output is downscaled to ≤1024px. NOTE: seedream rejects sizes below
3,686,400 px (1920×1920), so the script requests `GEN_SIZE=2048` and downscales — do not
lower `GEN_SIZE` below 1920. Verified end-to-end 2026-10-31 (3 textures generated).
Legacy `--ai` remains on Seedream 4.5 and still ignores `background: transparent`; do not
use it for alpha-dependent assets. Gemini 3.1 Flash Image advertised broad ratios but the
current provider rejected authentication. The validated role-aware model is GPT-5 Image Mini.
- Runtime assets: all 15 loaded world/HUD/card textures now live in
`assets/textures/ai/game_ready/`; each passes role audit. Generated tile edges are blended,
icon alpha masks are recolored white where needed, and the vignette uses the existing
deterministic generator because the API returned an empty transparent image. The six
`docs/screenshots/` captures were regenerated after wiring and pass screenshot audit.
- Detailed graphics: textured facade/awning/skyline, humanoid NPC rigs + props + walk bob +
attention head-turn, inspection artifact cards, mode icons, escalation vignette, body-cam
overlay (REC + shift clock). Screenshot set regenerated (`docs/screenshots/`, day + night).
- Polish: wand scan cooldown (`WAND_COOLDOWN_S` + `wand.recharge` message).
- NOTE: collaborators occasionally add uncommitted work to this shared tree (e.g. the radio
easter egg); check `git status` before committing and leave foreign changes untouched unless asked.
- Screen-design references (`tools/screen_designs.py`, tests in `tools/test_screen_designs.py`):
`generate` turns the screen descriptions in `docs/` (README captions, ADRs 0007–0010,
PLAN, architecture) into reference images under `docs/screens/designs/` — `--draft`
draws offline Pillow wireframes, `--ai` generates realistic 3D scenes / in-game UI via
OpenRouter (default model `openai/gpt-5-image-mini`). Screens are tagged `scene` or
`ui`; prompts get a shared art-direction suffix, and requests use a 3:2 landscape
source cropped to 16:9 (not a square centre-crop). `compare` checks `docs/screenshots/*.png` for aspect/size/
blank-frame and coarse layout/palette/detail drift. Drift distances are warnings, not
failures: a mockup never matches a real render, so semantic composition is a follow-up
vision-model judge (tracked in TODO).
## Known Issues / Notes
## Gotchas / Known Issues
- **Pose capture is blocked** (narrowed 2026-11-03): the recipe (SubViewport + `force_draw()` +
`force_update_all_bone_transforms()` + disabled process/physics) still yields byte-identical rest
frames. Decisive probe: teleporting the avatar renders (diff 10) but moving its `Rig` child does
not (diff 0) — the skinned mesh ignores its `Rig` parent transform. Next attempt must answer
that, or stage poses via the known-good `capture_npc_review.gd`.
- Capture/pose harness gotchas: scripts that read captured-mouse input must run windowed
(`smoke_props.gd`); the NPC FSM re-writes `rotation.y` every physics tick, so pose/portrait
captures set the yaw after `set_physics_process(false)`; and `visible = false` does not keep the
crowd out of a portrait frame (park the rest of the crowd away instead).
- Pose capture blocked — root cause narrowed (2026-11-03 re-attempt): rebuilding the harness with
the recipe (SubViewport + `RenderingServer.force_draw()` + `force_update_all_bone_transforms()`
+ disabled avatar process/physics) still produced byte-identical rest frames. Decisive probe:
teleporting the **avatar** (CharacterBody3D) changes the render (diff 10), but moving the
**`Rig` child** (the GLB model's parent) does not (diff 0) — the skinned mesh does not follow the
`Rig` Node3D transform, so the rig lowering (`rig_y=-0.339`) never reaches the rendered model.
The failure is upstream of capture ordering. Do not re-attempt the recipe; next attempt must
answer why the skinned model ignores its `Rig` parent, or stage poses via `capture_npc_review.gd`.
- Blender tooling: `bisect_plane` and thin bevels leave zero-area triangles (Godot warns
"non-finite normal in LOD generation"); `_clean()` + a weld after bisect fix them and the audit
rejects them.
- Image models have per-model size floors/caps (Seedream needs 1920+, gpt-5-image-mini caps
lower); `generate_image` applies model-specific sizing, and concept generation defaults to
`--request-size 1536`. Do not hand-set a single size for every model.
- Frozen decisions: do not re-open `override_access`. Do not bind wand to `W`. Do not remove the
(`smoke_props.gd`); the NPC FSM rewrites `rotation.y` every physics tick, so pose/portrait
captures set the yaw after `set_physics_process(false)`; `visible=false` does not keep the crowd
out of a portrait frame (park the rest of the crowd away instead).
- Blender: `bisect_plane` and thin bevels leave zero-area triangles (Godot "non-finite normal in
LOD generation"); `_clean()` + a weld after bisect fixes them and the audit rejects them.
- Image models have per-model size floors/caps (Seedream needs 1920+; `GEN_SIZE=2048`, do not
lower; gpt-5-image-mini caps lower). `generate_image` applies model-specific sizing; concept
generation defaults to `--request-size 1536`.
- Local photo research tooling: DDG rate-limits by IP (HTTP 202) — the `refcollect.py` token bucket
(0.5 req/s, burst 3) plus retry/backoff handles it; some DDG frames are AVIF mislabeled
`.png`/`.jpg` (convert with ffmpeg; `refcollect.py sheet` skips unreadable). Commons API needs a
descriptive User-Agent and rate-limits thumbnail downloads (fetch all metadata in one
`titles=A|B|C` query).
- Frozen decisions: do not re-open `override_access`; do not bind wand to `W`; do not remove the
radio easter egg.
- Reference cycles are fixed: `FSM` holds its owner via `WeakRef`, `ShiftCoordinator` is
`RefCounted`, and persistence tests free the `Save` node — the suite exits with no
"resources still in use" warnings (was ~13). If a future subsystem introduces a cycle,
repeat the WeakRef pattern.
- Shift length: `GameManager` uses the scenario's `shift_duration_s` (900s ≈ 10-15 min);
tests and the screenshot tool override elapsed time directly.
- Headless (dummy renderer) logs "Parameter m is null" from `mesh_get_surface_count`; it does not occur with a real GL driver and does not affect logic.
- Steps have per-tread colliders (`_build_steps()` adds a `StaticBody3D` with one `BoxShape3D`
per tread), so the guard and NPCs walk up the entrance; the guard spawns on the landing.
- Persistence tests write into the real `user://shift_results.json` (harmless local pollution of the demo history).
- macOS export required `rendering/textures/vram_compression/import_etc2_astc=true` in `project.godot` (universal build demands ETC2 ASTC enabled).
- Venue `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
- Access loadout is implemented; do not re-fix `override_access` as if it were still unread.
- `Save.SETTINGS_FILE` is declared and unused; settings persistence goes through `Save.load_settings()` / `save_settings()`.
- Headless suite is **276/276**; Python tooling is **143/143**; screenshot compare is **21 good / 0 bad** and the automatic judge is **21/21 match, 0 diff**. Remaining visual follow-ups are in `TODO.md`.
- Typed spawn signal extra args: handlers must accept the 4th `event: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
- GDScript inference trap that broke the main scene: a `var` typed as a base class
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
parse error. Type it as the script type (`ActionRadial`) or annotate the local explicitly.
`--check-only --script` cannot see the `Save` autoload; use a real boot (`--quit-after 180`) to
verify a full-scene compile.
- Do not snapshot right after changing a transform in the same frame: `capture_screenshots.gd`
- GDScript: no static signals; lambdas capture locals **by value** (mutating a captured local does
not affect the outer variable — use member functions for accumulators).
- Reference cycles: `FSM` holds its owner via `WeakRef`, `ShiftCoordinator` is `RefCounted`; if a
new subsystem introduces a cycle, repeat the WeakRef pattern.
- Headless (dummy renderer) logs `Parameter m is null` from `mesh_get_surface_count` — benign, does
not occur with a real GL driver.
- macOS export requires `rendering/textures/vram_compression/import_etc2_astc=true` (universal
build demands ETC2 ASTC).
- Typed spawn signal: handlers must accept the 4th `event: Dictionary` arg or Godot will not call
them.
- GDScript inference trap: a `var` typed as a base class (`radial: CanvasLayer`) exposes script
members as `Variant`; type it as the script type (`ActionRadial`). `--check-only --script`
cannot see the `Save` autoload — use a real boot (`--quit-after 180`) to verify full-scene
compile.
- Do not snapshot right after changing a transform in the same frame — `capture_screenshots.gd`
needs a state/frame gap or the capture uses the old camera orientation.
- `EventScheduler` probabilities are per simulated second, not per call. Never poll it per frame.
- Screenshot staging lives in `src/test/capture_screenshots.gd` (guard at z≈10.5 facing the door)
and `src/test/capture_ui_states.gd` (UI states 14–21). `tools/check_screenshots.sh` renders all
captures into `docs/screenshots/`; do not regenerate them by hand.
- `EventScheduler` probabilities are per simulated second, not per call; never poll it per frame.
- Light falloff: `omni_attenuation = 0.0` is **constant**, not physical, falloff in Godot 4.7 —
all local lights use `1.0`.
- glTF `baseColorFactor` is LINEAR — writing sRGB makes props pastel; `srgb_to_linear()` is applied
in `build_glb`. A new/changed `.glb` needs `godot --headless --import` before `load()` sees it.
- `Save.SETTINGS_FILE` is declared and unused; settings persist through
`load_settings()`/`save_settings()`.
- Crowd reactions emit `event_bus.crowd_changed`; `GameManager._connect_crowd()` must reconnect
after every `coordinator.setup()` (same class of bug as the radio-dispatch reconnect).
- Packaging: NSIS uses `-D` defines (never `/D`) and resolves relative defines against the script
directory (pass `$PWD`-absolute paths); installed exe must be space-free; ICO must be classic
BMP entries (PIL emits PNG-compressed ICOs NSIS can't load). appimagetool runs system
`appstreamcli` — pass `--pedantic`.
- Credentials: `~/.git-credentials` holds the Gitea creds; `.env` holds only `OPENROUTER_API_KEY`.
- `assets/textures/game_ready/` is not imported (no `.import`) — use the imported
`assets/textures/skyline.png` path.
- Godot 4.7 writes `*.gd.uid` files — kept out of `git status` via `.git/info/exclude`.
- Collaborators occasionally leave uncommitted work in the shared tree; check `git status` before
committing and leave foreign changes untouched.
## Commands
- Tests: `godot --headless --path . --script res://src/test/test_runner.gd`
- Boot check: `godot --headless --path . --quit-after 120`
- Single screenshot: `godot --path . -s res://src/test/take_screenshot.gd`
- Staged gallery: `tools/check_screenshots.sh` (re-run the whole script, never a partial capture)
- Blender build: `flatpak run org.blender.Blender --background --python tools/npc_pipeline.py -- build`
(flatpak cwd is `/app/blender` and cannot read `/tmp` — use absolute repo paths; `.gdignore`
markers keep Godot from importing `.blend`/`tmp`)
- Textures: `python3 tools/generate_textures.py` (then `godot --headless --import`); AI mode
`python3 tools/generate_textures.py --ai`; general pipeline `python3 tools/asset_pipeline.py`
- Character checks: `python3 tools/blender_assets.py inspect|audit|fit`,
`python3 tools/npc_proportions.py`
- Docs: `python3 tools/check_docs.py` (every tracked image must be linked from its folder README)
- Reference research: `python3 tools/refcollect.py search <q> --slug <block> --limit N`
(local-only), `tools/refboard.py` (Commons), `tools/three_up.py compose/rows`,
`tools/scene_concepts.py generate/anchor/realism`
## Open Questions
@@ -756,20 +158,12 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
- Localization target languages beyond English.
- macOS notarization approach for broad distribution.
## Plan vs code
## Current State
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of
2026-11-03 the playable build covers the three venues, tutorials/campaign/checkpoints, the 0.9.3
UI/accessibility pass, the 0.9.4 NPC visual optimization, the 0.9.5 texture/material pass, the
0.9.6 integration checkpoint, and the Blender + realism asset pipeline. `TODO.md` holds the remaining
0.9.6–0.10.0 work. The project uses a custom headless runner, not GUT, and Gitea Actions, not
GitHub Actions.
The playable build covers the three venues, tutorial/campaign/checkpoints, the 0.9.3–0.9.6
polish/texture/integration passes and the Blender + realism asset pipeline (see `DONE.md` and
`CHANGELOG.md`). Active work is the unversioned **3D model and local map re-evaluation** track,
then character development, then the 0.10.0 release candidate — `TODO.md` is the priority source.
Test counts as of 2026-11-03: Godot 276/276, Python 160/160, `check_docs` OK.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
## Uncommitted Workstreams
- **None (2026-11-03).** The 0.9.6 integration checkpoint is committed, tagged
`v0.9.6` and published with seven assets; the workspace is clean apart from
local/gitignored realism and capture output. Keep the rule from `AGENTS.md`: check `git status`
before committing, and never revert another contributor's in-flight files.