Files
security-game/MEMORY.md
T
zwitschi f6bb2b1de3
Full build / Import + headless tests (push) Successful in 41s
Full build / Full desktop build + optional release (push) Skipped
Prepare the modular part intake: inventory, extract, verify, join and the part contract
2026-11-06 18:54:08 +01:00

363 lines
29 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MEMORY
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
- **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`.
- **AI-assisted asset review loop (2026-11-04):** head/face shape decisions use a concept +
judge loop — generate a target concept (`generate_textures.generate_image`, Seedream) and use
`tools/describe_image.py` as the vision judge, iterating until the judge's specific fixes
converge. Review pages: `docs/screens/head-review/README.md`, `docs/3d-model-review.md`
§AI-driven head rework.
- **Project agent skills (2026-11-03):** two tracked skills live in `.agents/skills/`:
`gig-line-blender-asset-contract` and `gig-line-release-qa`. They are guidance only,
add no runtime dependency and are referenced from `tools/README.md`. The third-party
`blender-skills` pack remains gitignored.
- **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/venues.md#hollywood-boulevard` 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.
- **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/release-qa.md#balance-acceptance` 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.
- **Blender MCP is optional (2026-11-04):** setup and safe part workflow are in
`docs/asset-tooling.md#optional-interactive-blender-mcp`. The headless flatpak scripts remain authoritative; MCP is
never a build/CI/test/runtime dependency, and anything MCP touched must pass the
CLI audit/fit gates.
- **Character mesh cap (2026-11-05):** all characters use 24,000 tris / 24,000 glTF vertices
(`CHAR_MAX_TRIS`/`CHAR_MAX_VERTS` in `tools/blender_assets.py`; `npc_pipeline.MAX_TRIANGLES`/
`MAX_VERTICES`). The required 13 core bones stay, optional extra bones are allowed, and the
low-poly `npc_base` fallback remains the shipped fallback. The raise funds the Quaternius base bake and
the 12-variant clothing detail. Measure the 24-NPC/Web budget after
any geometry change; never silently lower the cap without a new decision.
- **Quaternius is the NPC base (2026-11-05, owner):** ADR-0017 supersedes the generator-first
sourcing decision. The CC0 Quaternius Ultimate Modular character pack is the primary NPC mesh
and animation source. Stage 1 bakes a proportion-warped, 13-core-contract `npc_base` so
`NpcAvatar`, gear and animations keep working; the 62-joint/24-clip source rig is authoring
input, not a runtime dependency. If the warp fails the silhouette gate, Stage 2 needs a new ADR
for a 62-joint runtime, not a hybrid. The MakeHuman head and procedural `npc_base` remain
tested fallbacks until the Quaternius rollout ships. Quaternius source: CC0, attribution in
`CREDITS.md`; the three researched GLBs and licence notes are kept in
`tmp/npc-research/quaternius/`. MPFB/MakeHuman body work is closed; do not add new work there.
- **Quaternius bake keeps source detail (2026-11-05, owner):** during the contract bake (§2),
keep the Quaternius mesh as close to source as possible — no unnecessary decimation or LOD
reduction. The canonical base is only 3,199 verts / 6,206 tris, far under the 24,000 cap, so
there is no budget reason to decimate before the silhouette gate. Do not reuse the old
7,000-triangle or MPFB decimation targets.
- **Quaternius runtime rig (2026-11-05, owner, ADR-0018 option 2):** the Stage 1 proportion warp
failed the silhouette gate (arm ×1.63 / leg ×0.74 / torso ×1.61; vision judge: paddle hands,
lost forearms, stretched shins). The owner chose to **adopt the Quaternius 62-joint rig at
runtime** instead of warping: `NpcAvatar` maps the Quaternius joint names, gear attach points
are re-derived from the Quaternius skeleton (hip ≈0.87, shoulder ≈0.148/1.39, wrist ≈0.99,
knee ≈0.52) and the mesh keeps source LOD (arm-down rest pose baked). **The 24 upstream clips are
not played**: `_animate()` drives the proxies and `_sync_skeleton()` writes the bone poses, which
is what the FSM already needed for sitting, hand-raising and head turns. Playing the clips would
replace that state machine, not supplement it, so all 24 stay unused and the clips are authoring
reference only.
- **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`.
- **Release roadmap (2026-11-05, owner):** `0.9.9` Quaternius pilot base (rig/export fix,
rest-relative pose math, owner face/silhouette gate), `0.9.10` 12-role roster, `0.9.11`
scheduled venue/prop/lighting polish, `0.9.12` performance/Web/accessibility verification,
`0.10.0` public-readiness minor, then `0.10.1`–`0.10.3` fixes, `0.11.0` replay + fourth venue +
localization foundation, `0.12.0` signed/storefront builds + save v2, `0.13.x` content complete
and release candidates, `1.0.0` stable. The full gap analysis and roadmap live in `PLAN.md`; the
character track is versioned now, so a blocked rig cannot block the release. Planning only —
none of these versions is built yet.
- **Quaternius face/readability gate (2026-11-05):** the base is 6.2k tris total and replaces a
library with a sculpted realistic face, so the 0.9.9 gate is a product decision, not just an
art check. Owner picks full swap / crowd-only / extra head detail. Do not lower character
fidelity silently.
- **Pose composition is rest-relative (resolved 2026-11-05):** see `_swing_bone()` in the gotchas —
the rest-relative `set_bone_pose_rotation` form was abandoned; the global-pose write replaced it.
- **Full roster swap (2026-11-05, owner):** at the `0.9.9` face gate the owner chose option (a),
full swap. All twelve public character names are baked from the three CC0 Quaternius sources
(`casual`, `hoodie`, `worker`); variety comes from the five tint slots, the outfit rules and the
gear library. The procedural library + MakeHuman head stay in the tree as the tested fallback
(`build --source procedural`), not in the shipped roster. Silhouette variety is the known
regression — three meshes for twelve roles — and the modular part library is the `0.9.10` fix.
- **Modular part intake is a one-time manual download (2026-11-06, owner):** the upstream pack is a
Google Drive folder, so the owner downloads it once into the gitignored
`tmp/quaternius-intake/` and `tools/quaternius_parts.py` does the rest
(`inventory` → `extract` → `verify`). A part must be one skinned mesh on the 62-joint rig; a GLB
exported *without* an armature has no `JOINTS_0` and validates as bone-less, so `extract` keeps
the armature and `build` drops it again at join time.
- **Hair variety is built in-project (2026-11-06):** the upstream modular pack is a Google Drive
folder, so it cannot be fetched reproducibly; `QUATERNIUS_HAIR_PARTS` reshapes the one hair shell
each base ships (`short` identity, `buzz` squash, `long` stretch, `bald` removal) and the role's
existing `CHARACTERS[...]["hair"]` picks it. `bald` is restricted to bases whose skin closes the
skull. Outfit variety still needs the pack or new garments.
- **No hats except the firefighter helmet (2026-11-06, owner):** the iconography is not worth the
cost — a cap or hood on this head hides the face, and every hat needed a lift+scale hack to clear
the 0.228 m hair shell. `ACCESSORIES` maps no role to `cap`/`hood`, the guard/vest per-character
caps are gone, the performer cone was removed, and the primitive cap/hood fallbacks were deleted
with their `GEAR_OFFSETS`/`GEAR_SCALE` compensation. `firefighter_helmet` is the only head item.
Do not re-add head gear without an owner decision and a fit measurement.
- **Mouths are authored, not upstream (2026-11-06):** none of the three Quaternius heads has a mouth
material, so the faces read as brow-and-eye masks. `_add_quaternius_mouth()` places a small lip
block on the measured skin surface (front-most skin vertex in the 1.600–1.632 m band, ±35 mm of
centre) at 1.616 m. One rule covers all three bases; it runs before the slot map, so match the
material as `_base_name(m.name).lower() == "skin"`.
- **One character source, one fallback:** no new NPC source without an ADR. MPFB body work stays
closed, the Stage-1 proportion warp is not reopened, and the procedural `npc_base` + MakeHuman
head stay tested until the Quaternius base passes its gate.
## Gotchas / Known Issues
- **Blender material-list edits corrupt polygons silently.** Two separate bugs, same root:
`obj.data.materials.clear()` resets **every polygon to slot 0**, so the per-face indices must be
captured *before* the clear and reassigned after; and `IDMaterials.pop(index=)` repoints the
polygons that used the slot, which once painted a whole face with the eye material. The safe
pattern is `_purge_unused_materials()`: read the per-face material **names**, rebuild the slot
list from the names, then write the indices back by name.
- **A zero-user material still owns its name.** Removing a slot leaves the datablock behind, so a
donor material renamed to the freed name lands as `Eye.001` and every `m.name == "Eye"` rule
silently misses it. Purge the datablocks (`bpy.data.materials.remove(m)` when `m.users == 0`) or
match on `_base_name()`.
- **A duplicated detail face has to be offset along its own normal.** Copying the eye quad into a
pupil and nudging it in world `-Y` vanished: the eye plane is tilted, so the "front" nudge went
backwards and the pupil sat inside the sclera. Offsetting along `face.normal * 0.0032` fixed it.
Also scale the depth axis when shrinking a copy, or it flattens onto the parent plane.
- **Blender node colours are linear; Godot tonemaps them.** A mouth lip at `0.16` linear rendered as
salmon/mauve. Divide by roughly 3–4 versus the intended sRGB value: the hair uses 0.027 linear,
the lip 0.055/0.018/0.02.
- **The modular Quaternius Worker has no torso under its vest and no crown under its hard hat.**
Deleting either material leaves real holes (that is what shipped in the first 0.9.9 build: guard
and firefighter showed the inside of the head and torso). Remap instead of deleting; the only
geometry the build removes is the Worker brow bar, which is replaced by a donor part.
- **Head gear authored for the 0.315 m procedural head is wrong on the 0.20 m Quaternius head.** Gear
is authored around the head *centre* (1.62) while the attach node is the Head *bone* (1.587), and
the Quaternius centre is 1.69 — a 7 cm error put every cap brim across the brows, which I spent
several passes misreading as a mesh hole (the band was pure black and no material had faces
there). `HEAD_GEAR_LIFT = 0.103` fixes the common case. With the hats removed this only affects
the firefighter helmet.
- **Two character builders — do not confuse them.** `tools/blender_assets.py` is the real builder
for the **eight library characters, 18 gear pieces and props** (it has the 0.9.4 face/hand
sculpting in `_face()`/`_hands_and_shoes()`); `tools/npc_pipeline.py` builds **only**
`assets/models/generated/npc_base.glb`, the frozen plain-primitive fallback. Reading
`npc_pipeline.py` to judge the library's face/hand detail is wrong.
- **Pose capture fixed (2026-11-04):** the harness was missing `set_physics_process(false)`/
`set_process(false)` on the avatar, so the FSM (`_advance_fsm` + `_move_toward_target`) ran every
tick, reset `arrived`/velocity and made `theater_pose()` revert to idle/rest. Disabling both fixes
it (sit vs idle diff 5.63, walk vs idle 2.29). The earlier "moving the `Rig` child does not
render" probe was a red herring — it set the position inside `_shoot()` in the same frame as
`root.get_texture().get_image()`, so it never rendered a frame. Lesson: pose/portrait harnesses
must freeze the avatar's process/physics before driving `_animate()`.
- Capture/pose harness gotchas: scripts that read captured-mouse input must run windowed
(`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.
- Quaternius arm-down bake (working recipe, `tmp/npc-research/bake_armdown_v5.py`): pose-rotate
`UpperArm.L/R` −90° local X → read the pose matrices → apply the Armature modifier → **reset the
pose to identity** → set each arm bone's rest matrix from its pose matrix → re-add the modifier →
export `export_apply=False`. Missing the pose reset double-applies the arm-down (mesh ±0.53
instead of ±0.23). `armature_apply` in Blender 5.2 is broken (deforms the mesh but does not bake
the rest). The canonical base must be **join-only** (`build_base_minimal.py`: no `remove_doubles`
weld, no scale/ground transform, keep the Armature modifiers) — the weld and the scale/ground
transform corrupt the skin weights (arm poses to ±0.51 instead of −0.23).
- Quaternius `NpcAvatar` wiring (commits `2a6f29f`, `cd285b3`): `RIG_BONES` +
`NpcAvatar.bone_name(joint, side)` hold the **only** side mapping — the Quaternius labels `L`/`R`
opposite the project, so proxy side 0 (`-x`) drives `UpperLeg.R`/`UpperArm.R` while side 1 drives
the `.L` bones. The arm/elbow swing axis is `Vector3.LEFT` (legs stay `RIGHT`). The hand proxy is
`global_wrist − global_elbow` (subtracting the parent-relative `elbow.position` misplaces
handheld gear). Old 13-core names resolve through the same table.
- **Bone posing must write the global pose** (`NpcAvatar._swing_bone`): `reset_bone_pose()` then
`set_bone_global_pose(Transform3D(Basis(swing * global_rot), current_origin))`, conjugated with
`get_bone_global_rest()`. Both rejected alternatives fail: the rest-relative
`set_bone_pose_rotation(rest * Quaternion(rest⁻¹·axis, angle))` swings about the parent-relative
rest (the glTF importer already folded the axis conversion into it) and splayed the Quaternius
arms sideways; the plain `rest⁻¹ * swing * rest` conjugation flips the limbs 180°. Verified by
measuring `get_bone_global_pose()` after the call — posed bone axis matches the proxy pivot at
dot 1.000 on the Quaternius and the procedural rigs render unchanged.
Two caveats: children need the parent's swing passed in (the write is absolute), and
`set_bone_global_pose()` is a **no-op outside the scene tree**, so the headless runner (tests run
in `_initialize()`) cannot assert on it.
- **The Quaternius source UV map is degenerate** — every loop at (0,0), flat-colour materials, no
atlas. The AO/texture bake silently produces a 100 % black map, which tints every AO'd slot
(skin, hair, accessory) black in Godot: a black leg is the symptom. `build_base_minimal.py` now
Smart-UV-unwraps and repacks the layout (texel density 0 → 73–74 px/m on 256²), and the base
pipeline is two explicit steps with distinct files: `build_base_minimal.py`
(`quaternius/Casual_Hoodie.glb` → `quaternius_tpose.glb`, joins without welding, renames the
materials to the five tint slots, unwraps) then `bake_armdown_v5.py` (`quaternius_tpose.glb` →
`quaternius_base.glb` + `assets/models/blender_src/npc_casual.blend`). Re-running v5 on its own
output double-bakes the rest.
- Hand gear must be self-contained: the camera carried a 0.42 m neck-strap loop that floats when the
item is parented to the wrist. Hand items get a short wrist strap, not a neck strap. The backpack
straps were authored for the wider procedural chest and floated ~5 cm proud of the Quaternius
torso; the chest points now sit at x 0.12 / Godot z 0.15, which fits both.
- 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.
- 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.
- 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.
- **Rebuilding a character GLB also requires re-baking its AO** (`tools/bake_ao.py -- --only
<name>`). `blender_assets.py build` writes the GLB only; the old `<name>_ao.png` then maps onto
the new geometry and produces large dark cheek/jaw blotches. Bake the AO, re-import, and refresh
the stale `.godot` cache (scratch-copy import) before judging a face/body change.
- `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)
- Head/part pipeline: `python3 tools/npc_parts.py build|extract|assemble|inspect|sheet|compare`
(Blender subcommands run under flatpak). Head sources: `build_character(head="face"|"proto"|"mh")`.
- 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
- Whether the responder avatar should be playable/controllable or purely scripted.
- Quaternius scope: full 12-role swap, crowd-only, or extra head detail (owner decision at 0.9.9).
- Localization target languages beyond English (0.11.0/0.12.0).
- macOS notarization approach for broad distribution (0.12.0).
- Fourth-venue choice for 0.11.0 (candidate: TCL Chinese Theatre forecourt).
## Current State
v0.9.8 is published. The build covers the three venues, the shift loop, tutorial/campaign/
checkpoints, the 0.9.3–0.9.8 polish/texture/integration/character passes and the Blender +
realism asset pipeline (see `DONE.md` and `CHANGELOG.md`). The 12-character procedural library
(realistic face + four crowd variants) and the Comedy Store map rebuild are complete.
**Planning pivot (2026-11-05, owner):** the character and map work was re-sequenced into
patches (`0.9.9`–`0.9.12`) with `0.10.0` as the public-readiness minor. The gap analysis and the
three release roadmaps now live in `PLAN.md`; `TODO.md` is the executable task list. ADR-0017
Stage 1 (proportion warp) is dead; ADR-0018 option 2 (62-joint runtime) is accepted.
**Full roster (0.9.9, commits `a6ccd86` … `9fd4824`):** all twelve characters run on the
Quaternius bases (62 joints, 73–74 px/m texel density), built by the shipped
`tools/blender_assets.py` two-pass pipeline (`_quaternius_tpose_glb` → `_bake_quaternius_rest`).
The owner signed the roster off **with follow-ups** and rejected three review rows; the Worker
body defects (holes, unibrow, missing eyes) and the head-gear seating are fixed, every hat except
the firefighter helmet is removed, and every head has an authored mouth. Gates: `audit` 12/12,
`inspect` clean, `fit` OK, 24-NPC budget 174,976 tris / 0.85 ms, Godot 278/278, Python 167/167.
The owner **accepted the roster on 2026-11-06** and the review page plus its sheets were then
retired; durable evidence lives in `docs/3d-model-review.md` and `docs/blender-assets.md`. Release
packaging was fixed the same day:
excluding `tmp/`, `build/`, `docs/`, `tools/`, `src/test/` and `.agents/` from the export presets
cut the Linux binary from 426 MB to 273 MB, and `tools/build_releases.sh` writes a SHA256SUMS file.
**Open after the swap:** hand-held gear and role props are still authored for the old body (wrist
y 0.78 → 1.017, arm shorter), so the clipboard and umbrella are oversized; the eyes have no whites;
the arms graze the torso on the `worker` base; roster variety is three meshes for twelve roles. The
prop/gear fit pass is queued in `TODO.md` under 0.9.10.
**Quaternius pilot (2026-11-05, commits `cd285b3`, `fb3e75a`, `90f189a`):** `npc_casual` ships as
the Quaternius base. It renders arm-down, carries the five tint slots, plays the Idle/Walk/Sit pose
code on the 62-joint rig, and the procedural rigs are visually unchanged. Verified: `inspect`
(no issues), `audit` OK (6,206 tris / 12,988 verts, 62 bones), `fit` head 0.201 m, texel density
73–74 px/m, `npc_proportions` measured, 24-NPC budget smoke OK (168 meshes, 162,304 tris,
0.82 ms avg frame). The face/silhouette gate sheet and the three options are in
the face-gate decision (owner chose the full swap).
Test counts as of 2026-11-05: Python 167/167, Godot 278/278, `check_docs` OK.
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->