Record the upgraded 4.7.2 binary, the retained 4.3 backup and the matching export templates. Keep the project/CI baseline at 4.3 and note that 4.7.2 verified import, tests, the session smoke and a Linux export. Hide the 4.7-generated per-script UID sidecars from this clone only.
665 lines
58 KiB
Markdown
665 lines
58 KiB
Markdown
# MEMORY
|
||
|
||
Session memory for durable decisions, state, and gotchas. Keep concise, remove obsolete entries.
|
||
|
||
## Decisions
|
||
|
||
- 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`.
|
||
- `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: the finished lighting/flashlight/NPC items
|
||
are archived in `DONE.md`, and `TODO.md` carries three planned workstreams (NPC gear fit,
|
||
venue geometry + committed gallery, Forward+ desktop preset). New visual work should be added
|
||
as concrete steps under one of those headings instead of a new "realism" bucket.
|
||
|
||
- Graphics v0.6.0 plan (2026-11-01): NPCs move to Blender-authored skinned meshes
|
||
(`docs/plan-graphics-v0.6.md`, ADR-0013). `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.3 stable (official editor build, `~/.local/bin/godot`). The system's preinstalled `godot-runner` (4.7.2, Fedora) 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 4.3 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.
|
||
|
||
- **Local Godot toolchain (2026-11-01):** `~/.local/bin/godot` is 4.7.2 stable;
|
||
4.3 stable is kept as `~/.local/bin/godot-4.3.stable`. Export templates for
|
||
4.7.2 are installed under `~/.local/share/godot/export_templates/4.7.2.stable`
|
||
(4.3 remains). The project and CI still pin 4.3 in `project.godot`/the Gitea
|
||
workflow; 4.7.2 imports, runs the 231-test suite, session-shell smoke and a
|
||
Linux export successfully. Godot 4.7 writes `*.gd.uid` files; they are kept
|
||
out of this clone's status via `.git/info/exclude` until the project upgrades.
|
||
- **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.
|
||
|
||
## Handoff (read this first)
|
||
|
||
- **Status (2026-11-01, after the venue-geometry pass):** v0.7.0 is released. Post-v0.6.0 the
|
||
realism pass, the visual-alignment pass and the NPC face/hand pass all landed. Headless
|
||
**229/229**, Python tooling **87/87**, `tools/check_screenshots.sh` **11 good / 0 bad**,
|
||
`screen_designs.py compare` 11 good / 0 bad, `scene_concepts.py audit` all GOOD,
|
||
`blender_assets.py audit` exit 0, `smoke_npc_budget` OK (16 avatars, 6.94 ms of 50 ms).
|
||
`PLAN.md` is compact; README/architecture track the current controls, asset pipeline, CI and
|
||
release artifacts. The historical notes below remain useful for implementation gotchas; for
|
||
current tasks use `TODO.md`.
|
||
|
||
- **Phase 2 — AI texturing (this agent, DONE 2026-11-01).** Phase 1 assets were
|
||
already in the tree (untracked) and are preserved. Generated six seamless AI
|
||
clothing albedos into `assets/textures/npc/ai/{shirt,trousers,jacket,vest,
|
||
coat,dress}.png` (512×512, `--role tile`, gpt-5-image-mini; audit 6 good).
|
||
**Gotcha:** `asset_pipeline.py generate` defaults to `GEN_SIZE=2048`, which
|
||
gpt-5-image-mini rejects (HTTP 400); pass `--request-size 1024` for 1:1 roles.
|
||
`generate --role tile` already applies `make_seamless`, so a separate
|
||
`seamless` pass is redundant. Added to `tools/npc_pipeline.py`: `prepare`
|
||
(layers AI shirt→cloth, trousers→pants albedos at 128px) and `palette`
|
||
(writes `assets/textures/npc/palette.json` via `model_pipeline.dominant_colors`).
|
||
`write_slot_textures()` now skips the albedo for slots with an AI override
|
||
(normal/ORM stay procedural). Blender `build` re-run: GLB audit ok, 1204 tris /
|
||
2488 verts, AI albedo embedded for cloth+pants. Phase 2 completed and shipped with
|
||
the v0.6.0 asset library.
|
||
**Import concurrency:** a parallel agent is running
|
||
`timeout 300 godot --headless --editor --quit --path .` and then checks
|
||
`assets/models/generated/npc_base.glb.import` — treat that as the canonical
|
||
import. My own `godot --headless --path . --import` hung with no output past
|
||
the engine banner (two Godot instances contended on `.godot`); I killed the
|
||
orphan (PID 2482934). Do not run a second import while that `--editor --quit`
|
||
is active.
|
||
Next: Phase 3 (`NpcAvatar._load_model` + slot tinting) consumes
|
||
`palette.json` and the GLB; Phase 2's capture/judge item is blocked on that.
|
||
|
||
- **Git hygiene + tooling (2026-11-01):** dev asset folders are now gitignored
|
||
and untracked: `assets/models/concepts/`, `assets/models/npc/` (Blender
|
||
sources), `assets/textures/npc/ai/` (raw AI fabrics), and the raw
|
||
`assets/textures/ai/*` outputs (only `assets/textures/ai/game_ready/` stays).
|
||
Kept in the repo: `assets/textures/npc/*_{albedo,normal,orm}.png` +
|
||
`palette.json`, `assets/models/generated/*` (props + npc_base.glb), and the
|
||
authored `assets/textures/*` set. `tools/asset_pipeline.py generate` gained
|
||
`--variants N` — generates N alternatives saved as `<stem>-1..N.png` (N=1
|
||
keeps the unsuffixed path). Python tooling tests still pass (exit 0).
|
||
|
||
- **Screenshots + docs (2026-11-01):** regenerated the eleven README captures
|
||
(`capture_screenshots.gd` + `capture_prop_gallery.gd`) — ten changed, the
|
||
prop gallery is byte-identical (props unchanged). README gained an NPC
|
||
character-model subsection, a "Confirmed game assets" section (linked
|
||
`game_ready` textures, generated models, NPC maps), and refreshed
|
||
dev-status / not-yet lines; `docs/architecture.md` Level 2 now lists
|
||
`npc_pipeline.py` + `asset_pipeline.py` and the confirmed-asset locations.
|
||
|
||
- Repo: `/run/media/system/DATA/Documents/06-games/security-game` (also reachable at
|
||
`/var/home/zwitschi/Documents/06-games/security-game`). Godot: `~/.local/bin/godot` (4.3.stable).
|
||
Tests: `godot --headless --path . --script res://src/test/test_runner.gd` → **177/177**.
|
||
Python tooling tests: `cd tools && python3 -m unittest test_asset_pipeline test_screen_designs test_model_pipeline` → **53**.
|
||
Session shell smoke: `godot --headless --path . --script res://src/test/smoke_session_shell.gd` → exit 0.
|
||
Windowed control smoke: `godot --path . --script res://src/test/smoke_focus_radial.gd` → exit 0
|
||
(now also covers wheel cycling and right-click clearing).
|
||
- `main` is in sync with origin. **v0.2.2** tags the session-shell + world-follow-up commit;
|
||
CI builds the portable binaries, NSIS installer, and AppImage on the tag push.
|
||
- **v0.3.0 (2026-10-31, working tree):** rendering pass (`src/game/materials.gd`, phase-driven
|
||
environment), generated `.glb` props (`tools/model_pipeline.py`, `assets/models/generated/`),
|
||
aim-free selection with wheel cycling and Shift precision walk (ADR-0011/0012), and a **Web**
|
||
export target. `export_presets.cfg` gains a `Web` preset with `variant/thread_support=false`
|
||
(static hosting, no COOP/COEP); `tools/build_releases.sh` exports `build/web/` and zips it to
|
||
`build/gig-line-web-<ver>.zip`, and `publish_release.sh` uploads it. Covered by
|
||
`src/test/tests/test_web_export.gd`.
|
||
- **Prop atlas (v0.3.1):** `model_pipeline.atlas_for()` builds a per-prop power-of-two PNG with
|
||
one 8×8 cell per palette role, plus a metallic/roughness atlas (G=roughness, B=metallic). The
|
||
GLB merges all parts into one primitive with constant per-vertex UVs (cell centre) and a single
|
||
material (`baseColorTexture` + `metallicRoughnessTexture`, NEAREST sampling). Colours are sRGB in
|
||
the texture, so the old `srgb_to_linear`/`_linear_rgba` helpers were removed. `audit_glb` fails
|
||
if a prop is not exactly one material or misses TEXCOORD_0/images.
|
||
- **Mockups complete (v0.3.2):** `docs/screens/designs/` holds AI references for all 19
|
||
catalogue screens. Only the six staged captures (01–06) are compared/judged; the other 13
|
||
have no `capture`, so adding staged captures for the wand panel, timeline and escalation HUD
|
||
would extend automated visual checks. `generate` now calls `_load_env_file()`.
|
||
- **Staged captures (v0.3.3):** `capture_screenshots.gd` now emits nine shots. States 7–10 switch
|
||
to The Joke Shop at elapsed 620 s (EVENING has routine spawns; LATE has none), retry the wand
|
||
until it alerts, then jump to 760 s after staging for night lighting. The timeline needs
|
||
`recorder.close_all_open()`, because `to_dict()` only lists closed incidents. The escalation
|
||
HUD mockup is third-person now; `judge` still classes it `review` (no diff).
|
||
- **Prop collision (v0.4.0):** `_add_prop_model()` fits a `StaticBody3D` from the model bounds
|
||
(cylinder for bollard/bin/hydrant, box otherwise) and adds a `NavigationObstacle3D`; NPC agents
|
||
enable avoidance and move with the safe velocity from `velocity_computed`. The navmesh stays a
|
||
flat plane, so avoidance (not a baked mesh) routes guests around props. Placements live in
|
||
`PROP_PLACEMENTS` with `placement_problems()` validation; variants come from `variant_for(index)`.
|
||
`smoke_props.gd` must be windowed because the guard reads captured-mouse input.
|
||
- **Realism pass (2026-11-01, completed + adopted):** `tools/scene_concepts.py`
|
||
(`list`/`generate`/`process`/`promote`/`audit`/`realism`) never overwrites an image
|
||
(`next_free_path` adds `-vN`; `process` skips existing unless `--new`; `promote` keeps an
|
||
existing pick). Raw AI output: `assets/textures/ai/v2/raw/` (gitignored). Candidate pool:
|
||
`assets/textures/ai/game_ready/v2/` (gitignored) and the **tracked runtime assets** are
|
||
`.../v2/picks/{skyline_day,skyline_dusk,skyline_night,brick_facade,sidewalk_pavers,asphalt,
|
||
asphalt_wet,awning_canvas}.png` (+ `_normal`). Concepts `docs/screens/concepts/` (committed,
|
||
`.gdignore`, targets in its README). Runs `docs/screens/realism/<run>/` (PNG captures
|
||
gitignored; `report.{json,md}` + `sheet.jpg` committed).
|
||
`src/test/capture_realism.gd` runs five stages (day 0 s / evening 620 s / late 760 s /
|
||
joke_shop 700 s / studio 700 s; `--stages=` subset) and swaps textures by the material's
|
||
*original* albedo path. **Gotcha:** in the SceneTree script, do not overwrite `_phase` after
|
||
`_begin_stage()` sets `done`, or the run loops forever between stage/shoot (`_queue` empty).
|
||
**Gotcha:** `elapsed_s = 620` is *Evening* (dusk), 700+ is Late. Godot 4.3
|
||
`StandardMaterial3D` has no `fog_enabled`; the skyline is a `ShaderMaterial` with
|
||
`render_mode unshaded, cull_disabled, fog_disabled` + `ALPHA_SCISSOR_THRESHOLD` so the
|
||
panorama's own haze shows. Backdrop plane is 90 m tall with 2 UV repeats (~200×90 m per
|
||
repeat, near the 2:1 panorama aspect). Legacy `assets/textures/skyline.png` is no longer
|
||
loaded but stays on disk for the `test_world` asset check.
|
||
**Phase look:** day sun 0.62 / exposure 0.6 / ambient 0.22; evening sun 0.12 / exposure
|
||
0.72 / dark violet sky; late exposure 0.95 / ambient 0.10 with the wet asphalt street and a
|
||
bright skyline tint (1.5) so the night windows read. Stripes: `stripe_period()` (detrended
|
||
autocorrelation) + `align_stripes()` (resample to whole periods) replace the edge crossfade
|
||
for `awning_canvas`; `audit_striped_tile()` compares the wrap step with the harshest interior
|
||
step.
|
||
**Judge:** the default `google/gemini-2.5-flash-lite` mostly works but intermittently answers
|
||
HTTP 400 "API key not valid" (one shot of ten on 2026-11-01); re-run or pass
|
||
`--model google/gemini-2.5-flash` when it does. Scores are noisy (±1–2), so
|
||
`realism --repeats 3` averages and `summarize()` reports a like-for-like mean over the shots
|
||
every set captured. Final: overall 0.8→1.0, backdrop 0.6→1.6, metric gap 0.444→0.348.
|
||
`screen_designs.py judge` gained one retry for the same transient 400.
|
||
**Character portrait capture (2026-11-01):** `capture_realism.gd` has a `portrait` stage
|
||
(`npc_portrait` -> concept `npc_portrait_day`) because crowd shots cannot judge avatars. It
|
||
freezes one NPC, parks the rest of the crowd 140 m away (a passer-by otherwise blocks the
|
||
frame), moves the subject to an open sidewalk patch, turns it into the camera and stands the
|
||
guard on the sunlit side. Needed because the NPC FSM re-writes `rotation.y` every physics tick
|
||
(so the yaw must be set after `set_physics_process(false)`), the venue's morning sun leaves a
|
||
face in shadow from most angles, and `visible = false` does not keep the crowd out of frame.
|
||
`--portrait-type=N` forces a role; hooded roles (trespasser/violent/crisis) are skipped.
|
||
**Flashlight (2026-11-01, final):** a *tight* `SpotLight3D` on `Head`: `spot_range` 20,
|
||
`spot_angle` 14, `light_energy` 4.2, `spot_attenuation` 0.9, forward `rotation_degrees = 0`,
|
||
starts invisible. A fake additive light shaft was tried twice (cylinder with a UV ramp, then a
|
||
vertical-fade `ShaderMaterial`) and **rejected both times**: on GL Compatibility a first-person
|
||
camera inside the cone renders it as a filled disc and the far ring shows, so
|
||
`Materials.light_beam()` and the beam mesh are gone. Regression guard:
|
||
`test_flashlight_aims_a_tight_forward_spot` in `src/test/tests/test_door_and_flashlight.gd`
|
||
(lamp on the head, aims straight, cone ≤ 20°, starts off). Night captures are staged in LATE
|
||
(`elapsed_s` 760), not dusk.
|
||
**NPC detail pass (2026-11-01):** `_face()` now builds sclera + iris + upper lid per eye, brow
|
||
ridge, cheekbones, nose bridge/tip, ears with lobes and split upper/lower lips; the new
|
||
`eye_white` material is a *fixed-colour* slot, so `_apply_outfit_materials()` leaves it white.
|
||
`_hands_and_shoes()` has four fingers plus a thumb and `_clothing()` adds `Cuff` tori.
|
||
Budget work: `CHAR_MAX_VERTS` 5000 (see the library note), body decimation 0.26, upper lids are
|
||
boxes rather than spheres, and knuckle spheres/nostrils were cut to stay inside it.
|
||
**Orientation debugging:** to check a GLB's front axis, compare the mean Z of the `eye_white`
|
||
surface (face) and of the `rubber` sole verts (toes) — all 8 avatars are +Z, so any
|
||
"they walk backwards" symptom is a yaw bug, not an export flip.
|
||
- **Blender asset library (v0.6.0, integrated 2026-11-01):**
|
||
`tools/blender_assets.py` (`build`/`render` in Blender; `audit`/`sheet`/`list` stdlib) writes
|
||
`assets/models/blender/{npc,npc_gear,props}/*.glb`, `manifest.json` (integration source of
|
||
truth), sources in `assets/models/blender_src/` (`.gdignore`d), and review sheets
|
||
`docs/blender_previews/sheet_*.jpg` (per-asset PNG renders are gitignored). 8 characters, 18
|
||
gear, 20 props; notes in `docs/blender-assets.md`. Characters are drop-in for `npc_base`
|
||
(same mesh node name, bones at the same rest positions, five tint slots) and stay under the
|
||
6000/4000 budget only because of body decimation (0.26) + `smart_project` at 80°: glTF vertex
|
||
count includes UV-seam splits, so extra beveled detail quickly breaks the limit. The 2026-11-01
|
||
face/hand pass raised `CHAR_MAX_VERTS` to 5000 (tris 3,974–5,026) after measuring 16 avatars at
|
||
~6.9 ms of a 50 ms frame; `npc_pipeline.MAX_VERTICES`/`MAX_TRIANGLES` (base mesh) are unchanged.
|
||
**Gotchas:** `bisect_plane` and thin bevels leave zero-area triangles (Godot warns "non-finite
|
||
normal in LOD generation"); `_clean()` + a weld after bisect fix it, and the audit rejects them.
|
||
Fixed-colour detail materials carry a 4×4 texture because `_apply_outfit_materials()` sets
|
||
`albedo_color = WHITE` on unknown slots. Cycles CPU render ≈ 3 s/asset; Godot import of the
|
||
library is clean.
|
||
**Integration:** `NpcAvatar.character_model_for()` selects a variant by role/seed; only the
|
||
five contract slots are tinted (detail materials untouched; `npc_vest` keeps its fixed hivis
|
||
vest, firefighter/guard get dark uniform tints). Gear is attached at head/back/chest/hand and
|
||
falls back to primitives; `npc_hoodie` skips the extra hood gear. `WorldBuilder` prefers
|
||
`assets/models/blender/props/` for the six replacements and the new street dressing, fits the
|
||
same colliders/obstacles, and collects library emissives for `set_night_mode()`.
|
||
`LIBRARY_TRIANGLE_BUDGET` = 60k (current ~27,986). Library props are excluded from
|
||
`PROP_PLACEMENTS` only when they need dedicated slots (door/marquee/sedan/handrail/lantern).
|
||
**Gear fit (v0.6.1):** `fit` measures head width (0.315 m), skirt/hip radius and gear bounds;
|
||
sunglasses are 0.202 m, the bag's body inner face is 0.262 m (coat 0.225, dress 0.260), and the
|
||
helmet is lifted 2 cm above the brow. `test_npc_character` guards head/back gear in Godot.
|
||
Blender's GLB export is byte-nondeterministic for unchanged assets (padding/index order), so do
|
||
not commit a full no-op rebuild; rebuild only the assets whose geometry changed.
|
||
- **Venue geometry (v0.6.1+):** `WorldBuilder.VENUE_SPECS` is the per-venue table; `WorldBuilder.new(root, id)` / `set_venue()` select it, unknown ids fall back to `hollywood_building`. Dimensions, window/pilaster rows, stairs, street, nav bounds, guard spawn, feature flags and prop placements are read from the spec. `GameManager.world_root` holds only the world; `_rebuild_world_for_venue()` frees/recreates it on venue selection/cycling so guard/UI survive. Joke Shop uses procedural alley walls/shutters/dumpster/AC; TV Studio uses a stone facade, bulb canopy, car paint and barrier placements. Gallery shots 12/13 use the existing `joke_shop_queue`/`studio_arrival` references.
|
||
- **Screen-design judge (2026-11-01):** `screen_designs.py judge` could not run because `OPENROUTER_API_KEY` returned HTTP 400 "API key not valid". `check_screenshots.sh` still passed 13/13; the judge retry is a TODO follow-up.
|
||
- **Portrait capture:** `capture_realism.gd` faces the **Rig** toward the camera (using the
|
||
inverse look direction) and hides the blue `FocusCircle`; the old code used the wrong yaw and
|
||
left the focus disc over the face. An explicit `--portrait-type=N` now overrides the hooded
|
||
skip, but the day roster spawns none of types 5-7, so a hooded close-up is deferred.
|
||
- **v0.6.0 environment pass:** `tools/bake_ao.py` bakes 256² AO maps per library asset
|
||
(`<name>_ao.png`, red channel) and `Materials.apply_ao()`/`SurfaceMaterials` wire them in;
|
||
`tools/make_normal_maps.py` derives tangent normals for the 1024² brick/concrete/asphalt
|
||
textures. Forward+ boots on desktop but is rejected for v0.6.0 (project-wide renderer, Web
|
||
must stay GL Compatibility). Headless 222/222, Python 69/69.
|
||
- **Prop pipeline (done):** audit enforces 400 tris / 600 verts, ground contact, UV range, no
|
||
degenerate tris, one material. `capture_prop_gallery.gd` writes `10-prop-gallery.png` (3x2 studio
|
||
line-up, back row on a platform) and the `prop_gallery` catalogue entry compares it.
|
||
- **NPC character pass (v0.5.0):** Rig is articulated (`Rig/Hip* -> Knee`, `Shoulder -> Elbow ->
|
||
Hand`); `outfit_for` adds layer/hair_style/hand_item; `_idle_layer` drives phone/clipboard/crossed
|
||
and escalation gestures; `_reaction_tag` shows `!!`, `?!`, `!`, `?`; `apply_lod` swaps to a
|
||
billboard impostor at 26 m (hysteresis 1.5 m); GameManager `_refresh_queue` assigns queue offsets.
|
||
Perf: 16 avatars = 534 meshes, ~6.9 ms headless.
|
||
- **Player movement (v0.5.0):** **W was inverted** (`get_axis(forward, back)` is negative for W and
|
||
the code multiplied it directly); now `forward * -input_dir.y`. Step-up uses test_move + a raised
|
||
copy (0.32 m). Guard emits footstep/landed/precision_changed; GameManager forwards them to
|
||
AudioFeedback and the HUD. Gamepad actions are added at runtime by
|
||
`GuardController._ensure_gamepad_bindings`; remaps persist via `Save` `input_bindings` and are
|
||
applied by `InputBindings.apply` (key/button events only). Windowed `smoke_movement.gd` guards it.
|
||
- **Scheduled TODO pass complete:** only the optional Future ideas remain (live replay, extra
|
||
venues, voice audio, image-to-mesh). Grounding facts: the
|
||
NPC rig is rigid capsules with a whole-limb swing, and `guard_controller.gd` uses one `accel`
|
||
value. Each task should get its own windowed smoke or capture where it changes feel/looks.
|
||
- **Backlog complete:** every planned TODO section is done and moved to `DONE.md`; `TODO.md`
|
||
now only lists optional future ideas (live 3D replay, NPC LOD, more venues, real audio, prop
|
||
atlas, AI mockups for the new catalogue entries, image-to-mesh props). GUT was consciously not
|
||
adopted (custom runner covers it without an editor plugin).
|
||
- Do not re-open `override_access`. Do not bind wand to `W`. Do not remove the radio easter egg.
|
||
|
||
## 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 last regenerated after the realism and NPC passes; re-run `tools/check_screenshots.sh` (not a partial capture) before replacing them.
|
||
- `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()` logs the NPC's seeded `dialogue_lines` pick.
|
||
- **NPC navigation was dead (fixed 2026-10-31):** avatars never moved in the live game, for three
|
||
stacked reasons. (1) `agent.target_position` was re-assigned every physics frame, which
|
||
invalidates the path so `get_next_path_position()` keeps returning the current point — only set
|
||
it when the target changes. (2) Nav path points sit on the navmesh plane (y=0) while bodies stand
|
||
at y≈0.5, so a 3D "are we there" test always looked 0.5 m away and the horizontal direction was
|
||
zero — compare horizontally. (3) `path[0]` is the old start point, so picking any point "far
|
||
enough away" can pick the point behind you and oscillate — pick the first point that is
|
||
**closer to the goal** than the current position. `_nav_try_frames`/`_move_direct` fallback is
|
||
still there.
|
||
- **Crowd reactions listen on the EventBus:** `CrowdSystem.apply_event()` emits
|
||
`event_bus.crowd_changed`; its own `changed` signal is never emitted. `GameManager._connect_crowd()`
|
||
therefore connects to `coordinator.event_bus.crowd_changed` (reconnect after every
|
||
`coordinator.setup()`, which recreates both crowd and event bus).
|
||
- **Crowd presentation:** `NpcAvatar.start_rubberneck(at)` (idle bystanders away from the door stop
|
||
and face the incident for 12 s) and `start_flee(from)` (panic: walk away at 2.4 m/s, then free
|
||
themselves). Someone `awaiting_interaction` within 3 m of the door is "engaged" and does not
|
||
rubberneck. Use `position` (not `global_position`) for that distance check so it also works in
|
||
headless tests outside the tree.
|
||
- **Perf:** a busy 16-NPC entrance measured ~1.2 ms/frame (824 fps with vsync off, 800 samples),
|
||
so LOD/culling is not warranted at `MAX_LIVE_NPCS = 16`. Re-measure before raising the cap.
|
||
- **Incident recording was dead:** `IncidentRecorder.start_incident()` was never called, so every
|
||
`add_event`/`add_access_evidence` silently no-opped and the review always showed 0 incidents.
|
||
`_begin_incident()` now starts one per focused NPC (`has_open_incident()` guard), verbs call
|
||
`_ensure_incident()`, and `ShiftCoordinator._finish_shift()` closes leftovers as `unresolved`
|
||
before building the review.
|
||
- **Editor-ready venues (2026-10-31):** `tools/export_venues.gd` (a SceneTree script) saves the
|
||
three factory scenarios to `content/venues/<id>.tres` with `ResourceSaver`; the `.tres` files
|
||
round-trip through `ResourceLoader` (events, profiles, policy, shift phases, door lines,
|
||
locations). `test_venue_resources.gd` also fails if the exported files are missing or stale
|
||
relative to the factory, so re-run the export after changing `ScenarioFactory`.
|
||
- **Vision judge (2026-10-31):** `python3 tools/screen_designs.py judge` sends the capture plus
|
||
its concept mockup to a vision model (`DEFAULT_JUDGE_MODEL = google/gemini-2.5-flash-lite`,
|
||
override with `--model`; `openai/gpt-4o-mini` also works). Reads the repo `.env` via
|
||
`_load_env_file()` (never overrides real env). Verdicts are classed `match` / `review`
|
||
(mismatch below `--fail-confidence`, default 0.6) / `diff` (confident mismatch → exit 1) /
|
||
`unknown`. `--dry-run`, `--json`, `--only` supported. The prompt explicitly ignores art style,
|
||
because the references are stylised mockups; the live run gave 3 match, 3 review, 0 diff.
|
||
Python tests: `cd tools && python3 -m unittest test_asset_pipeline test_screen_designs`.
|
||
- **String formatting safety (2026-10-31):** `Strings.placeholder_count()` counts `%` specs
|
||
(ignoring `%%`), `fmt()` warns and returns the template when the arg count does not match
|
||
(instead of raising an engine error), and `validate()` lints the table for trailing `%`,
|
||
unsupported conversions, and mixed positional/sequential placeholders. Tests cover all three
|
||
plus a formatting pass over every placeholder key with type-correct dummy args.
|
||
- **Authored scenario content (2026-10-31):** events can now carry `hint` (a string key logged
|
||
when the avatar spawns), `extra_spawns` (celebrity arrivals pull in tourists/influencers via
|
||
`_spawn_npc` with `kind: routine`), and `refuses_screening` (copied to
|
||
`NPCController.refuses_screening`; `_toggle_wand_screening` logs `wand.refused`, records a
|
||
`wand_refused` event, applies a small prop hit, and does not open the panel). `film_boundary`
|
||
is a new intent that maps to `steps_end` so the influencer stands past the posted line.
|
||
- **Accessibility (2026-10-31):** escalation text is `"<name> [###--] L3/5"` via
|
||
`Hud.escalation_meter(level)` (pure, tested) so the cue survives grayscale. HUD font scale uses
|
||
`add_theme_font_size_override` on the core labels (`set_font_scale`), **not** `Control.scale`,
|
||
which pushed the right-anchored clock label off-screen. Settings load before `_build_ui()`, so
|
||
`_ready()` re-applies player settings after the HUD exists.
|
||
- **Audio (2026-10-31):** `AudioFeedback.start_crowd_bed()` builds a 4 s looping `AudioStreamWAV`
|
||
(one-pole-filtered noise + 68/103 Hz drone, seeded) and `set_crowd_level(mood)` maps 0..1 to
|
||
-34..-12 dB. `play_radio_open/close` push short filtered-noise bursts. All procedural; with a
|
||
dummy (headless) audio server the calls are no-ops. `_free_player()` returns null when `_ready`
|
||
has not run (tests), and callers handle null.
|
||
- **NPC theater (2026-10-31):** `NpcAvatar.theater_pose()` maps `intent` + `arrived` to
|
||
idle/sit/photo/film/perform. Pure helpers `crouch_for_pose`, `prop_lift_for_pose`,
|
||
`sway_for_pose` keep the math testable. Sit lowers the rig by 0.34 and swings the legs forward
|
||
(negative X pitch; positive looked like kneeling). `_photo_prop` is set for influencer/tourist
|
||
cameras, `_sign_prop` for protester/performer boards; their base Y values are captured at build
|
||
time so the animation offsets from rest.
|
||
- **Body-camera timeline (2026-10-31):** `src/game/ui/event_timeline.gd` flattens
|
||
`review.incidents.incidents[].events` into one time-ordered list (`flatten`), formats each row
|
||
with `describe`, and steps with Left/Right or the buttons. `ShiftCoordinator.started_at_ms`
|
||
gives the review an absolute start for mm:ss stamps. `GameManager._open_timeline` hides the
|
||
review dim (two stacked overlays made the screen nearly black) and `_timeline_from_review`
|
||
restores it on close. Smoke: `src/test/smoke_timeline.gd`.
|
||
- **Radio dispatch was dead after the first shift:** `coordinator.setup()` builds a new
|
||
`RadioSystem`, so the `responder_arrived` connection made in `_ready` was lost on every venue
|
||
switch/restart (responders never resolved anything). `_connect_radio()` now re-wires it next to
|
||
`_connect_crowd()` after every setup. Same class of bug as the crowd signal.
|
||
- **Emergency aftermath:** `ShiftCoordinator.responder_outcomes` / `investigations` feed the review.
|
||
`evacuation` (emergency call, low danger, no weapon) locks the door, makes every NPC flee, marks
|
||
them `leave_requested`, and applies a `security_failure` hit. An emergency placed while
|
||
`current_npc_truth` has no weapon and `danger_level <= 1` appends an investigation note, records
|
||
`investigation_opened`, and applies `false_report` -6.
|
||
- **Skyline backdrop (2026-10-31):** `PlaneMesh` defaults to a **horizontal** XZ plane — set
|
||
`mesh.orientation = PlaneMesh.FACE_Z` or the "backdrop" renders as a band overhead. The skyline
|
||
is now a ring of four 400x60 planes at 200 m (center y=30, so building bases meet the ground),
|
||
`TEXTURE_FILTER` default, `TRANSPARENCY_ALPHA_SCISSOR` with threshold **0.5** (0.3 left faint
|
||
mip dashes across the sky), unshaded with a lifted albedo (`Color(2.6,2.7,3.0)`) so it is not
|
||
pure black. Ground is 500x500 so no sky gap shows under the buildings. The `skyline.png` import
|
||
needs `mipmaps/generate=true`.
|
||
- **Asset gotcha:** `assets/textures/game_ready/` is *not* imported (no `.import`). Use the
|
||
imported `assets/textures/skyline.png` path. Untracked duplicate folder should not be referenced.
|
||
- Host sign-in follow-up is **done**: `REQUIRE_SIGNIN_VIEW = false` in `/etc/gitea/app.ini`
|
||
(backup `app.ini.bak-*` alongside), Gitea restarted; anonymous repo page/API return 200 and
|
||
release assets download in full. Reverse by setting it back to `true` and restarting.
|
||
- **Release publishing was broken since v0.2.2 (fixed 2026-10-31):** `publish_release.sh`
|
||
only built `AUTH` in the `GITEA_TOKEN` branch; the `~/.git-credentials` branch set `CRED`
|
||
and then used unbound `${AUTH[@]}` under `set -u`, so every API call failed and the tag
|
||
job ended "could not create or find release". The repo also has **no** `GITEA_TOKEN`
|
||
secret (`/actions/secrets` is empty) and the runner has no git credentials. Fixes: the
|
||
script sets `AUTH=(-u "$CRED")`; the workflow falls back to `github.token`
|
||
(`secrets.GITEA_TOKEN || github.token`) — but `github.token` is **not** provided by this
|
||
Gitea/act runner, so the env dump showed no token and the v0.2.10 tag build failed again.
|
||
Final fix: `publish_release.sh` falls back to the **local git extraheader** that
|
||
`actions/checkout` writes (`git config --local --get-regexp '^http\..*\.extraheader$'`),
|
||
which carries the runner's basic-auth credentials for the same server. Verified locally by
|
||
adding that extraheader and publishing v0.2.10 (all five artifacts). **v0.2.9 and v0.2.10
|
||
are published**; v0.2.4–v0.2.8 tags have no artifacts. Verify the CI path on the next tag.
|
||
- **CI publishing verified (2026-10-31):** tagging v0.2.12 produced a release with all five
|
||
artifacts automatically (no repo secret), confirming the checkout-extraheader fallback.
|
||
- **Host access (2026-10-31):** `root@192.168.88.30` now works from this workstation (key auth),
|
||
host name `gitea`, Gitea 28.2.0. Maintenance applied: `apt-get clean`, journald capped to
|
||
`SystemMaxUse=300M` (`/etc/systemd/journald.conf.d/99-size.conf`, journal ~133M), disk at
|
||
~7.6G free of 16G.
|
||
- **Host disk resolved (2026-10-31):** the Gitea VM root filesystem was expanded from 7.8G to
|
||
16G (`/dev/mapper/pve-vm--500--disk--0`, now ~7.8G free). The pruned **v0.2.0** and **v0.2.9**
|
||
releases were restored from the local `build/` artifacts and verified (4 and 5 assets).
|
||
`git.allucanget.biz` runs on this host as user `zwitschi`; root@192.168.88.30 is **not**
|
||
reachable from this workstation (publickey denied; password auth closes the banner), so host
|
||
maintenance must go through a `zwitschi` login with sudo.
|
||
- **Host disk full (2026-10-31, historical):** the v0.2.11 tag build created release id 27 and logged the
|
||
uploads, but they failed with `HTTP 500 Attachments.Save: write … no space left on device`
|
||
(`/dev/mapper/pve-vm--500--disk--0`, 7.8G, 100%, 0 free). `git push` then failed too
|
||
("unpacker error"). Unblocked by deleting the superseded **v0.2.0** and **v0.2.9** releases
|
||
via the API (~565 MB of assets), then publishing v0.2.11 locally with the fixed script
|
||
(verified 5 assets). Remaining ~150 MB is tight — the next release needs host cleanup on
|
||
192.168.88.30: `sudo journalctl --vacuum-size=100M` (journal 837 MB), `sudo apt-get clean`
|
||
(91 MB), clear `/var/lib/gitea/data/attachments/tmp`. SSH works as `zwitschi`; `sudo -n`
|
||
needs a password. **Releases now:** v0.2.11, v0.2.10, v0.2.1 (5 assets each); v0.2.0 and
|
||
v0.2.9 entries were pruned. PLAN.md + ADRs 0007–0010 still describe the intended UX.
|
||
- **Release v0.2.1 is LIVE** (2026-10-31): CI run 1110 built the NSIS installer + AppImage
|
||
and published — installers/AppImage are on the releases page. Releasing = bump version → tag → push.
|
||
- 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`.
|
||
- Distribution facts (verified via Gitea API + host creds): repo is **public**, account is
|
||
admin; `v0.2.1` has all 5 assets; the installers were initially missing from the release
|
||
because `publish_release.sh` uploaded only portables — now fixed (uploads installer +
|
||
AppImage; falls back to `~/.git-credentials` when no `GITEA_TOKEN`).
|
||
- Host sign-in: instance `[service] REQUIRE_SIGNIN_VIEW=true` blocks anonymous browse/download
|
||
(not a repo/API setting). `~/.git-credentials` holds the Gitea creds (basic-auth usable);
|
||
`.env` only holds `OPENROUTER_API_KEY`.
|
||
- **Release v0.2.0 was LIVE**: pushing the `v0.2.0` tag made Gitea Actions run the build job
|
||
and publish automatically (host has the `GITEA_TOKEN` secret). Tag page and `/releases` are
|
||
public (HTTP 200). Caveat: anonymous **asset downloads** redirect to login on this host
|
||
(host setting — tracked in TODO). Future releases = bump `config/version`, `git tag vX.Y.Z`,
|
||
push. No local token needed.
|
||
- Core simulation, three scenarios, v0.2.1 installers/AppImage, first-person prototype (inspect/wand/radio, shift review, Tab targeting, V venue cycle) are on local `main`. Tagged v0.2.1 was 57 tests; this working tree is **74/74**.
|
||
- 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
|
||
(id/ticket/wand-check, refusal, prohibited-item) — Joke Shop flavor.
|
||
- CI cache: `.gitea/workflows/build.yml` no longer uses `actions/cache` (the runner can't reach
|
||
the GitHub cache service; import is fast). `GODOT_SILENCE_ROOT_WARNING=1` 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
|
||
|
||
- 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 are visual-only (no collider) so NPCs can path to the entrance; the guard spawns on the landing so it never needs to climb.
|
||
- 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).
|
||
- Joke Shop `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 **229/229**; the windowed smokes, screenshot compare and judge pass. 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`
|
||
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).
|
||
`docs/screenshots/` copies are manual: copy from `user://screenshots/`.
|
||
|
||
## Open Questions
|
||
|
||
- Whether the responder avatar should be playable/controllable or purely scripted.
|
||
- Localization target languages beyond English.
|
||
- macOS notarization approach for broad distribution.
|
||
|
||
## Plan vs code
|
||
|
||
`PLAN.md` describes intended design; `README.md` and `DONE.md` describe what shipped. As of
|
||
2026-11-01 the playable build covers all three venues, controls, access/radio/evidence/review,
|
||
the Blender asset library, the realism pass, and the visual-alignment pass. `TODO.md` holds only
|
||
the realism follow-ups and optional future ideas. The project uses a custom headless runner, not
|
||
GUT, and Gitea Actions, not GitHub Actions.
|
||
|
||
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|
||
|
||
## Uncommitted Workstreams
|
||
|
||
- **None (2026-11-01).** The NPC detail pass, the realism follow-ups and the flashlight beam
|
||
decision are committed together with the portrait capture and the refreshed screenshots.
|
||
Keep the rule from `AGENTS.md`: check `git status` before committing, and never revert
|
||
another contributor's in-flight files.
|