diff --git a/MEMORY.md b/MEMORY.md index 20abfe2..984f3d7 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -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//` 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 /.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/.md`, so keep the pack flat when updating: - `cp -a /.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-.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/.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 `" [###--] 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 --slug --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. - -## 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.