assets/images/ is now the runtime root for composed interface/scene art: ui/ for screen and panel art, scenes/ for full-frame scene art. Tiled PBR textures remain under assets/textures/; docs/screens/designs and concepts stay reference-only; QA captures stay under docs/screenshots. Mockups that become runtime art are copied/processed with a new name (no overwrites) and always have a code fallback. Records the convention in MEMORY, adds ADR-0014 to the architecture index, and closes the first storage-restructure TODO item.
48 KiB
MEMORY
Session memory for durable decisions, state, and gotchas. Keep concise, remove obsolete entries.
Decisions
-
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.pybuildsassets/models/generated/npc_base.glb;NpcAvatarkeeps the procedural_build_body()fallback. PreserveRig/Head, the limb node names, and the seed-deterministicoutfit_for/layer_for/hand_item_forhelpers. 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 preinstalledgodot-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 (notclass_nameglobals) so scripts compile in headless--scriptruns 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.
ShiftCoordinatorowns anEventBusinstance (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.
-
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/.gdignoreis required, otherwise Godot's editor tries to import the committed.blendand hangs during--import(also add.gdignoreto local build/squashfs folders when scanning is slow). -
Skinned NPC integration (v0.6.0):
NpcAvatar._build_skinned_body()loadsnpc_base.glband keeps_build_body()as fallback (use_skinned_model=falsein tests). Bone pose rotation is the absolute local orientation, not an offset from rest: compose withget_bone_rest(i).basisor the whole body snaps to a broken pose. Proxies named Hip/Knee/ Shoulder/Elbow/Hand + Head marker preserve the old animation math;Rig/Headis the marker. Five material slots are tinted viaSurfaceMaterials.npc_slot(); keep_rig.scaleuniform 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. UseNpcAvatar.yaw_towards(dir)and setrotation.y; do not calllook_at()on avatars. Head-turn offsets use+basis.zfor the same reason. Covered bytest_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 underassets/textures/;docs/screens/designs/anddocs/screens/concepts/stay reference-only; QA captures stay indocs/screenshots/. When a mockup becomes runtime art, copy/process it intoassets/images/with a new name (no overwrites) and keep a fallback in code.
Handoff (read this first)
-
Visual-alignment pass planned (2026-11-01):
TODO.mdgains a "Visual alignment with rendered mockups" section. Goal: restructure generated-image storage, move the reference mockups out ofdocs/screens/designs/into a runtime-accessible root (e.g.assets/images/), load the proposed UI/scene art at runtime (start menu, selection screens, HUD, panels), and make the game match the mockups — verified byscreen_designs.py compare+judge. Not started; no code moved yet. -
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 generatedefaults toGEN_SIZE=2048, which gpt-5-image-mini rejects (HTTP 400); pass--request-size 1024for 1:1 roles.generate --role tilealready appliesmake_seamless, so a separateseamlesspass is redundant. Added totools/npc_pipeline.py:prepare(layers AI shirt→cloth, trousers→pants albedos at 128px) andpalette(writesassets/textures/npc/palette.jsonviamodel_pipeline.dominant_colors).write_slot_textures()now skips the albedo for slots with an AI override (normal/ORM stay procedural). Blenderbuildre-run: GLB audit ok, 1204 tris / 2488 verts, AI albedo embedded for cloth+pants. Do not commit yet — Phase 1 files are uncommitted; coordinate the commit with the Phase 1/3 agents. Import concurrency: a parallel agent is runningtimeout 300 godot --headless --editor --quit --path .and then checksassets/models/generated/npc_base.glb.import— treat that as the canonical import. My owngodot --headless --path . --importhung 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 --quitis active. Next: Phase 3 (NpcAvatar._load_model+ slot tinting) consumespalette.jsonand 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 rawassets/textures/ai/*outputs (onlyassets/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 authoredassets/textures/*set.tools/asset_pipeline.py generategained--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 (linkedgame_readytextures, generated models, NPC maps), and refreshed dev-status / not-yet lines;docs/architecture.mdLevel 2 now listsnpc_pipeline.py+asset_pipeline.pyand the confirmed-asset locations. Foreign, left uncommitted: a TODO section "Bug Fix NPCs walking backwards" from another agent — do not touch or commit it. -
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). -
mainis 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.glbprops (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.cfggains aWebpreset withvariant/thread_support=false(static hosting, no COOP/COEP);tools/build_releases.shexportsbuild/web/and zips it tobuild/gig-line-web-<ver>.zip, andpublish_release.shuploads it. Covered bysrc/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 oldsrgb_to_linear/_linear_rgbahelpers were removed.audit_glbfails 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 nocapture, so adding staged captures for the wand panel, timeline and escalation HUD would extend automated visual checks.generatenow calls_load_env_file(). -
Staged captures (v0.3.3):
capture_screenshots.gdnow 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 needsrecorder.close_all_open(), becauseto_dict()only lists closed incidents. The escalation HUD mockup is third-person now;judgestill classes itreview(no diff). -
Prop collision (v0.4.0):
_add_prop_model()fits aStaticBody3Dfrom the model bounds (cylinder for bollard/bin/hydrant, box otherwise) and adds aNavigationObstacle3D; NPC agents enable avoidance and move with the safe velocity fromvelocity_computed. The navmesh stays a flat plane, so avoidance (not a baked mesh) routes guests around props. Placements live inPROP_PLACEMENTSwithplacement_problems()validation; variants come fromvariant_for(index).smoke_props.gdmust be windowed because the guard reads captured-mouse input. -
Blender asset library (v0.6.0, integrated 2026-11-01):
tools/blender_assets.py(build/renderin Blender;audit/sheet/liststdlib) writesassets/models/blender/{npc,npc_gear,props}/*.glb,manifest.json(integration source of truth), sources inassets/models/blender_src/(.gdignored), and review sheetsdocs/blender_previews/sheet_*.jpg(per-asset PNG renders are gitignored). 8 characters, 18 gear, 20 props; notes indocs/blender-assets.md. Characters are drop-in fornpc_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.36) +smart_projectat 80°: glTF vertex count includes UV-seam splits, so extra beveled detail quickly breaks 4000. Gotchas:bisect_planeand 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()setsalbedo_color = WHITEon 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_vestkeeps its fixed hivis vest, firefighter/guard get dark uniform tints). Gear is attached at head/back/chest/hand and falls back to primitives;npc_hoodieskips the extra hood gear.WorldBuilderprefersassets/models/blender/props/for the six replacements and the new street dressing, fits the same colliders/obstacles, and collects library emissives forset_night_mode().LIBRARY_TRIANGLE_BUDGET= 60k (current ~27,986). Library props are excluded fromPROP_PLACEMENTSonly when they need dedicated slots (door/marquee/sedan/handrail/lantern). -
v0.6.0 environment pass:
tools/bake_ao.pybakes 256² AO maps per library asset (<name>_ao.png, red channel) andMaterials.apply_ao()/SurfaceMaterialswire them in;tools/make_normal_maps.pyderives 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.gdwrites10-prop-gallery.png(3x2 studio line-up, back row on a platform) and theprop_gallerycatalogue entry compares it. -
NPC character pass (v0.5.0): Rig is articulated (
Rig/Hip* -> Knee,Shoulder -> Elbow -> Hand);outfit_foradds layer/hair_style/hand_item;_idle_layerdrives phone/clipboard/crossed and escalation gestures;_reaction_tagshows!!,?!,!,?;apply_lodswaps to a billboard impostor at 26 m (hysteresis 1.5 m); GameManager_refresh_queueassigns 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); nowforward * -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 byGuardController._ensure_gamepad_bindings; remaps persist viaSaveinput_bindingsand are applied byInputBindings.apply(key/button events only). Windowedsmoke_movement.gdguards 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.gduses oneaccelvalue. 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.mdnow 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 toW. Do not remove the radio easter egg.
Current State
-
v0.3.0 pass (2026-10-31, uncommitted): 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_LIGHTINGgainedexposure/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 != 2lit), stone base course, door trim/handles, curb, lane dashes (the old loop hadVector3(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.pybuildsassets/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 recordsloaded_models/fallback_props. Gotcha: glTFbaseColorFactoris LINEAR — writing sRGB made every prop pastel;srgb_to_linear()is applied inbuild_glb. Gotcha: a new/changed.glbneedsgodot --headless --path . --importbeforeResourceLoader.exists()/load()sees it (test_generated_props fails otherwise).model_pipeline.py concept(OpenRouter, reads repo.env) writes per-prop concept sheets toassets/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;_armsand_legsnow swing (limb_swing) while walking. KeepRig/Headnamed for focus. - Selection:
NpcFocus.rank_indices(angle then distance) —pick_index= first.GuardController.cycle_target(direction)walks nearest-firstnearby_candidates()and setstarget_pinned, which makes_update_target()skip the cone.clear_target()setsfocus_suppress_s = 0.75so the cone does not instantly re-acquire the person. New input actionswalk_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)showhud.nearby;hud.controlsandhud.targetingmention 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), newlistcommand,generate --only/--force(existing designs are kept by default so a draft run cannot clobber AI mockups), andjudge --dry-runno longer raisesKeyError: 'class'.- Screenshot tool leak:
capture_screenshots.gdintermittently printsObjectDB instances leaked at exit. Verified against agit worktreeof 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.gdbuilds from eventoverride_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 usescontroller.screened. Joke Shop/Studio gainedsidewalk/steps_endwaypoints. Wand venues rollcarried_itemsper spawn (seeded). Tests insrc/test/tests/test_access_loadout.gd. -
Wand shortcut is
C, notW(Wis walk).radio_emergencyinput map no longer includes W.hud.controls/hud.screen_firstupdated. -
Control pass (in tree, uncommitted, smoke-tested): cone focus via
src/game/npc_focus.gd, accel/decel, gravity only airborne,input_lockedfreeze, Esc no longer on the guard.NpcAvatarin groupnpc; neck/hair/eyes/shoes; blue focus circle +Label3Dname;set_focus_visual.src/game/ui/action_radial.gddoughnut (LMB/Space open, A/D/arrows, Space/click confirm, Esc/RMB cancel). Crosshair CanvasLayer. Emergency stillRonly. 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 unusedinteractaction was removed. -
Runtime fixes from the smoke:
Headis +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;ShiftCoordinatorpulls the event scheduler once per simulated second (was every physics frame → >1200 NPCs in seconds);GameManager.MAX_LIVE_NPCS = 16caps live avatars. -
Session shell (2026-10-31, uncommitted): the game now 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_runninggates input and_refresh_freeze(). Esc with nothing open now pauses (PauseOverlay, layer 18) → resume / restart / settings / quit-to-menu.SettingsPanel(layer 22) writeslook_sensitivity+hud_font_scalethroughSave.save_settings()→user://settings.json;Save.load_settings()merges overSave.DEFAULT_SETTINGSon boot andGameManager._apply_settings()pushes them into the guard/HUD. Shift review "Run another shift" now calls_show_start_menu()(not a silent restart).Vstays 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 anTRANSPARENCY_ALPHA_SCISSORunshaded material (_skyline_mat); steps + landing are aStaticBody3Dwith per-treadBoxShape3Dcolliders; marqueefont_size160 / outline 24; focus circle (blue disc in front of the face) + name tag 48px;NpcAvatar.ephemeral/lifetime_sdespawns routine traffic (authored events stay). Guard spawn height verified on the landing (guard_y=0.84). -
PLAN verbs (2026-10-31, uncommitted): the radial now has 12 slices (
Stop, Look, Point, No, Ask, Inspect, Wand, Warn, Block, Reason, Recognize, Radio).eye_contactreuses the Level 1 reaction table via_apply_interaction. Recognition is a player decision:NPCController.player_recognized+IncidentRecorder.add_access_evidence, andGameManager._artifacts_for()prepends aRECOGNITIONartifact only after the player declares it. The body camera is a real flag (GameManager.body_camera_on,Hud.set_body_camera), off by default,Btoggles it, and a formal warning arms it and writes abody_cameraincident 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 seededdialogue_linespick. -
NPC navigation was dead (fixed 2026-10-31): avatars never moved in the live game, for three stacked reasons. (1)
agent.target_positionwas re-assigned every physics frame, which invalidates the path soget_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_directfallback is still there. -
Crowd reactions listen on the EventBus:
CrowdSystem.apply_event()emitsevent_bus.crowd_changed; its ownchangedsignal is never emitted.GameManager._connect_crowd()therefore connects tocoordinator.event_bus.crowd_changed(reconnect after everycoordinator.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) andstart_flee(from)(panic: walk away at 2.4 m/s, then free themselves). Someoneawaiting_interactionwithin 3 m of the door is "engaged" and does not rubberneck. Useposition(notglobal_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 everyadd_event/add_access_evidencesilently 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(), andShiftCoordinator._finish_shift()closes leftovers asunresolvedbefore building the review. -
Editor-ready venues (2026-10-31):
tools/export_venues.gd(a SceneTree script) saves the three factory scenarios tocontent/venues/<id>.treswithResourceSaver; the.tresfiles round-trip throughResourceLoader(events, profiles, policy, shift phases, door lines, locations).test_venue_resources.gdalso fails if the exported files are missing or stale relative to the factory, so re-run the export after changingScenarioFactory. -
Vision judge (2026-10-31):
python3 tools/screen_designs.py judgesends 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-minialso works). Reads the repo.envvia_load_env_file()(never overrides real env). Verdicts are classedmatch/review(mismatch below--fail-confidence, default 0.6) /diff(confident mismatch → exit 1) /unknown.--dry-run,--json,--onlysupported. 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), andvalidate()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_npcwithkind: routine), andrefuses_screening(copied toNPCController.refuses_screening;_toggle_wand_screeninglogswand.refused, records awand_refusedevent, applies a small prop hit, and does not open the panel).film_boundaryis a new intent that maps tosteps_endso the influencer stands past the posted line. -
Accessibility (2026-10-31): escalation text is
"<name> [###--] L3/5"viaHud.escalation_meter(level)(pure, tested) so the cue survives grayscale. HUD font scale usesadd_theme_font_size_overrideon the core labels (set_font_scale), notControl.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 loopingAudioStreamWAV(one-pole-filtered noise + 68/103 Hz drone, seeded) andset_crowd_level(mood)maps 0..1 to -34..-12 dB.play_radio_open/closepush short filtered-noise bursts. All procedural; with a dummy (headless) audio server the calls are no-ops._free_player()returns null when_readyhas not run (tests), and callers handle null. -
NPC theater (2026-10-31):
NpcAvatar.theater_pose()mapsintent+arrivedto idle/sit/photo/film/perform. Pure helperscrouch_for_pose,prop_lift_for_pose,sway_for_posekeep the math testable. Sit lowers the rig by 0.34 and swings the legs forward (negative X pitch; positive looked like kneeling)._photo_propis set for influencer/tourist cameras,_sign_propfor 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.gdflattensreview.incidents.incidents[].eventsinto one time-ordered list (flatten), formats each row withdescribe, and steps with Left/Right or the buttons.ShiftCoordinator.started_at_msgives the review an absolute start for mm:ss stamps.GameManager._open_timelinehides the review dim (two stacked overlays made the screen nearly black) and_timeline_from_reviewrestores it on close. Smoke:src/test/smoke_timeline.gd. -
Radio dispatch was dead after the first shift:
coordinator.setup()builds a newRadioSystem, so theresponder_arrivedconnection made in_readywas 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/investigationsfeed the review.evacuation(emergency call, low danger, no weapon) locks the door, makes every NPC flee, marks themleave_requested, and applies asecurity_failurehit. An emergency placed whilecurrent_npc_truthhas no weapon anddanger_level <= 1appends an investigation note, recordsinvestigation_opened, and appliesfalse_report-6. -
Skyline backdrop (2026-10-31):
PlaneMeshdefaults to a horizontal XZ plane — setmesh.orientation = PlaneMesh.FACE_Zor 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_FILTERdefault,TRANSPARENCY_ALPHA_SCISSORwith 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. Theskyline.pngimport needsmipmaps/generate=true. -
Asset gotcha:
assets/textures/game_ready/is not imported (no.import). Use the importedassets/textures/skyline.pngpath. Untracked duplicate folder should not be referenced. -
Host sign-in follow-up is done:
REQUIRE_SIGNIN_VIEW = falsein/etc/gitea/app.ini(backupapp.ini.bak-*alongside), Gitea restarted; anonymous repo page/API return 200 and release assets download in full. Reverse by setting it back totrueand restarting. -
Release publishing was broken since v0.2.2 (fixed 2026-10-31):
publish_release.shonly builtAUTHin theGITEA_TOKENbranch; the~/.git-credentialsbranch setCREDand then used unbound${AUTH[@]}underset -u, so every API call failed and the tag job ended "could not create or find release". The repo also has noGITEA_TOKENsecret (/actions/secretsis empty) and the runner has no git credentials. Fixes: the script setsAUTH=(-u "$CRED"); the workflow falls back togithub.token(secrets.GITEA_TOKEN || github.token) — butgithub.tokenis 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.shfalls back to the local git extraheader thatactions/checkoutwrites (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.30now works from this workstation (key auth), host namegitea, Gitea 28.2.0. Maintenance applied:apt-get clean, journald capped toSystemMaxUse=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 localbuild/artifacts and verified (4 and 5 assets).git.allucanget.bizruns on this host as userzwitschi; root@192.168.88.30 is not reachable from this workstation (publickey denied; password auth closes the banner), so host maintenance must go through azwitschilogin 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 pushthen 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 aszwitschi;sudo -nneeds 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
-Ddefines (never/D), resolves relative-DICON/-DARTIFACTagainst the script directory so CI passes$PWD-absolute paths; installed exe must be space-free (gig-line.exe); ICO must be classic BMP entries (ImageMagickicon:auto-resize=48,32,16; PIL emits PNG-compressed ICOs NSIS cannot load). -
appimagetool runs system
appstreamcliif 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.1has all 5 assets; the installers were initially missing from the release becausepublish_release.shuploaded only portables — now fixed (uploads installer + AppImage; falls back to~/.git-credentialswhen noGITEA_TOKEN). -
Host sign-in: instance
[service] REQUIRE_SIGNIN_VIEW=trueblocks anonymous browse/download (not a repo/API setting).~/.git-credentialsholds the Gitea creds (basic-auth usable);.envonly holdsOPENROUTER_API_KEY. -
Release v0.2.0 was LIVE: pushing the
v0.2.0tag made Gitea Actions run the build job and publish automatically (host has theGITEA_TOKENsecret). Tag page and/releasesare public (HTTP 200). Caveat: anonymous asset downloads redirect to login on this host (host setting — tracked in TODO). Future releases = bumpconfig/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.setuptakes an explicitdoor_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 toLOITERING_DISTANT;tick_return(delta)fires after 20-60s and transitions back toAPPROACH.NpcAvatar._advance_fsmre-targets the door on return. -
Perception pass:
NPCController.perceive_guard(distance)returns{noticed, band, reaction}with bandsunaware(>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:
RadioSystemtrackspending_callsand decrementsarrival_delay_sintick; emitsresponder_arrivedwhen a call's delay elapses.RadioSystem.responder_outcome(call)maps danger/weapons → command/restraint/evacuation/arrest.GameManager._on_responder_arrivedspawns aRESPONDERavatar at the entrance and logs the outcome. HUD shows an emergency-radio cooldown label via_update_radio_cooldown. -
Wand screening (Joke Shop):
WandScreenerclassifies carried items as prohibited vs false-alarm vs clear.NpcProfile.carried_itemsis filled at spawn on wand venues.GameManagerbindsCto_toggle_wand_screeningandA/Dtodecide_wandwhile inspect/wand is open.decide_accessstill blocks admission untilcontroller.screenedis true, andverifynow 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 onNPCController.roll_reaction/apply_player_actionno longer exist;NpcProfile.dialogue_linesis a flatArray[String];GameManageruses_apply_interaction(level, action_name); theTkey is gone. HUDtone_labelremoved andhud.tone_prefixstring deleted from JSON. -
Polish/accessibility: venue cycling (
VcyclesVENUE_ORDERand restarts the shift), aim-free targeting (Tabcycles nearby NPCs withinNEARBY_RADIUS), color-coded escalation indicator (Hud.set_escalation), procedural audio (AudioFeedbacknode generates sine tones for confirm/radio/escalation/deny), and a centralizedStringstable (src/core/strings.gd,lookup/fmt) backed bycontent/strings.en.json. NOTE:Strings.getwas renamed toStrings.lookupbecausegetcollides withObject.get. -
Newly added: LATE shift phase (start 700s) with a reachability test; PROTESTER archetype + events + colors; CALL_SOMEONE reaction;
DoorStatecore class +Lbinding + visual indicator; flashlight (G, SpotLight3D, perception bonus at evening/late); persistence wired (Save.save_shift_resulton shift end, history + best score on the review screen);NavigationRegion3Dflat navmesh +NavigationAgent3DNPC paths (steps now visual-only); strings moved tores://content/strings.en.json(41 keys, loader keepslookup/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 touser://screenshot.png);godot --path . -s res://src/test/capture_screenshots.gd(staged README gallery shots touser://screenshots/, copied todocs/screenshots/). -
Release build verified locally: export templates for 4.3 installed at
~/.local/share/godot/export_templates/4.3.stable/;./tools/build_releases.shproduced Linux/Windows/macOS artifacts for 0.1.0 inbuild/and the Linux binary passed a headless smoke test. Publishing to Gitea still needs a token (seetools/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 andworkflow_dispatch). A repo secretGITEA_TOKENis required for tag releases. The old.github/workflows/ci.ymlwas removed so hosts scanning both paths do not double-run. -
Venue cycling:
Vswitches 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.ymlno longer usesactions/cache(the runner can't reach the GitHub cache service; import is fast).GODOT_SILENCE_ROOT_WARNING=1set 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.pyproduces all authored PNGs underassets/textures/(≤1024px, pow2); regenerate after changing the script, then rungodot --headless --importbefore tests. 57 headless tests guard the pipeline. -
General image pipeline:
python3 tools/asset_pipeline.pydefaults toopenai/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 fromrequirements-tools.txt. -
AI texture mode:
python3 tools/generate_textures.py --aigenerates textures via the OpenRouter image API (POST https://openrouter.ai/api/v1/images, modelbytedance-seed/seedream-4.5by default, base64 PNG indata[].b64_json). RequiresOPENROUTER_API_KEY(a gitignored.envin the repo root holds it locally; source it withset -a && . ./.env && set +a).--prompt/--name/--model/--sizeoverride the default batch. Output is downscaled to ≤1024px. NOTE: seedream rejects sizes below 3,686,400 px (1920×1920), so the script requestsGEN_SIZE=2048and downscales — do not lowerGEN_SIZEbelow 1920. Verified end-to-end 2026-10-31 (3 textures generated). Legacy--airemains on Seedream 4.5 and still ignoresbackground: 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 sixdocs/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.rechargemessage). -
NOTE: collaborators occasionally add uncommitted work to this shared tree (e.g. the radio easter egg); check
git statusbefore committing and leave foreign changes untouched unless asked. -
Screen-design references (
tools/screen_designs.py, tests intools/test_screen_designs.py):generateturns the screen descriptions indocs/(README captions, ADRs 0007–0010, PLAN, architecture) into reference images underdocs/screens/designs/—--draftdraws offline Pillow wireframes,--aigenerates realistic 3D scenes / in-game UI via OpenRouter (default modelopenai/gpt-5-image-mini). Screens are taggedsceneorui; prompts get a shared art-direction suffix, and requests use a 3:2 landscape source cropped to 16:9 (not a square centre-crop).comparechecksdocs/screenshots/*.pngfor 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:
FSMholds its owner viaWeakRef,ShiftCoordinatorisRefCounted, and persistence tests free theSavenode — 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:
GameManageruses the scenario'sshift_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=trueinproject.godot(universal build demands ETC2 ASTC enabled). - Joke Shop
door_staff_linesare logged throughGameManager(inspect / wand / refuse). They are not a separate dialogue UI. - Access loadout is implemented in the working tree (see Current State); do not re-fix
override_accessas if it were still unread. Save.SETTINGS_FILEis declared and unused; no mouse-sensitivity or pause menu yet.- Headless suite is 77/77; the windowed focus/radial smoke passes. Remaining visual follow-ups are at the top of
TODO.md. - Typed spawn signal extra args: handlers must accept the 4th
event: Dictionaryor Godot will not call them (test_shift_coordinator_runs_a_shiftfailed until_on_spawntook four args). - GDScript inference trap that broke the main scene: a
vartyped as a base class (radial: CanvasLayer) exposes script members asVariant, sovar x := radial.is_openis a parse error. Type it as the script type (ActionRadial) or annotate the local explicitly.--check-only --scriptcannot see theSaveautoload; 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.gdneeds a state/frame gap or the capture uses the old camera orientation. EventSchedulerprobabilities 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 fromuser://screenshots/.
Open Questions
- Add a vision-model (LLM) judge on top of the screen-design comparison: pixel and perceptual metrics cannot decide whether a screenshot's composition is correct.
- 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 (re-evaluated)
On local main (6 commits not on origin): v0.2.1 packages, ADRs 0007–0010, asset pipeline wired to
assets/textures/ai/game_ready/, screenshots refreshed.
Uncommitted: AccessLoadout, control pass (cone, ring, radial, NPC face, named input actions),
and boot/spawn fixes. Headless 77/77; windowed smoke passes. Access-artifact and control-pass
sections moved to DONE.md.
Still in TODO.md: world follow-ups (guard float, step colliders, skyline asset, marquee size);
session shell; leftover PLAN verbs; crowd rubberneck; emergency aftermath; body-cam replay;
sit/photo/film poses; real crowd audio; Joke Shop/Studio extra events; PLAN.md says GUT/GitHub.