Files
security-game/DONE.md
T
zwitschi 26d64baa01
Full build / Import + headless tests (push) Successful in 34s
Full build / Full desktop build + optional release (push) Skipped
Close the NPC gear-fit section
Move the completed gear section to DONE.md and record the hooded-portrait
follow-up. An explicit --portrait-type now overrides the hooded-subject skip,
though the day roster has no types 5-7 yet. Headless 229/229.
2026-11-01 18:12:37 +01:00

76 KiB
Raw Permalink Blame History

DONE

Completed task sections moved from TODO.md. Sections are moved only when every item is finished and verified.

Project Setup

  • Confirm Godot 4.x editor version to standardize on (Godot 4.3 stable, official build).
  • Initialize a git repository and add a sensible root .gitignore for Godot and export artifacts.
  • Scaffold the Godot project (project.godot, input map, folder structure).
  • Add the GUT test framework and a runnable headless test command. (Deviation: GUT requires the editor plugin; replaced with a lightweight custom harness in src/test/test_runner.gd that runs without the editor.)
  • Document setup, run, and test commands in README.md.

Prototype Scene

  • Build a placeholder Hollywood Boulevard building entrance with steps, sidewalk, and a restricted door (procedural WorldBuilder).
  • Add a controllable guard character with first-person camera controls (GuardController).
  • Add basic keyboard/mouse input: move, look, interact, and cancel.
  • Add a debug overlay showing NPC intent, escalation level, and shift timer (F toggles).

Core Simulation — Systems

  • Implement ShiftCoordinator to start, advance, and end a shift.
  • Implement VenueScenario as a Resource with locations, rules, NPC pools, and event pools.
  • Implement an event scheduler that emits routine traffic and occasional scripted events (EventScheduler).
  • Implement NPCController with movement toward waypoints, doors, and queues (movement handled by NpcAvatar).

NPC Interaction

  • Define NpcProfile resources with intent, temperament, compliance, and escalation tolerances.
  • Implement a lightweight finite state machine for NPC states (FSM).
  • Implement the observation phase (Level 0) without forced intervention.
  • Implement simple deterrence (Level 1): raise hand, point, eye contact, and head shake.
  • Implement polite dialogue prompts and reaction logic driven by escalation level and profile.
  • Implement formal warnings (Level 2) and make NPC reactions depend on profile and level.
    • Note (2026-10-31): the selectable dialogue tone-of-voice system (Professional/Friendly/Firm/ Suspicious) was intentionally removed from code and docs; reactions now depend solely on escalation level and NPC profile.
  • Implement blocking and physical positioning (Level 3) without default force.
  • Wire typical NPC reactions: leave, argue, ignore, record, move closer, or return later.

Access Verification

  • Implement AccessVerifier with input from recognition, wristbands, IDs, tickets, credentials, and guest lists.
  • Add wristband inspection UI showing color, pattern, date, and event marking.
  • Model valid, expired, counterfeit, mismatched, missing, and ambiguous access artifacts.
  • Record admit, redirect, hold, and refuse decisions for shift review.
  • Add feedback scoring for incorrectly denying an authorized person or admitting an unauthorized one.

Escalation and Radio — Core

  • Implement EscalationManager with explicit levels 0 through 5 and available-action gating.
  • Implement the security-assistance radio call (Level 4) with a report form and arrival delay.
  • Implement the emergency radio (Level 5) with hold-to-confirm and a report form.
  • Add radio cooldown and battery limits to prevent spam.

Incident, Reputation, and Shift Review

  • Implement IncidentRecorder emitting structured events (actor, behavior, action, evidence, escalation, outcome).
  • Implement body-camera evidence tied to incident events.
  • Implement ReputationEvaluator for peaceful resolutions, accuracy, complaints, damage, and entrance security.
  • Implement CrowdSystem reactions to calm, aggressive, armed, and emergency situations.
  • Implement the end-of-shift review screen summarizing decisions, consequences, and justification.

Scenario Content — Venue Data

  • Author a minimal Hollywood Boulevard building scenario with tourist, confused-visitor, and simple-trespasser NPCs.
  • Author a rare violent-threat event that requires justified emergency escalation.
  • Add The Joke Shop scenario data: ID checks, guest lists, and wand-screening policy.
  • Add the Hollywood Boulevard Late Night TV Studio scenario data with the full NPC mix and credential checks.
  • Add a mental-health-crisis event with careful, respectful de-escalation guidance.

Tooling and Quality

  • Add headless test coverage for access verification rules.
  • Add headless test coverage for escalation transitions and scoring.
  • Add fixed-seed scenario tests for deterministic NPC behavior.
  • Set up GitHub Actions to run headless tests on push (ci.yml).
  • Add a smoke-test checklist for launching one full shift (in README.md).

Bug Fixes — LATE shift phase

  • Add a Rules.Phase.LATE entry (start_s 700) to shift_phases in all three venues.
  • Confirm the LATE-tagged violent_emergency_event is now pulled from EventScheduler.pull_due.
  • Add a test asserting the LATE phase is reached and the event is eligible in phase.

Missing PLAN Features — Protester NPC

  • Add PROTESTER to Rules.NpcType and NPC_TYPE_NAMES.
  • Add a protester profile (id "protester") with ARGUE/IGNORE/RECORD/CALL_SOMEONE reactions.
  • Add a TYPE_COLORS entry for PROTESTER in npc_avatar.gd.
  • Add routine protester events to the Hollywood Boulevard and TV Studio event pools.
  • Add scenario tests asserting the protester profile and events exist.

Missing PLAN Features — “Call someone else” reaction

  • Add CALL_SOMEONE to Rules.Reaction.
  • Handle CALL_SOMEONE in NPCController._update_state_for_reaction (transitions to ARGUING).
  • Give influencer, trespasser, and protester profiles CALL_SOMEONE weights.
  • Add a reaction log line in GameManager and a Strings entry.
  • Add a test that CALL_SOMEONE can be rolled and updates the FSM.

Missing PLAN Features — Door lock/secure

  • Add a DoorState core class with locked/unlocked state and attempt_force().
  • Add a visible door lock indicator (green open / red locked) to the world.
  • Add the L key binding toggling the door lock, with HUD indication.
  • Make FORCE_ENTRY reactions fail while the door is locked (thwarted outcome).
  • Add Strings entries and a headless test for the lock state machine.

Missing PLAN Features — Flashlight

  • Add a G key binding toggling the guard's flashlight.
  • Attach a SpotLight3D to the guard that toggles with the flashlight.
  • Widen NPC perception radius while lit at evening/late phases (perceive_guard bonus).
  • Add Strings entries and a headless test for the perception effect.

Persistence

  • Call Save.save_shift_result(review) from GameManager._on_shift_ended.
  • Load past results and show record count + best score on the shift review screen.
  • Add a test that a saved review round-trips through the Save helper.

Navigation

  • Add a NavigationRegion3D with a hand-authored flat navmesh covering sidewalk, steps, and entrance.
  • Switch NpcAvatar from direct movement to NavigationAgent3D path following (with direct-movement fallback).
  • Keep per-intent waypoint and returning-trespasser re-targeting on the navmesh.
  • Make the step geometry visual-only (no collider) so pathing to the entrance works.

Localization — JSON strings

  • Move the string table to res://content/strings.en.json (41 keys).
  • Load the JSON at startup; Strings.lookup/Strings.fmt signatures unchanged.
  • Update the strings tests to confirm every JSON key resolves to a real value.

Distribution — Downloadable executables

  • Commit export_presets.cfg with Linux, Windows, and macOS desktop presets.
  • Add tools/build_releases.sh for headless release exports.
  • Add docs/RELEASING.md with template installation, build, and Gitea release steps.
  • Add a prominent Downloads section at the top of README.md linking to the release page.
  • Add tools/publish_release.sh (Gitea API release + asset upload, requires GITEA_TOKEN).

CI/CD — Gitea Actions full build

  • Add .gitea/workflows/build.yml with a fast checks job (import + headless tests on push/PR) and a full build job (export templates, Linux/Windows/macOS exports, artifact upload) on tags and workflow_dispatch.
  • Tag builds automatically publish the Gitea release via tools/publish_release.sh using the GITEA_TOKEN secret.
  • Remove the old .github/workflows/ci.yml to avoid double runs on hosts scanning both paths.
  • Document the CI flow in docs/RELEASING.md.

Gameplay — Venue selection

  • Add a V control that cycles Hollywood Blvd → The Joke Shop → TV Studio and restarts the shift (GameManager._cycle_venue), with a hud.venue_switched string entry.

🎨 Visual polish pass (v0.2.0, pre-release graphics)

  • Phase-based lighting: per-phase sun direction/energy/color, sky gradient (ProceduralSkyMaterial), and ambient in GameManager.PHASE_LIGHTING; applied on phase change and shift start.
  • Street lamps: poles with emissive bulbs + OmniLights on the sidewalk; WorldBuilder.set_night_mode switches bulbs/lights on at night.
  • Marquee signage: emissive “GIG LINE” strips + Label3D above the entrance.
  • Street dressing: crosswalk stripes, planters, and low-poly trees.
  • NPC visual variety: per-seed height/brightness variation (deterministic LCG).
  • Denser routine traffic (tourists/performers/influencers/trespassers).
  • capture_screenshots.gd now fast-forwards to the LATE phase and captures a night set (lamps + marquee + flashlight cone); README gallery is now 2×3 (day + night); six shots regenerated in docs/screenshots/.
  • config/version → 0.2.0; build/ artifacts rebuilt and Linux binary smoke-tested.
  • New world/lighting headless tests (56 total).

Screenshot Refresh

  • Regenerate and verify the six README screenshots from the current build.

🔴 PRIORITY — Ship downloadable builds (complete 2026-10-31)

  • Install Godot 4.3 export templates locally.
  • ./tools/build_releases.sh produces Linux/Windows/macOS artifacts (0.2.0).
  • Linux binary smoke-tested (headless, 0 script errors).
  • Publish the release: no local token needed — the host has the GITEA_TOKEN secret, so pushing the v0.2.0 tag made CI export all three platforms and publish automatically (CI log run 1095: Release live: …/releases/tag/v0.2.0, Job succeeded).
  • Confirm links resolve: tag page and /releases return HTTP 200 publicly. Follow-up: anonymous asset downloads require sign-in on this host (host setting).
  • CI packaging follow-up was already live (.gitea/workflows/build.yml on tags).

🎨 AI texture generation (OpenRouter)

  • Extend tools/generate_textures.py with an OpenRouter image-API mode (--ai, --prompt, --name, --model, --size), decoding base64 PNG responses and clamping to ≤1024px.
  • Verify a live --ai generation against a real OPENROUTER_API_KEY (generated ai/… set at 1024×1024; seedream minimum-pixel by requesting 2048 and downscaling.

🧹 Runtime hygiene + timeline/polish items

  • RefCounted cycle cleanup: FSM now holds its owner via WeakRef; ShiftCoordinator is RefCounted (logic-only, no tree needs); persistence tests free the Save node. Test suite now exits with zero “resources still in use” warnings.
  • Full-length shifts: the prototype now uses each scenario's shift_duration_s (900s ≈ 10-15 min per PLAN.md) instead of a hardcoded 300s test duration.
  • Wand cooldown feel: a short WAND_COOLDOWN_S recharge between wand scans with a wand.recharge message (Open Polish item).

📦 Installer packaging (Windows NSIS + Linux AppImage)

  • Export presets: binary_format/embed_pck=true on all platforms — every binary is now self-contained (all game data in one file; .pck no longer shipped).
  • App icon: assets/textures/icon.png (1024) + multi-size icon.ico, copied to packaging/windows/gig-line.ico and packaging/linux/gig-line.png via the generator.
  • NSIS script packaging/windows/gig-line.nsi (installs Gig Line.exe, uninstaller, shortcuts, registry entry; makensis /DVERSION=… /DARTIFACT=…).
  • AppImage: packaging/linux/gig-line.desktop, AppRun, appdata.xml, and build_appimage.sh (appimagetool, APPIMAGE_EXTRACT_AND_RUN=1).
  • Local end-to-end: rebuilt embedded exports and produced gig-line-0.2.0-x86_64.AppImage, ran it headless (0 script errors).
  • CI: NSIS + AppImage steps added to .gitea/workflows/build.yml build job; publish glob already uploads build/gig-line-*<ver>* (covers setup/AppImage names).
  • CI run 1110 verified end-to-end (v0.2.1): NSIS installer gig-line-setup-0.2.1.exe built, 7z l shows gig-line.exe + uninstall.exe, AppImage built (33.9 MB), release v0.2.1 published, Job succeeded.
    • Gotchas fixed on the way: makensis needs -D defines (not /D) and absolute paths (it resolves relatives against the script dir); installed exe must be space-free (gig-line.exe); icon.ico must use classic BMP entries (ImageMagick; PIL emits PNG-compressed ICOs NSIS rejects).

🎮 Assets & detailed graphics pass (textures, rigs, UI feedback)

  • Authored texture set via tools/generate_textures.py (deterministic PIL): brick facade, seamless concrete, asphalt, awning, skyline, vignette, HUD icons, access-card art — all ≤1024px and power-of-two; imported under assets/textures/.
  • Environment: textured facade + glass door, concrete sidewalk, asphalt street, striped awning with “GIG LINE”, generated skyline backdrop planes, soft sun shadows.
  • NPCs: humanoid rig (torso/head/limbs) with archetype props (camera, delivery box, hat, sign, backpack), procedural walk bob, head-turns-toward-guard cue.
  • UI/feedback: inspection artifact cards, mode icons (flashlight/door/wand), escalation vignette, body-camera overlay (corner brackets + REC + shift clock).
  • Assets convention documented in README; test_asset_textures_present_and_pow2 guards the pipeline (57 tests total).
  • (remaining, in TODO) crowd performance profiling and LOD/culling when the crowd grows.

CI — cache timeout mitigation

  • Remove actions/cache@v4 from the checks job (runner cannot reach GitHub's cache service; ~2 min of retry timeouts per run; Godot import is fast without it).
  • Add GODOT_SILENCE_ROOT_WARNING=1 to both jobs.
  • Confirmed in run 1090: zero CacheService lines, 54/54 tests pass, job succeeds.
  • Document the cache situation and re-enable path in docs/RELEASING.md.
  • Ignore runner logs in .gitignore (.gitea/logs/).

Gameplay — Venue door-staff dialogue

  • Add VenueScenario.door_staff_line(context, rng) picking a seeded-random line from door_staff_lines (empty for unknown contexts/venues).
  • Wire GameManager to log venue lines: id/ticket check line when inspection opens, wand-check line when screening starts, prohibited-item line on correct wand refusal, and refusal line on denied entry (The Joke Shop flavor only).
  • Add a headless test covering all Joke Shop contexts, unknown contexts, empty venues, and deterministic seeded picks.

Core Simulation — NPC Perception

  • Implement a perception pass so NPCs react to the guard's gestures and proximity.
  • Give NPCs local waypoints per intent (photo spots, queue, stage door, sidewalk exit) instead of all walking to the entrance.
  • Add returning-trespasser behavior: an NPC that was directed away may circle back later in the shift.

Escalation and Radio — Responder Dispatch

  • Spawn a dispatched security NPC / armed responder avatar that arrives at the venue after the call delay.
  • Give the responder command, restraint, evacuation, and arrest outcomes that resolve the incident.
  • Add a visible emergency-radio cooldown indicator in the HUD.

Scenario Content — Wand Screening (The Joke Shop)

  • Implement the handheld wand inspection interaction with alerts from keys, belts, phones, and weapons.
  • Enforce screening-before-entry flow and record false alarm / prohibited item decisions.
  • Add venue-specific dialogue lines for door staff.

Polish and Accessibility

  • Add aim-free targeting (Tab) and venue cycling (V). (The earlier dialogue tone picker was superseded and removed with the tone-of-voice system.)
  • Add interaction alternatives that do not require precise aiming (e.g. scan-and-select pinned list of nearby NPCs).
  • Add visual and audio feedback per escalation level (colors, tones, shake).
  • Add audio for crowd ambience, radio responses, and confirmation prompts.
  • Externalize dialogue and UI text for localization.

Generalized Game Asset Tooling

  • Reuse the OpenRouter image backend for role-aware arbitrary asset generation.
  • Add exact-dimension image crop and aspect-preserving resize commands.
  • Audit existing assets for role-specific dimensions, aspect, alpha, and tile seams; report projection/perspective checks for visual review.
  • Add automated tests and document asset-pipeline usage.

Generated Asset Integration

  • Regenerate the AI asset set at role-specific aspect ratios and dimensions.
  • Audit, process, and wire every generated asset into the game runtime.
  • Add screenshot auditing and refresh the README screenshot gallery.

Access artifacts actually match the venue

Completed in the working tree (uncommitted with the control pass). Headless 74/74.

  • Build artifacts from the event (override_access, carried items) and the venue policy, not from NpcType alone. (src/core/access_loadout.gd; spawn signal is (profile, intent, npc_id, event).)
  • Joke Shop: valid ID+ticket, ID without ticket, expired/missing ID. Recognition does not auto-grant on every AUTHORIZED_GUEST.
  • Wand venues roll carried_items per spawn (seeded); override can force a pocket.
  • Studio counterfeit / follow-through events carry override_access.
  • AccessVerifier.verify(..., already_screened) so a screened guest is not still denied.
  • Joke Shop / Studio sidewalk and steps_end waypoints so film/photo intents do not walk to the entrance.
  • Wand shortcut is C, not W. Tests: src/test/tests/test_access_loadout.gd.

Player control, focus circle, and radial action menu

Completed 2026-10-31 with ADR-0007–0010. Headless 77/77, plus a windowed smoke (src/test/smoke_focus_radial.gd) that opens the wheel and saves screenshots.

Locomotion

  • Wand off W (now C + wheel). A/D allow/deny only while inspect/wand is open (they step the wheel while it is open). Q remains a closed-menu polite shortcut.
  • Accel/decel on walk; gravity only when airborne; floor_snap_length 0.3.
  • Mouse look only while captured and not input_locked. Freeze walk+look while radial / inspect / wand / radio / review is up. Recapture on close.
  • cancel (Esc / RMB) closes the top modal first; with nothing open, Esc toggles mouse capture and RMB does nothing.

Focus and focus circle

  • Focus cone in src/game/npc_focus.gd (~10 m, ~10°). Closest-to-forward wins; occlusion ray skips building hits. Tab still cycles 12 m.
  • 8-frame miss grace so walk-bob does not drop focus.
  • Billboarded blue circle in front of the face on Rig/Head; brighter while the radial is open; Label3D display_name. HUD target line uses the display name, not the node name.
  • Screen-center crosshair.

Radial HUD

  • src/game/ui/action_radial.gd: LMB or Space opens when someone is focused. Opening click ignored for 120 ms. Slices: raise hand, point, head shake, polite, inspect, wand (grey if no wand), warning, block, radio assist. Emergency stays on R.
  • Highlight by mouse angle, or A/D / arrows. Click slice or Space confirms. Esc / RMB closes with no action (no recorder event).
  • Inspect / wand / radio slices close the wheel then open the existing panels. Freeze until that panel closes.
  • Number keys 1–6 still work when the wheel is closed.

NPC display

  • Procedural rig: neck, shoes, hair cap from seed, two eye spheres. Props, height variation, walk bob, notice-head-turn kept. Collision capsule scaled to rig height. Named Head is the ring attach point. Group npc.

Input, copy, tests

  • Named input map: action_menu (Space/LMB), confirm (Space/Enter), cancel (Esc/RMB), inspect (E), wand (C). GameManager uses is_action_pressed.
  • Windowed smoke: focus circle + name, wheel open/step/confirm/cancel (src/test/smoke_focus_radial.gd, exits non-zero on failure).
  • README controls table, smoke checklist, test count, implemented/not-yet lists.

Visual polish — Skyline backdrop transparency

Found during the control smoke test; verified with PIL analysis.

  • Regenerated assets/textures/game_ready/skyline.png with transparent background (commented out opaque border line 234 in tools/generate_textures.py).
  • Integrated backdrop into WorldBuilder via _build_skyline_backdrop(): creates a PlaneMesh with T_SKYLINE texture at Vector3(-80, 12, -50) behind the building.
  • PIL verification confirms RGBA mode with transparent pixels in bottom rows.

Runtime boot and spawn fixes

Found while running the windowed smoke; all verified with a boot run and regenerated screenshots.

  • GameManager did not compile: radial was typed CanvasLayer, so radial.is_open / confirm_selected() made := inference fail. Typed as ActionRadial (the parse error would have broken the whole main scene).
  • Camera sat at the guard's feet: Head had no height offset, and the new gravity pulled the guard to the floor. Head is now at y=1.6, and the guard spawns on the landing.
  • Guard spawned inside the building collider and fell through the world. Building volume moved back to z ∈ [-8, -4] so the door / awning / marquee sit on its front face; the guard spawns in front of the door facing the street.
  • Steps rose away from the door; treads now rise toward the entrance.
  • Event scheduler was polled every physics frame (~60 Hz), spawning >1200 NPCs in seconds. ShiftCoordinator now accumulates time and pulls once per simulated second; regression test in test_scenarios.gd.
  • Live NPCs capped at MAX_LIVE_NPCS (16); routine traffic is dropped at the cap and a scripted event evicts the oldest unfocused NPC.
  • Removed the skyline backdrop planes (opaque black texture). Tracked as a follow-up in TODO.md.
  • README screenshots regenerated with the fixed eye height and entrance staging.

World and presentation follow-ups (found during the control smoke)

Completed 2026-10-31 (working tree). Headless 88/88; src/test/smoke_session_shell.gd and the windowed focus/radial smoke both pass.

  • The skyline backdrop was removed: assets/textures/ai/game_ready/skyline.png has an opaque black background, so the planes rendered as black slabs with window dots. Re-add the backdrop only with a transparent asset or a keyed shader.
  • Guard settles ~0.5 m above the ground after spawning on the landing. Check the capsule offset and the spawn y, and verify eye height while walking the steps.
  • Steps and the landing are visual-only (no collider). The guard walks through them; add simple step colliders or restage the landing so it does not intersect the walk path.
  • Marquee text (GIG LINE) is tiny from the sidewalk. Scale the Label3D up.
  • Focus circle and name tag are small at 2–3 m. Consider a larger circle / font after a real playtest.
  • NPC crowd cap (MAX_LIVE_NPCS) silently drops routine traffic at 16. Add a short despawn/lifetime for ordinary guests so the entrance does not fill up with standers.

Session shell

Completed 2026-10-31 (working tree). Headless 88/88; src/test/smoke_session_shell.gd and the windowed focus/radial smoke both pass.

The game boots into a live 900 s shift. Save declares settings.json and never uses it. V cycles venues by restarting the shift.

  • Startup menu: pick venue, start shift, quit. Keyboard + click. Replaces V as the player-facing path (V may stay as a debug bind).
  • Pause overlay when Esc releases the mouse (or a dedicated pause key): resume, restart shift, settings, quit to menu.
  • Settings: mouse look sensitivity, persist through Save / settings.json. Load on boot.
  • Shift review "Run another shift" returns to the venue menu, not a silent restart of the same scenario.

Documentation drift

Completed 2026-10-31 (working tree).

  • README controls table, smoke checklist, test count, and implemented/not-yet lists refreshed for the control pass (77 tests).
  • PLAN.md Tech Stack / Planned Commands still say GUT and GitHub Actions; the project uses src/test/test_runner.gd and .gitea/workflows/build.yml.
  • MEMORY: door_staff_lines log through GameManager; 300 s default removed.

Remaining PLAN verbs (radial exists)

Completed 2026-10-31 (working tree). Headless 98/98; src/test/smoke_plan_verbs.gd exercises all five verbs.

  • Eye contact as a Level 1 action (PLAN / glossary). Distinct from raise-hand. Add a radial slice if it ships.
  • "I recognize this person" is a player decision recorded as evidence, not a pre-attached artifact on every staff/guest type. Staff still auto-recognize when override_access is empty (AccessLoadout).
  • Formal warning can activate the body-camera / security-light flag (PLAN Level 2). Overlay is currently always on; toggling should write an incident event.
  • Blocking stations the guard between the person and the door (PLAN Level 3). Today it only sends apply_player_action(BLOCKING).
  • "Request reason for entry" as a Level 3 ask, with a short NPC reply from dialogue_lines.

Crowd presentation

Completed 2026-10-31 (working tree). Headless 105/105; src/test/smoke_crowd.gd verifies rubbernecking and panic clearing. A 16-NPC entrance measured ~1.2 ms/frame (824 fps, vsync off), so no LOD/culling was added at the current cap.

CrowdSystem is a mood/attention float. PLAN wants people watching, gathering, and panicking.

  • On aggressive_behavior / emergency_responder, nearby idle NPCs stop and face the incident (rubberneck). On crowd_panic, they move away from the door.
  • Profile a busy entrance with textured materials and props; add LOD or culling for distant NPCs if frame time drops.

Emergency aftermath

Completed 2026-10-31 (working tree). Headless 113/113; src/test/smoke_emergency.gd covers the unjustified-call investigation, the evacuation aftermath, and the review copy.

Responder outcomes (command / restraint / evacuation / arrest) are log lines. PLAN also wants evacuation, a closed street, and an investigation when the call was unjustified.

  • evacuation outcome actually clears the entrance (NPCs leave, door stays locked).
  • Unjustified emergency (low danger, no weapon) writes an investigation note on the shift review and applies the existing false-report / reputation hit.
  • Review copy names the responder outcome, not only battery and crowd %.

Body-camera replay

Completed 2026-10-31 (working tree). Headless 120/120; src/test/smoke_timeline.gd opens the timeline from the review, steps through every event, and returns to the review.

  • Shift review: a timeline of recorded incident events (ADR-0005 already stores them). Scrub or step through; no live 3D replay required in the first version.

NPC theater

Completed 2026-10-31 (working tree). Headless 127/127; verified visually with a staged sit/photo/perform lineup.

Intents sit / photo / film / perform already pick waypoints. ADR-0010 rig (face/hair) is in the working tree. Bodies still idle-stand at those spots.

  • Sit pose at steps_end. Photo: brief raise of the influencer/tourist prop. Performer stays put and plays the hat/sign.

Audio

Completed 2026-10-31 (working tree). Headless 134/134; src/test/smoke_audio.gd checks the bed follows mood and the radio crackles, headless and windowed (real audio server).

AudioFeedback is sine beeps. DONE called that "crowd ambience"; it is not.

  • Loop a low crowd bed that rises with CrowdSystem.mood.
  • Radio open/close crackle on the existing radio panel, still headless-safe (no-op without an audio server).

Accessibility

Completed 2026-10-31 (working tree). Headless 139/139; verified visually at 1.4x font scale with the shape meter showing.

  • Escalation colors that still read in grayscale (shape or label, not hue alone).
  • HUD font scale in settings.

Scenario content still missing vs PLAN lists

Completed 2026-10-31 (working tree). Headless 149/149; src/test/smoke_scenario_content.gd spawns the refusal event and the celebrity arrival through the real signal path.

  • Joke Shop: guest on the reservation list without ID; borrowed/mismatched ID; guest refuses wand screening; late arrival during a performance.
  • Studio: filming-boundary repeat (influencer ignores a posted line); celebrity-arrival crowd spike (more rubberneckers, not only one arrive spawn).
  • Building: "activate body camera" and "request reason" once those verbs exist, used in at least one authored event.

Distribution follow-up (host config)

Completed 2026-10-31 (working tree). Headless 153/153.

Verified via API+host credentials (2026-10-31):

  • Repo is public (private:false) and account has admin rights.
  • Release v0.2.1 now carries all five artifacts (portable binaries + NSIS installer + AppImage).
  • Host setting applied: [service] REQUIRE_SIGNIN_VIEW = false in /etc/gitea/app.ini (backup written alongside), Gitea restarted. Verified anonymously: repo page 200, API 200, and a release asset downloads with the full 50.8 MB AppImage.

Release pipeline follow-up

Completed 2026-10-31 (working tree). Headless 153/153.

  • Fix tools/publish_release.sh: the ~/.git-credentials branch never set AUTH, so every tag build since v0.2.2 failed to publish.
  • Add a github.token fallback in .gitea/workflows/build.yml (the repo has no GITEA_TOKEN secret).
  • Fall back to the checkout's git extraheader when no token/credentials exist (the Gitea runner does not expose github.token).
  • Uploads now fail loudly and the release is verified to carry every artifact (a silent HTTP 4xx used to leave an empty release with a green job).
  • Host disk expanded to 16G; v0.2.0 and v0.2.9 releases restored from the local artifacts.
  • Host maintenance via root@192.168.88.30: apt cache cleaned and journald capped (SystemMaxUse=300M in /etc/systemd/journald.conf.d/99-size.conf); ~7.6G free.
  • Verified end to end: tagging v0.2.12 produced a release with all five artifacts (AppImage, Linux, macOS, NSIS installer, Windows) from CI, with no repo secret.
  • Backfill decision: v0.2.4–v0.2.8 stay superseded; their tags and source remain, and the release page points at the current build. No rebuilds planned.

Open Polish

Completed 2026-10-31 (working tree). Headless 153/153.

  • Keep % placeholders stable in Strings.fmt when adding keys (positional formatting).

Screen design references (QA tooling)

Completed 2026-10-31 (working tree). 30 Python tooling tests pass; the live judge run classed 3 captures as match and 3 as review (concept mockups are more stylised), with no confident mismatch.

  • tools/screen_designs.py generate writes one reference design per screen (from the docs/ descriptions) to docs/screens/designs/, with an offline --draft wireframe mode and an OpenRouter --ai mockup mode.
  • tools/screen_designs.py compare checks docs/screenshots/*.png against the designs (aspect ratio, size, blank-frame, and layout/palette/detail drift).
  • Vision-model judge for semantic screenshot comparison: judge sends the capture + reference design to a vision model, classes verdicts as match/review/diff with a confidence threshold, supports --dry-run, --json, --only, and --model, and reads the repo .env for the API key.

Tooling (when an editor workflow exists)

Completed 2026-10-31 (working tree). Headless 157/157; the exported .tres venues round-trip through the resource loader.

  • Author venues as .tres resources: tools/export_venues.gd writes content/venues/<id>.tres from the factory, and a test loads them back to check nothing is lost (events, profiles, policy, phases, door lines, locations).
  • GUT decision: not adopted. The custom headless runner (src/test/test_runner.gd) already covers 157 assertions without an editor plugin, runs in CI, and keeps the project dependency-free; adding GUT would duplicate coverage and require the editor.

In-Game Scene Examples

Completed 2026-10-31 (v0.3.0 working tree). python3 tools/screen_designs.py list reports 19 screens (8 scene, 11 UI); the Python tooling tests pass.

  • Extend the tools/screen_designs.py catalogue with the screens that exist in code but had no reference: wand screening, Joke Shop door queue, TV Studio celebrity arrival, crowd panic, responder arrival, body-camera timeline, escalation HUD.
  • Add a list subcommand so the catalogue is inspectable without generating images.
  • Cover the new entries with tools/test_screen_designs.py assertions.
  • Regenerate the deterministic draft references under docs/screens/designs/. (generate now keeps existing references unless --force is passed, so a draft run cannot replace an AI mockup; --only selects a subset.)

Rendering Quality

Completed 2026-10-31 (v0.3.0 working tree). Screenshots regenerated; screen_designs.py compare reports 6 good / 0 bad.

  • Add a shared surface-material module (src/game/materials.gd) with roughness/metallic/uv-scale profiles per surface and a single environment tune-up (tonemap, glow, fog, shadows).
  • Rebuild WorldBuilder surfaces with the material profiles and add street detail: window bands, door trim, bollards, bench, bin, curb, planter foliage.
  • Per-phase environment values (fog, sky energy, glow) so night reads darker and the marquee glows.
  • Improve NPC silhouette and materials: per-role clothing palette, caps/hoods/backpacks, sleeves and hands, walking leg swing.
  • Cover materials/environment/avatar helpers with headless tests (test_materials.gd, test_target_cycle.gd).

AI 3D Model Tooling

Completed 2026-10-31 (v0.3.0 working tree). model_pipeline.py build + audit pass for all six props; test_generated_props.gd proves they import as scenes and that the world places them.

  • Add tools/model_pipeline.py: prop catalogue, deterministic low-poly GLB builder, OpenRouter concept-image generation, palette sampling and GLB audit.
  • Add tools/test_model_pipeline.py (valid GLB structure, deterministic output, audit failures, sRGB→linear conversion, role assignment).
  • Generate the prop models into assets/models/generated/ and import them.
  • Load the generated props in WorldBuilder with a procedural fallback when the GLB is missing.

Controls, Movement and NPC Selection

Completed 2026-10-31 (v0.3.0 working tree). The windowed smoke now covers wheel cycling and right-click clearing; the headless suite passes 177/177.

  • Record the re-evaluated control model as ADR-0011.
  • Rank focus candidates by view angle then distance (NpcFocus.rank_indices) instead of insertion order.
  • Add mouse-wheel target cycling next to Tab, right-click clearing focus, and a Shift precision walk speed.
  • Show the nearby-candidate count and the cycle hint in the HUD.
  • Cover ranking, cycling and walk speed with tests, then re-run the windowed smoke.

Web Export Build Target

Completed 2026-10-31 (v0.3.0). Headless 181/181; godot --headless --export-release "Web" produces a 63 MB static bundle and it serves locally with HTTP 200s for html/wasm/pck.

  • Add a Web preset to export_presets.cfg using variant/thread_support=false so the build runs on plain static hosting without COOP/COEP headers.
  • Export to build/web/ and package build/gig-line-web-<version>.zip from tools/build_releases.sh (zip or Python fallback).
  • Publish the web zip with the desktop artifacts (tools/publish_release.sh).
  • Cover the preset, the non-threaded choice, and the release wiring in src/test/tests/test_web_export.gd.
  • Document the artifact in docs/RELEASING.md, the README gallery/status, and PLAN.md.

Prop Texture Atlas

Completed 2026-10-31 (v0.3.1). Headless 182/182, Python tooling 53/53; screenshots regenerated and screen_designs.py compare still reports 6 good / 0 bad.

  • model_pipeline.atlas_for() packs each prop's palette roles into one small power-of-two PNG and a matching metallic/roughness atlas (roughness in G, metallic in B), with nearest sampling so a palette cell stays a flat colour.
  • build_glb() merges every part into a single primitive with per-vertex atlas UVs and one material (baseColorTexture + metallicRoughnessTexture), so a prop is one draw material instead of one per part.
  • audit_glb() checks the single-material/atlas contract (one material, both textures, TEXCOORD_0 on every primitive, two images).
  • Tests: atlas determinism and power-of-two sizing, UV range, palette override reaching the atlas, and a Godot test that each imported prop scene uses exactly one material.
  • Removed the now-unused sRGB→linear factor helpers (colours travel in the sRGB texture).

Complete AI Mockup Set

Completed 2026-10-31 (v0.3.2). All 19 catalogue screens now have an AI reference; Python tooling 53/53.

  • Generated mockups for the seven entries that only had wireframes: wand screening, Joke Shop queue, TV Studio arrival, crowd panic, responder arrival, body-camera timeline, escalation HUD.
  • generate loads .env like judge already did, so --ai works without exporting OPENROUTER_API_KEY by hand.
  • Verified every reference is 1280x720, reviewed the new images visually, and confirmed list reports 19 designs (12 kept, 7 new).

Staged Captures For The New Screens

Completed 2026-11-01 (v0.3.3). screen_designs.py compare reports 9 good / 0 bad; vision judge: wand match (0.90), timeline match (1.00), escalation review, 0 diff. Python tooling 53/53, headless 182/182.

  • capture_screenshots.gd now continues past the night set: switches to The Joke Shop, stages a guest, retries the wand until it alerts (falls back to the documented keys false alarm), applies formal warning + blocking, and opens the body-camera timeline.
  • New captures 07-wand-screening.png, 08-escalation-hud.png, 09-body-camera-timeline.png added to docs/screenshots/ and to the README gallery.
  • Wired ref for wand_screening, escalation_hud and body_camera_timeline so compare and judge include them (9 of 19 screens now have a staged capture).
  • Regenerated the escalation HUD mockup as a third-person over-the-shoulder night shot so the reference matches the game camera.
  • Added Hud.clear_log() so staged captures drop target-cycling noise; the escalation shot keeps the real reaction lines.

Prop Collision And Placement

Completed 2026-11-01 (v0.4.0). Headless 187/187, Python tooling 53/53, the new smoke_props.gd passes windowed, and screen_designs.py compare still reports 9 good / 0 bad.

  • Every generated prop now gets a fitted static collider: a cylinder for bollard, bin, and hydrant, a box for everything else. The collider is computed from the model's mesh bounds in holder-local space, so placement rotation and variant scale carry over.
  • Added smoke_props.gd (windowed): the guard walks into a bench and stops instead of passing through it; a guest heading for a point beyond the same bench routes around it with navigation avoidance and never ends up inside the collider.
  • Props publish a NavigationObstacle3D footprint, and NpcAvatar enables agent avoidance with a velocity_computed callback. The navmesh stays a flat walkable plane; avoidance does the steering around street furniture.
  • Replaced the hard-coded prop calls with WorldBuilder.PROP_PLACEMENTS, grouped by entrance, mid-block, and curb zones, with a static placement_problems() validator for bounds, overlap/min-spacing, duplicates, and the door/steps/guest-lane exclusions.
  • Deterministic variants per placement (variant_for): yaw jitter, subtle scale, and optional procedural clutter (bag by the bin, crate by the newspaper boxes, cone by the hydrant), so repeated props stop looking cloned.
  • Fallback primitive props go through the same bounds fit, so a missing .glb cannot silently remove collision; a unit test covers the fallback body, shape, width, and navigation obstacle.
  • Tests: placement validation and its failure modes, variant determinism/range, colliders on every placed prop (including obstacle presence), and the fallback collider path.

Prop Pipeline Checks

Completed 2026-11-01. Python tooling 56/56, headless 188/188, prop gallery match (0.90) in the vision judge, compare 10 good / 0 bad.

  • audit_glb() now decodes positions/UVs/indices and enforces the art budget (MAX_TRIANGLES = 400, MAX_VERTICES = 600), rejects degenerate triangles, UVs outside [0, 1], a prop that floats above or sinks below the ground (min y ~= 0), and the single-material/atlas contract. The newspaper box sat 3 cm high; its catalogue parts moved down so it rests on y = 0.
  • Added capture_prop_gallery.gd: renders all six props on a studio backdrop (two rows on a display platform) to docs/screenshots/10-prop-gallery.png, with a prop_gallery catalogue entry and AI mockup, so compare/judge cover close-range prop art.
  • Confirmed instances share the imported atlas material (headless test) and documented the triangle/vertex budget in the pipeline docstring.
  • README lists the gallery capture command and image; the pipeline docstring and audit docs describe the budget checks.

Better NPC Characters

Completed 2026-11-01 (v0.5.0). Headless suite 208/208; NPC budget smoke reports 16 avatars, 534 meshes, ~6.9 ms average frame time; PLAN-verb, crowd, emergency, and timeline smokes pass.

  • Replaced the rigid capsules with an articulated rig: hip -> thigh -> knee -> shin -> shoe and shoulder -> upper arm -> elbow -> forearm -> hand, with knee/elbow pivots driven by the walk cycle and the seated pose.
  • Layered clothing (layer_for): jacket, hi-vis vest, coat, and dress over the base outfit, with role-appropriate defaults and deterministic colourways.
  • Extended accessories and hand items: bag, umbrella, phone, camera, clipboard, cup, plus the existing cap/hood/backpack/sunglasses/lanyard. Hand items attach to the right hand socket.
  • Face and hair variety: ears, brows, mouth, five hair silhouettes (long, bun, beard, bald, short), height range, and deterministic per-seed appearance.
  • Added idle behaviour layers (idle_style_for): breathing, weight shift, look-around, phone, clipboard, and crossed arms; escalation gestures raise both hands (L3+) or point (L2), and noticed NPCs turn their head toward the guard.
  • Walk cycle now swings hips/shoulders, bends knees and elbows, and uses a faster, longer stride when fleeing.
  • Door queue staging: GameManager sorts waiting guests and assigns spaced queue_slot_for offsets, so one person is at the door and the rest wait behind without overlapping.
  • Reaction readability: a billboarded symbol above the head (!! panic, ?! watching, ! blocked, ? warned) updates from the reaction/escalation state.
  • Three LOD tiers with hysteresis: full detail, simplified (props/shadows off), and a billboard impostor; apply_lod is tested for the swap.
  • Re-measured the live-NPC budget with the full rig (16 avatars, 534 meshes) and added smoke_npc_budget.gd, which fails above the documented 50 ms frame budget.
  • Tests: deterministic layers/hand items/hair, role layers, queue slot spacing, LOD thresholds/hysteresis, reaction labels, articulated pivots, mesh budget, and the walk-to-idle animation transition.

Better Player Movement

Completed 2026-11-01 (v0.5.0). Windowed smoke_movement.gd passes; headless tests cover surface lookup, constants, gamepad bindings, and settings persistence.

  • Split acceleration and braking (accel 22, decel 30 m/s²) so stopping is snappier than starting, and fixed the inverted forward axis found while staging the movement smoke: W now walks toward the camera's forward vector instead of moonwalking.
  • Step-up assist (0.32 m) climbs kerbs and the entrance steps, with wall sliding left to move_and_slide; the smoke walks the guard from the sidewalk onto the landing.
  • Camera feel: speed-scaled head bob, a landing dip, and a precision-walk FOV change, all gated by the new "Head bob and camera motion" setting; a motion-sickness toggle persists.
  • Precision walk shows a HUD chip and plays a short tick when it engages, in addition to the slower speed.
  • Surface-aware footsteps (concrete/asphalt/stone) synced to stride distance, plus a landing cue; both have observable counters for tests.
  • Gamepad support: left-stick movement, right-stick look with deadzone, A/B/X/Y face buttons, LB slow, RB wand; bindings are added at runtime and the control legend swaps to gamepad hints when a pad event is seen.
  • Moved the remaining hard-coded gameplay keys (Q/F/G/L/B/V, Tab cycle) onto named InputMap actions, and the panel allow/deny keys onto movement actions.
  • Key/button remapping in the settings panel: click a binding, press a key or pad button, persisted as input_bindings through Save; InputBindings.apply rewrites only key/button events and leaves mouse and gamepad axes alone.
  • Added invert-Y and head-bob settings (with save/load round-trip tests) alongside the existing look sensitivity.
  • Tests: surface lookup, step/stride/precision constants, gamepad binding presence and deadzone, remap descriptor/apply/labels, and settings persistence for camera and bindings.
  • Added smoke_movement.gd (windowed) and 11-precision-walk.png with a movement catalogue entry, AI mockup, and compare coverage.

NPC Facing During Movement

Completed 2026-11-01 (post-v0.5.0, pre-v0.6.0). Headless suite 209/209.

  • Root cause: NpcAvatar used Godot's look_at(), which aims the node's -Z axis at the target, but the procedural rig's face (eyes, brows, mouth, focus disc) and every Blender-authored successor face +Z. Every walking NPC therefore showed its face toward the side it was leaving.
  • Added the pure NpcAvatar.yaw_towards(direction) helper (+Z forward, tested at +Z/+X/-Z and an arbitrary direction), and replaced look_at() in the navigation and direct-move paths plus the reaction-face turn.
  • Corrected the head-turn offsets to use the same +Z convention (basis.z instead of -basis.z), so noticed NPCs still look at the guard with the fixed facing.
  • Added test_yaw_faces_the_movement_direction; recaptured the targeting screenshot to confirm NPCs now show their back only when walking away.

Blender Asset Library Integration

Completed 2026-11-01 (v0.6.0 work). Headless 222/222, Python 68/68, all windowed/headless smokes pass, compare 11 good / 0 bad. AO/ORM baking remains in Phase 4.

  • Committed the audited library (8 characters, 18 gear, 20 props), generator, .blend sources, manifest, review sheets, and integration notes.
  • NpcAvatar selects a character variant by role/seed (npc_casual, npc_jacket, npc_coat, npc_vest, npc_dress, npc_hoodie, npc_firefighter) with npc_base and the procedural rig as fallbacks; the choice is recorded in character_model.
  • Only the five contract slots (skin, cloth, pants, accessory, hair) are tinted; fixed detail materials keep their authored colour. npc_vest keeps its fixed hi-vis vest, and firefighter/guard use dark uniform tints.
  • Library gear replaces the procedural accessories and hand items at the documented head / back / chest / hand attach points (cap, hood, helmet, backpack, SCBA tank, lanyard, phone, cup, clipboard, umbrella, camera, sign, flashlight, wand, radio), with the primitive path retained as fallback.
  • WorldBuilder loads the Blender replacements (bollard, bench, trash_bin, planter_box, newspaper_box, fire_hydrant, street_lamp, street_tree, traffic_cone) and places the new dressing (palm_tree, round_planter, a_frame_sign, crowd_barrier, stanchion, security_podium, handrail, double_door, marquee, sedan, wall_lantern) with colliders/avoidance and placement validation.
  • Library emissive surfaces (lamp bulbs, marquee bulbs/sign face, wall lanterns, door LED, sedan lights) are collected and driven by set_night_mode().
  • Added a library triangle counter and 60k triangle budget check; the current set is ~27,986 triangles. NPC budget smoke: 16 avatars = 112 meshes at ~6.9 ms.
  • Recaptured the README/catalogue screenshots (including the prop gallery from the library models) and fixed the marquee text placement.

Environment and Lighting Lift

Completed 2026-11-01 (v0.6.0). Headless 222/222, Python 69/69, all smokes pass, compare 11 good / 0 bad.

  • Baked per-asset AO maps for the full Blender library with tools/bake_ao.py (Cycles, 256×256, stored next to each GLB as <name>_ao.png); Godot wires the red channel into StandardMaterial3D.ao_texture for character slots, gear and props.
  • Regenerated brick, concrete and asphalt at 1024² through asset_pipeline.py, derived tangent normal maps with tools/make_normal_maps.py, and wired them into SurfaceMaterials.textured() while keeping the existing UV scales.
  • Evaluated Forward+: it boots on the desktop Vulkan driver, but the renderer is a project-wide setting and the Web build must stay on GL Compatibility. Decision: keep Compatibility for v0.6.0; a desktop-only preset needs a separate project/override and is deferred.
  • Recaptured docs/screenshots/ and ran compare + judge; no regressions.
  • Fixed the library marquee text/face alignment and added the library AO/triangle tests.

v0.6.0 Release

Completed 2026-11-01. config/version = 0.6.0; ADR-0013 marked Accepted; CI published six artifacts (Linux, Windows, macOS, NSIS installer, AppImage, Web) for tag v0.6.0.

  • Bumped the version and synchronised README/architecture/blender-assets/plan docs.
  • Headless 222/222, Python 69/69, all smokes, compare 11 good / 0 bad.
  • Release verified: https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.6.0.

Runtime Image Path Migration

Completed 2026-11-01. Historical note: the screen-design references originally lived only in docs/screens/designs/; they were promoted to the runtime root assets/images/ui/ under ADR-0014. docs/screens/designs/ remains a reference-only snapshot; tools/screen_designs.py now reads and writes assets/images/ui/.

Realistic Backgrounds, Textures and Scene References

Completed 2026-11-01 (part of the v0.6.x realism pass). The flat box skyline was replaced by per-phase AI panoramas, the surface tiles by higher-detail v2 picks with normal maps, and the lighting/exposure was retuned against AI concept frames. Evidence: docs/screens/realism/2026-11-01-adopt/ (report.md, sheet.jpg), screen_designs.py compare 11 good / 0 bad; headless 227/227, Python 86/86, all smokes.

Realistic backgrounds, textures and scene references

Goal: replace the outdated flat skyline backdrop (assets/textures/skyline.png, black boxes with dot windows) and the flat-looking surface tiles with AI-generated, more realistic backgrounds and textures. Add a larger set of example 3D scene images as art direction, and measure the in-game result against that target with the screenshot tools. Plan: PLAN.md → Visual Quality Direction → Realism pass.

Rule for every step: never overwrite an existing image. New outputs get new paths. When a path is taken, the tools pick the next free -vN suffix. Raw AI output stays on disk under assets/textures/ai/v2/raw/ (gitignored like the other raw AI folders). Processed game-ready candidates go to assets/textures/ai/game_ready/v2/, a local pool where only the validated picks are tracked (ADR-0014). Scene references go to docs/screens/concepts/, and trial captures and reports to docs/screens/realism/<run>/.

Step 1 — Tooling (tools/scene_concepts.py)

  • Add a catalogue of prompts in three groups: scene (16:9 example 3D game frames), backdrop (4:1 skyline panoramas with transparent sky), and texture (seamless PBR surface tiles). Each entry has a name, prompt, role and target path.
  • generate [--group] [--only] [--variants N]: call the OpenRouter image API through generate_textures.generate_image (key from the repo .env), save raw output, and never overwrite (next_free_path). Print every file written.
  • process: turn raw backdrops into 2048×512 RGBA strips that tile horizontally (edge crossfade, sky made fully transparent, hard alpha for the alpha-scissor material). Turn raw textures into seamless 1024² tiles (asset_pipeline.make_seamless) plus a normal map (make_normal_maps.normal_map). Skip outputs that already exist.
  • audit: run asset_pipeline.audit_image on the processed candidates (backdrop / tile roles) and fail on any bad result.
  • realism: for each trial capture, report the Pillow detail metrics (edge density, contrast, colour spread, saturation) next to the matching concept image. Optionally ask a vision model (google/gemini-2.5-flash-lite) for 0–10 realism scores (lighting, materials, texture detail, backdrop, overall) with concrete fixes. Write a new JSON + Markdown report per run.
  • Unit tests (tools/test_scene_concepts.py): catalogue shape, the no-overwrite path picker, backdrop/tile processing on synthetic images, metrics, and the judge payload/parse (offline, no network).

Step 2 — Example 3D scene images

  • Generate 3D-game-style example frames for every venue and time of day: Hollywood entrance (morning, golden hour, rainy night), boulevard skyline view (day, dusk), Joke Shop alley (evening, close-up of the door), studio stage door (night arrival), sidewalk material close-up, entrance steps and podium, crowd-control barrier line, and a responder arrival wide shot.
  • Review the set and record the visual targets each frame sets (sky, haze, material detail, backdrop depth) in the report.

Step 3 — Backgrounds (replace the outdated skyline)

  • Generate skyline panoramas for day, dusk and night (lit windows): Hollywood/Los Angeles mid-rise blocks, palm silhouettes, the hills on the horizon, a transparent sky, and a straight-on view, in several variants each.
  • Process them into assets/textures/ai/game_ready/v2/skyline_{day,dusk,night}*.png, seamless horizontally for the 3× UV repeat around the backdrop ring. Strips are 2048×1024 (2:1, one UV repeat of the 400×60 m ring is about 133×60 m). 12 candidates, 11 audit clean; skyline_day-v3 misses the seam limit by 0.6. Variants 1–2 contain garbled "Hollywood" lettering, so the prompt now forbids signs. Picks: skyline_day-v4, skyline_dusk-v3, skyline_night-v4.

Step 4 — Textures

  • Generate higher-detail seamless tiles: red brick facade, granite/limestone facade (studio), sidewalk concrete pavers with joints, asphalt with aggregate, wet night asphalt, stone steps, striped awning canvas, and stucco wall.
  • Process them into 1024² game-ready tiles plus normal maps in assets/textures/ai/game_ready/v2/ and audit them (16 tiles + 16 normals, all good). The awning_canvas seam crossfade smears the stripes at the tile edge; it needs a stripe-aligned crop.

Step 5 — In-game trial captures (no game-code change)

  • Add src/test/capture_realism.gd. It boots the Hollywood venue, captures a fixed shot list (entrance day, boulevard/skyline day, boulevard night), and can swap textures at runtime from --swap=OLD=NEW / --tint= / --normal= user arguments (the first version used --skyline=/--brick=/… flags; --swap is path-based and also covers night stages). The swap finds materials by their original texture path, so baseline and candidate frames use the same camera. Writes to the --out= directory.
  • Capture the baseline (current assets) and the candidate set (v2 skyline + textures), and copy them into docs/screens/realism/<run>/{baseline,candidate}/.

Step 6 — Compare against the realistic target

  • Run scene_concepts.py realism on baseline and candidate. Record the metric deltas and the vision scores, and note which candidate textures/backdrops win. Run docs/screens/realism/2026-11-01-v2/ (report-v2.md, sheet.jpg) gives these mean judge scores: backdrop 2.6 → 3.6 (v2 day + night skyline) → 4.0 (v2 day + dusk skyline for the Evening phase), texture detail 1.2 → 1.6–1.75, overall 1.6 → 2.0. Single scores swing by ±1–2 between runs, so read them as trends. The characters score moved from 3.0 to 1.0 with identical NPCs, which shows the noise.
  • Note the remaining realism gaps (sky/fog, lighting, building geometry, NPC fidelity) as follow-up tasks below.

Step 6b — Follow-ups from the 2026-11-01 realism report

  • Fix sidewalk overexposure: lowered the day sun (0.78→0.62), exposure (0.8→0.6) and ambient (0.42→0.22), and darkened the sidewalk/street albedo tints. The like-for-like ground luminance in docs/screens/realism/2026-11-01-adopt/ went from 173/162/132 to 95/92/73 (concept targets 78/100/100). The entrance shot stays ~20 % brighter than the concept: a residual, tracked in the follow-ups below.
  • Make the Evening/Late phases darker and lit by practical lights (marquee, lamps, windows), and add wet-asphalt roughness at night, as in the rain-night concept. Evening is now dusk (sun 0.3→0.12, exposure 0.95→0.72, dark violet sky), Late is near-dark, and WorldBuilder.set_phase() swaps the street to the wet asphalt map in Late.
  • Choose the skyline per phase (day → skyline_day, Evening → skyline_dusk, Late → skyline_night) and stop the scene fog from bleaching it. The backdrop is now a fog_disabled shader, because Godot 4.3 has no fog_enabled on StandardMaterial3D. The first dusk panorama was a red floodlit skyline that clashed with the Evening sky, so a calmer prompt produced skyline_dusk-v5 (blue-grey towers, lit windows).
  • Raise the backdrop plane (60 m → 90 m) and use 2 UV repeats so the skyline is a band with proportions near the 2:1 panorama, not a thin strip at the horizon.
  • Add facade depth (plinth, string courses, cornice, brick pilasters, window glazing bars) and replace the blob trees and shrubs with denser foliage meshes: _shrub() now builds many small flat-shaded clumps plus outer clumps that break the silhouette (street_tree 1472→2592 tris), with fresh AO maps for street_tree, planter_box and round_planter.
  • Re-cut awning_canvas so its stripes align at the tile edge: stripe_period() measures the repeat and align_stripes() resamples to a whole number of periods (edge crossfade smeared the stripes before). audit_striped_tile() checks the wrap step against the harshest interior step instead of the flat edge-difference rule.
  • Capture shots from the Joke Shop and the studio too: capture_realism.gd now runs five stages (day, evening, late, joke_shop, studio) with 9 shots and a --stages= subset. The venues share the Hollywood geometry, so those two compare lighting, not layout.

Step 7 — Adopt the winners

  • Track the validated picks in place (ADR-0014). .gitignore now ignores the candidate pool and un-ignores game_ready/v2/picks/, where scene_concepts.py promote copies the eight validated candidates under stable names (skyline_{day,dusk,night}, brick_facade, sidewalk_pavers, asphalt, asphalt_wet, awning_canvas) plus normal maps.
  • Point WorldBuilder at the chosen v2 skylines (separate day/dusk/night textures through the new set_phase()), and retune the tints per phase.
  • Switch materials.gd / world_builder.gd surface preloads to the chosen v2 tiles + normal maps, keeping the current UV scales (Materials.asphalt() gained a wet variant). Updated test_materials.gd (pick paths, wet vs dry) and test_world.gd (shader backdrop, panorama per phase, wet street).
  • Regenerate docs/screenshots/ with capture_screenshots.gd (11/11), then run screen_designs.py compare (11 good / 0 bad) and judge (5 match, 2 review, 4 diff — the diffs are HUD/mockup layout gaps from the "Visual alignment" section, not realism regressions; the default flash-lite judge model is currently failing with a provider 400, so the run used --model google/gemini-2.5-flash). scene_concepts.py realism on docs/screens/realism/2026-11-01-adopt/ (3 judge repeats per shot) gives a like-for-like improvement on every sub-score: overall 0.8→1.0, lighting 0.8→1.0, materials 0.8→1.0, texture 0.8→1.0, backdrop 0.6→1.6, and the Pillow metric gap 0.444→0.348.
  • Update README.md (tool usage + gallery), MEMORY.md and docs/architecture.md.

Realism Follow-ups — Lighting, Flashlight, NPC Detail

Completed 2026-11-01. The follow-ups left over after the texture/skyline adoption, split off from TODO.md when each one was finished. Evidence: docs/screens/realism/2026-11-01-npc/ (report.md, sheet.jpg), the portrait capture, and the refreshed docs/screenshots/ gallery (screen_designs.py compare 11 good / 0 bad). Headless 228/228, Python 86/86.

Ground exposure at the entrance

  • Fixed the over-bright entrance ground: the morning sun now sits at 34° elevation (was 50°) with a phase sun_yaw, so the facade shades the near sidewalk and the props cast long shadows. Ground luminance went 95 → 83 against the concept's 78 (it was 173 before the realism pass), with the boulevard at 82 and the street at 81. Day haze warmed and thickened (fog_color 0.87/0.83/0.76, density 0.0034, energy 0.5) and the night glow went to 1.15.

Flashlight readability

  • Made the flashlight read in the dark: the cone went 30° → 14° with a 20 m range, forward aim, light_energy 4.2 and spot_attenuation/spot_angle_attenuation 0.9, so it lights the door instead of washing the facade.
  • Staged the two night captures in LATE (elapsed_s 760) instead of dusk in capture_screenshots.gd; the judge moved 06-night-flashlight from diff (1.00) to review (0.10).
  • Rejected a fake additive beam twice (cylinder + UV ramp, then a vertical-fade ShaderMaterial): on GL Compatibility a first-person camera inside the cone renders it as a filled disc with a bright far ring. Materials.light_beam() and the mesh were removed; the visible shaft is parked behind the Forward+ preset in TODO.md.
  • Guarded the aim with test_flashlight_aims_a_tight_forward_spot (lamp on the head, straight ahead, cone ≤ 20°, starts off).

NPC faces, hands and sleeves

  • Rebuilt the avatar detail so skin and clothing stop reading as low-poly at conversation distance: each eye has a sclera, iris and upper lid; added a brow ridge, cheekbones, a nose bridge and tip, ears with lobes, and split upper/lower lips. Hands have four fingers plus a thumb, and every sleeve ends in a cuff ring.
  • Raised CHAR_MAX_VERTS 4000 → 5000 (documented in the code and docs/blender-assets.md) after measuring 16 avatars at ~6.9 ms of a 50 ms frame on a single skinned mesh, so npc_coat fits at 4807 vertices; the character range is now 3,974–5,026 triangles and 3,744–4,807 vertices. Body decimation 0.36 → 0.26.
  • Rebuilt all 8 variants and re-baked their AO maps (assets/models/blender/npc/*_ao.png), re-imported, and kept blender_assets.py audit at exit 0.
  • Added a staged portrait capture: capture_realism.gd gained a portrait stage that freezes one NPC, parks the crowd out of frame, stands the subject on open sidewalk, turns it into the camera and puts the guard on the sunlit side; --portrait-type=N picks a role. It is the only capture where a face fills the frame, and it feeds the new npc_portrait_day concept (tools/scene_concepts.py SHOTS).
  • Regenerated the gallery shots and re-ran the judge: like-for-like overall 2.33 → 2.44, lighting 3.0 → 3.22, materials 2.22 → 2.44. The characters sub-score stayed flat (~2.2–2.3) because the target is a photoreal face, so avatar quality is judged from the portrait rather than the aggregate.
  • Re-checked the screen_designs.py judge default model (google/gemini-2.5-flash-lite): it works but intermittently answers HTTP 400 "API key not valid" (one shot in ten on 2026-11-01); --model google/gemini-2.5-flash remains the fallback.

Visual Alignment — UI Paths and Art Loader

Completed 2026-11-01.

Storage restructure

  • Decide the canonical runtime image root (assets/images/ for UI/scene art): recorded in MEMORY.md and ADR-0014.
  • Move the reference mockups from docs/screens/designs/ into the runtime root (keep docs/screens/designs/ as a docs-only copy or replace it with the new path).
  • Document one convention for game vs reference vs development images across assets/textures/, assets/images/, assets/models/generated/, docs/screenshots/, and docs/screens/designs/ (in docs/architecture.md, ADR-0014).
  • Re-run godot --headless --path . --import to regenerate .import sidecars after any move.
  • Confirm .gitignore still excludes development-only outputs (concepts, Blender sources, raw AI fabrics) under the new layout; Blender .blend sources are now untracked and ignored.

Documentation path updates

  • Update README.md paths (screen-designs section, confirmed-assets links, the assets/images/scenes resize example).
  • Update docs/architecture.md, PLAN.md, and ADR references: docs/screens/designs/ is now explicitly the reference-only snapshot; runtime art lives in assets/images/ (ADR-0014).
  • Update tools/screen_designs.py docstrings and mark the old paths in DONE.md as historical.

Code path updates

  • Point tools/screen_designs.py generate at the new design output path and keep compare/judge reading the right locations.
  • Update GDScript preload() paths if game textures move (no runtime material/model textures moved; UI art added under assets/images/).
  • Update capture output directories if captures relocate (none did; QA captures stay in docs/screenshots/).

Load proposed interfaces + scene art from disk

  • Add a central UI/scene art loader (e.g. src/game/ui/art.gd) that loads the mockup-derived images from the new runtime root, with a procedural fallback when a file is missing.
  • Wire the start menu and venue selection screen to use the start_menu scene art.
  • Wire the pause overlay to pause_overlay scene art (backdrop + opaque panel cover).
  • Wire the settings panel to settings_panel art.
  • Wire the shift-review screen to shift_review art (backdrop + opaque panel cover).
  • Wire the wand popup to wand_screening art.
  • Style the radial menu and finish the remaining HUD chrome against their reference images.

Verify against the mockups

  • Regenerate the staged captures and run screen_designs.py compare (11 good, 0 bad; verified in a clean HEAD worktree).
  • Run screen_designs.py judge and clear every diff verdict (7 match, 4 review, 0 diff).
  • Add the capture + compare step as a repeatable smoke so drift is caught early (tools/check_screenshots.sh).

Fix mouse movement bug

  • Investigate why mouse input is not being captured or processed correctly in the game window (fixed: _start_shift() now calls _refresh_freeze()).

Improve in-game pop-ups

  • Improve the visual appearance of in-game pop-ups (start/pause/settings/review/radio/wand/inspection art, buttons, palette, icons, text).

Visual Alignment With Rendered Mockups

Completed 2026-11-01. Every screen with an AI reference now loads or matches its mockup: the runtime image root (ADR-0014), the UI/scene art loader, the menus/panels/HUD, and — last — the scene lighting, fog and post-processing, which had been held until the realism pass landed. screen_designs.py compare 11 good / 0 bad; judge on the two scene shots: 01-venue-overview match (0.90), 05-late-night-avenue review (0.10), 0 diff. Lighting: morning sun 34° elevation with a phase sun_yaw, warm day haze (fog_color 0.87/0.83/0.76, density 0.0034, energy 0.5), stronger night glow, street albedo 0.62. Remaining differences (camera height, marquee text, hand-authored tree shapes) are content, not grading.

Visual alignment with rendered mockups

Align in-game visuals

  • Make the venue selection screen match the start_menu mockup (layout, palette, background).
  • Make the HUD match the targeting_npc / escalation_hud mockups (focus ring, escalation meter, log, hints).
  • Align the pause, settings, and shift-review panels to their mockups.
  • Align the radio and wand panels to their mockups.
  • Align the body-camera timeline to its mockup.
  • Align scene lighting, fog, and post-processing for venue_overview and late_night_avenue: lowered the morning sun to 34° (sun_yaw is now a phase value), warmed and thickened the day haze, raised the night glow, and brightened the street albedo. judge moved 01-venue-overview to match (0.90) and 05-late-night-avenue from diff to review (0.10); compare stays 11 good / 0 bad.
  • Fix the body-camera REC/clock overlay overlapping the shift/rep/crowd status in the top-right HUD.

Documentation Consistency

Completed 2026-11-01. PLAN.md was compacted from the historical implementation log into a product/design plan (674 → 187 lines): escalation, NPC/venue content and design constraints stay, while the completed control/rendering pass specs were folded into a short Presentation and Controls section. README.md now reflects the implemented venues, Blender/v2 asset pipeline, the custom test runner, the Web target, the current release state, and the capture smoke. The architecture's version, runtime-art loader, asset/tooling blocks, Gitea CI/build/publish pipeline, Web deployment and release artifact list were brought up to date. MEMORY.md was cleaned of obsolete uncommitted/active-pass claims and now points at the current follow-ups.

  • Re-check PLAN.md/MEMORY.md against the current build, update README.md and docs/architecture.md, and compact the historical PLAN sections.

NPC gear fit (v0.6.1)

Completed 2026-11-01. Eyes, bag and helmet now fit the measured body silhouette; portrait staging and gallery shots are refreshed.

The 2026-11-01 detail pass fixed the bodies but not the props worn on them. In the portrait capture (--stages=day,portrait, see the report in docs/screens/realism/2026-11-01-npc/) the sunglasses span the whole upper face and the shoulder bag cuts through npc_coat's skirt. Blender sources for gear live in tools/blender_assets.py (_build_gear()), the contract in docs/blender-assets.md (gear budget 1500 tris, GEAR_ATTACH points in NpcAvatar).

  • Measure the fit in Blender, not by eye: python3 tools/blender_assets.py fit prints each character's measured head width and hip/skirt radius, then every head/back/chest gear bounding box with head-band status or skirt clearance. Current measurements: head width 0.315 m; coat skirt 0.225 m, dress 0.260 m; sunglasses 0.282 m (0.90x head); shoulder-bag x-clearance -0.025 m on the coat (radial -0.031 m on the dress).
  • Narrow sunglasses from 0.28 m to ~0.20 m. The hinges moved to ±0.075 (0.035 box), the arms to ±0.098, and the lenses are 0.024 m radius at the same y=-0.152, z=0.035 plane. Rebuilt GLB/AO: measured width 0.202 m (0.64x head), still inside the head band. Also fixed capture_realism.gd so --portrait-type=N is actually parsed (it was documented but ignored).
  • Fix shoulder_bag clipping through npc_coat/npc_dress: narrowed the bag to 0.06 m and moved it outboard (centre x=0.292); the body's inner face is now 0.262 m, clearing the coat (0.225) by 0.037 m and the dress (0.260) by 0.002 m. The strap's lower attach points sit on the same outboard line. fit now measures the bag body separately from the torso strap.
  • Same pass for cap/beanie/hood/firefighter_helmet: rendered the cap (vest), hood (hoodie), and firefighter helmet on their characters. Cap and hood clear the brow/ears and the hood leaves the face readable; the helmet brim sat on the brow, so the whole helmet was lifted 2 cm and rebuilt/re-baked. Beanie uses the same capped-shell sizing as the cap and is currently unused; it is covered by the upcoming headgear regression check.
  • Rebuild the gear (build --only <18 gear names>) and re-bake AO (tools/bake_ao.py --only <same>), then godot --headless --path . --import (clean). Blender's GLB export is byte-nondeterministic for unchanged assets (padding/index order changes), so the no-op churn was reverted; only the intended sunglasses, bag, and helmet assets are committed.
  • Add a regression test: tools/test_blender_assets.py limits the glasses to 0.70x head width and requires positive bag clearance on both coat and dress; test_npc_character.test_head_and_back_gear_clear_the_body_silhouette loads the gear on six roles and checks head-parent/width/brow bounds plus back-parent/0.26 m outer radius.
  • Re-capture the portrait (--stages=day,portrait) and the gallery shots; record the before/after in docs/screens/realism/2026-11-01-gear/. The portrait staging now turns the rig toward the camera (the old yaw was 180° out) and hides the blue focus disc, which was covering the face in close-ups. check_screenshots.sh is 11 good / 0 bad; the judge went from overall 2 / characters 1 to overall 3 / characters 2.
  • Consider a second character close-up for a hoodie/winter NPC so headgear is covered too. --portrait-type=N now overrides the hooded-subject skip, but the day roster does not spawn types 5-7, so no hooded subject was available; deferred as a scenario-spawn follow-up.