Once an NPC reaches its intent waypoint it now does something instead of standing in an idle T-pose: - sit: the rig settles 0.34 m into a seat and the legs swing forward on the steps - photo/film: influencers and tourists raise their camera prop briefly (a sine pulse, so it reads as taking a shot rather than a permanently raised arm) - perform: performers sway the sign board they carry Pose selection lives in NpcAvatar.theater_pose() (intent + arrived); the math is in pure helpers (crouch_for_pose, prop_lift_for_pose, sway_for_pose) so it is headless-testable. Prop/leg nodes are captured at build time and animated from their rest offsets. Performers also get a hand-held board and post. Tests/docs - Headless suite 120 -> 127 (test_npc_theater.gd). - Verified visually with a staged sit/photo/perform lineup in front of the entrance. - README, TODO section moved to DONE, MEMORY. Version 0.2.7.
278 lines
25 KiB
Markdown
278 lines
25 KiB
Markdown
# MEMORY
|
||
|
||
Session memory for durable decisions, state, and gotchas. Keep concise, remove obsolete entries.
|
||
|
||
## Decisions
|
||
|
||
- Engine: Godot 4.3 stable (official editor build, `~/.local/bin/godot`). The system's preinstalled `godot-runner` (4.7.2, Fedora) cannot load projects from arbitrary paths because it was compiled without path overrides; use the official editor binary instead.
|
||
- Language: GDScript. All cross-file references use explicit `preload()` constants (not `class_name` globals) so scripts compile in headless `--script` runs without the editor's class cache.
|
||
- Test framework deviation: the project uses a custom minimal headless test runner (`src/test/test_runner.gd`) instead of GUT, because GUT needs the editor plugin. CI runs it with `--headless`.
|
||
- Eventing: no autoload signal bus. `ShiftCoordinator` owns an `EventBus` instance (`src/core/event_bus.gd`) passed to subsystems; subsystems emit only when a bus is assigned. Autoloads at parse time are unreliable under `--script`.
|
||
- Static signals are not available in Godot 4.3 GDScript ("Expected func or var after static") — do not use them.
|
||
- GDScript lambdas capture local variables **by value**; mutating a captured local inside a lambda does not affect the outer variable. Use member functions for accumulators in tests.
|
||
- Shift length is the scenario's `shift_duration_s` (900 s in all three venues). Tests and the screenshot tool override elapsed time; they do not change the default.
|
||
- RNG: NPC reactions and event scheduling are seedable for deterministic tests.
|
||
|
||
## Handoff (read this first)
|
||
|
||
- Repo: `/run/media/system/DATA/Documents/06-games/security-game`. Godot: `~/.local/bin/godot` (4.3.stable).
|
||
Tests: `godot --headless --path . --script res://src/test/test_runner.gd` → **88/88**.
|
||
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.
|
||
- `main` is in sync with origin. **v0.2.2** tags the session-shell + world-follow-up commit;
|
||
CI builds the portable binaries, NSIS installer, and AppImage on the tag push.
|
||
- Next work: `TODO.md` audio (crowd bed rising with mood, radio crackle), then accessibility and
|
||
scenario content.
|
||
- Do not re-open `override_access`. Do not bind wand to `W`. Do not remove the radio easter egg.
|
||
|
||
## Current State
|
||
|
||
- Access artifacts: `src/core/access_loadout.gd` builds from event `override_access` + venue policy.
|
||
Spawn signal is now `(profile, intent, npc_id, event)`. Recognition is not auto-attached to every
|
||
guest. `AccessVerifier.verify(..., already_screened=false)` so Joke Shop scoring uses
|
||
`controller.screened`. Joke Shop/Studio gained `sidewalk`/`steps_end` waypoints. Wand venues roll
|
||
`carried_items` per spawn (seeded). Tests in `src/test/tests/test_access_loadout.gd`.
|
||
- Wand shortcut is **`C`**, not `W` (`W` is walk). `radio_emergency` input map no longer includes W.
|
||
`hud.controls` / `hud.screen_first` updated.
|
||
- Control pass (in tree, uncommitted, smoke-tested): cone focus via `src/game/npc_focus.gd`,
|
||
accel/decel, gravity only airborne, `input_locked` freeze, Esc no longer on the guard.
|
||
`NpcAvatar` in group `npc`; neck/hair/eyes/shoes; torus head ring + `Label3D` name; `set_focus_visual`.
|
||
`src/game/ui/action_radial.gd` doughnut (LMB/Space open, A/D/arrows, Space/click confirm, Esc/RMB
|
||
cancel). Crosshair CanvasLayer. Emergency still `R` only. Number keys 1–6 when the wheel is closed.
|
||
- Named input actions now exist and are used: `action_menu`, `confirm`, `cancel` (Esc + RMB),
|
||
`inspect` (E), `wand` (C). The old unused `interact` action was removed.
|
||
- Runtime fixes from the smoke: `Head` is +1.6 above the guard root (root sits on the floor); guard
|
||
spawns in front of the door facing the street; building volume moved to z ∈ [-8,-4] so the door,
|
||
awning, and marquee are visible; step treads rise toward the door; `ShiftCoordinator` pulls the
|
||
event scheduler once per simulated second (was every physics frame → >1200 NPCs in seconds);
|
||
`GameManager.MAX_LIVE_NPCS = 16` caps live avatars.
|
||
- **Session shell (2026-10-31, 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_running` gates input and `_refresh_freeze()`.
|
||
Esc with nothing open now pauses (`PauseOverlay`, layer 18) → resume / restart / settings /
|
||
quit-to-menu. `SettingsPanel` (layer 22) writes `look_sensitivity` + `hud_font_scale` through
|
||
`Save.save_settings()` → `user://settings.json`; `Save.load_settings()` merges over
|
||
`Save.DEFAULT_SETTINGS` on boot and `GameManager._apply_settings()` pushes them into the guard/HUD.
|
||
Shift review "Run another shift" now calls `_show_start_menu()` (not a silent restart). `V` stays a
|
||
debug venue bind. Smoke: `src/test/smoke_session_shell.gd` (menu → shift → pause → settings → menu).
|
||
- **World follow-ups closed (2026-10-31):** skyline backdrop restored with `T_SKYLINE`
|
||
(`assets/textures/skyline.png`, regenerated RGBA with a transparent sky) and an
|
||
`TRANSPARENCY_ALPHA_SCISSOR` unshaded material (`_skyline_mat`); steps + landing are a
|
||
`StaticBody3D` with per-tread `BoxShape3D` colliders; marquee `font_size` 160 / outline 24; focus
|
||
torus 0.26–0.34 + name tag 48px; `NpcAvatar.ephemeral`/`lifetime_s` despawns routine traffic
|
||
(authored events stay). Guard spawn height verified on the landing (guard_y=0.84).
|
||
- **PLAN verbs (2026-10-31, uncommitted):** the radial now has 12 slices
|
||
(`Stop, Look, Point, No, Ask, Inspect, Wand, Warn, Block, Reason, Recognize, Radio`).
|
||
`eye_contact` reuses the Level 1 reaction table via `_apply_interaction`. Recognition is a player
|
||
decision: `NPCController.player_recognized` + `IncidentRecorder.add_access_evidence`, and
|
||
`GameManager._artifacts_for()` prepends a `RECOGNITION` artifact only after the player declares
|
||
it. The body camera is a real flag (`GameManager.body_camera_on`, `Hud.set_body_camera`), off by
|
||
default, `B` toggles it, and a formal warning arms it and writes a `body_camera` incident event.
|
||
Blocking also calls `_station_between()` to physically place the guard 0.9 m from the NPC toward
|
||
the entrance. `_request_reason()` logs the NPC's seeded `dialogue_lines` pick.
|
||
- **NPC navigation was dead (fixed 2026-10-31):** avatars never moved in the live game, for three
|
||
stacked reasons. (1) `agent.target_position` was re-assigned every physics frame, which
|
||
invalidates the path so `get_next_path_position()` keeps returning the current point — only set
|
||
it when the target changes. (2) Nav path points sit on the navmesh plane (y=0) while bodies stand
|
||
at y≈0.5, so a 3D "are we there" test always looked 0.5 m away and the horizontal direction was
|
||
zero — compare horizontally. (3) `path[0]` is the old start point, so picking any point "far
|
||
enough away" can pick the point behind you and oscillate — pick the first point that is
|
||
**closer to the goal** than the current position. `_nav_try_frames`/`_move_direct` fallback is
|
||
still there.
|
||
- **Crowd reactions listen on the EventBus:** `CrowdSystem.apply_event()` emits
|
||
`event_bus.crowd_changed`; its own `changed` signal is never emitted. `GameManager._connect_crowd()`
|
||
therefore connects to `coordinator.event_bus.crowd_changed` (reconnect after every
|
||
`coordinator.setup()`, which recreates both crowd and event bus).
|
||
- **Crowd presentation:** `NpcAvatar.start_rubberneck(at)` (idle bystanders away from the door stop
|
||
and face the incident for 12 s) and `start_flee(from)` (panic: walk away at 2.4 m/s, then free
|
||
themselves). Someone `awaiting_interaction` within 3 m of the door is "engaged" and does not
|
||
rubberneck. Use `position` (not `global_position`) for that distance check so it also works in
|
||
headless tests outside the tree.
|
||
- **Perf:** a busy 16-NPC entrance measured ~1.2 ms/frame (824 fps with vsync off, 800 samples),
|
||
so LOD/culling is not warranted at `MAX_LIVE_NPCS = 16`. Re-measure before raising the cap.
|
||
- **Incident recording was dead:** `IncidentRecorder.start_incident()` was never called, so every
|
||
`add_event`/`add_access_evidence` silently no-opped and the review always showed 0 incidents.
|
||
`_begin_incident()` now starts one per focused NPC (`has_open_incident()` guard), verbs call
|
||
`_ensure_incident()`, and `ShiftCoordinator._finish_shift()` closes leftovers as `unresolved`
|
||
before building the review.
|
||
- **NPC theater (2026-10-31):** `NpcAvatar.theater_pose()` maps `intent` + `arrived` to
|
||
idle/sit/photo/film/perform. Pure helpers `crouch_for_pose`, `prop_lift_for_pose`,
|
||
`sway_for_pose` keep the math testable. Sit lowers the rig by 0.34 and swings the legs forward
|
||
(negative X pitch; positive looked like kneeling). `_photo_prop` is set for influencer/tourist
|
||
cameras, `_sign_prop` for protester/performer boards; their base Y values are captured at build
|
||
time so the animation offsets from rest.
|
||
- **Body-camera timeline (2026-10-31):** `src/game/ui/event_timeline.gd` flattens
|
||
`review.incidents.incidents[].events` into one time-ordered list (`flatten`), formats each row
|
||
with `describe`, and steps with Left/Right or the buttons. `ShiftCoordinator.started_at_ms`
|
||
gives the review an absolute start for mm:ss stamps. `GameManager._open_timeline` hides the
|
||
review dim (two stacked overlays made the screen nearly black) and `_timeline_from_review`
|
||
restores it on close. Smoke: `src/test/smoke_timeline.gd`.
|
||
- **Radio dispatch was dead after the first shift:** `coordinator.setup()` builds a new
|
||
`RadioSystem`, so the `responder_arrived` connection made in `_ready` was lost on every venue
|
||
switch/restart (responders never resolved anything). `_connect_radio()` now re-wires it next to
|
||
`_connect_crowd()` after every setup. Same class of bug as the crowd signal.
|
||
- **Emergency aftermath:** `ShiftCoordinator.responder_outcomes` / `investigations` feed the review.
|
||
`evacuation` (emergency call, low danger, no weapon) locks the door, makes every NPC flee, marks
|
||
them `leave_requested`, and applies a `security_failure` hit. An emergency placed while
|
||
`current_npc_truth` has no weapon and `danger_level <= 1` appends an investigation note, records
|
||
`investigation_opened`, and applies `false_report` -6.
|
||
- **Skyline backdrop (2026-10-31):** `PlaneMesh` defaults to a **horizontal** XZ plane — set
|
||
`mesh.orientation = PlaneMesh.FACE_Z` or the "backdrop" renders as a band overhead. The skyline
|
||
is now a ring of four 400x60 planes at 200 m (center y=30, so building bases meet the ground),
|
||
`TEXTURE_FILTER` default, `TRANSPARENCY_ALPHA_SCISSOR` with threshold **0.5** (0.3 left faint
|
||
mip dashes across the sky), unshaded with a lifted albedo (`Color(2.6,2.7,3.0)`) so it is not
|
||
pure black. Ground is 500x500 so no sky gap shows under the buildings. The `skyline.png` import
|
||
needs `mipmaps/generate=true`.
|
||
- **Asset gotcha:** `assets/textures/game_ready/` is *not* imported (no `.import`). Use the
|
||
imported `assets/textures/skyline.png` path. Untracked duplicate folder should not be referenced.
|
||
- Host sign-in follow-up remains deferred. PLAN.md + ADRs 0007–0010 still describe the intended UX.
|
||
- **Release v0.2.1 is LIVE** (2026-10-31): CI run 1110 built the NSIS installer + AppImage
|
||
and published — installers/AppImage are on the releases page. Releasing = bump version → tag → push.
|
||
- NSIS gotchas (validated in a local ubuntu:24.04 docker repro): makensis uses `-D` defines
|
||
(never `/D`), resolves relative `-DICON`/`-DARTIFACT` against the **script directory** so CI
|
||
passes `$PWD`-absolute paths; installed exe must be space-free (`step-line.exe`); ICO must be
|
||
classic BMP entries (ImageMagick `icon:auto-resize=48,32,16`; PIL emits PNG-compressed ICOs NSIS
|
||
cannot load).
|
||
- appimagetool runs system `appstreamcli` if present (strict, exit 3 on warnings) — project
|
||
metadata now passes `--pedantic`.
|
||
- Distribution facts (verified via Gitea API + host creds): repo is **public**, account is
|
||
admin; `v0.2.1` has all 5 assets; the installers were initially missing from the release
|
||
because `publish_release.sh` uploaded only portables — now fixed (uploads installer +
|
||
AppImage; falls back to `~/.git-credentials` when no `GITEA_TOKEN`).
|
||
- Host sign-in: instance `[service] REQUIRE_SIGNIN_VIEW=true` blocks anonymous browse/download
|
||
(not a repo/API setting). `~/.git-credentials` holds the Gitea creds (basic-auth usable);
|
||
`.env` only holds `OPENROUTER_API_KEY`.
|
||
- **Release v0.2.0 was LIVE**: pushing the `v0.2.0` tag made Gitea Actions run the build job
|
||
and publish automatically (host has the `GITEA_TOKEN` secret). Tag page and `/releases` are
|
||
public (HTTP 200). Caveat: anonymous **asset downloads** redirect to login on this host
|
||
(host setting — tracked in TODO). Future releases = bump `config/version`, `git tag vX.Y.Z`,
|
||
push. No local token needed.
|
||
- Core simulation, three scenarios, v0.2.1 installers/AppImage, first-person prototype (inspect/wand/radio, shift review, Tab targeting, V venue cycle) are on local `main`. Tagged v0.2.1 was 57 tests; this working tree is **74/74**.
|
||
- NPC avatars now walk to per-intent waypoints (`VenueScenario.waypoint_for_intent`): enter/trespass/deliver/arrive → entrance, photo/film/perform/wander/roam → sidewalk, sit → steps_end. `NpcAvatar.setup` takes an explicit `door_position`.
|
||
- Returning-trespasser behavior: `NpcProfile.return_probability` (trespasser 0.6, mental-crisis 0.4, others 0). `NPCController._maybe_schedule_return()` moves a directed-away NPC to `LOITERING_DISTANT`; `tick_return(delta)` fires after 20-60s and transitions back to `APPROACH`. `NpcAvatar._advance_fsm` re-targets the door on return.
|
||
- Perception pass: `NPCController.perceive_guard(distance)` returns `{noticed, band, reaction}` with bands `unaware` (>6m), `aware` (2-6m), `near` (<2m). Near + temperament ≥0.6 → ARGUE, else COMPLY. `GameManager._perception_pass()` feeds real guard distance each physics tick and logs first-time notice.
|
||
- Responder dispatch: `RadioSystem` tracks `pending_calls` and decrements `arrival_delay_s` in `tick`; emits `responder_arrived` when a call's delay elapses. `RadioSystem.responder_outcome(call)` maps danger/weapons → command/restraint/evacuation/arrest. `GameManager._on_responder_arrived` spawns a `RESPONDER` avatar at the entrance and logs the outcome. HUD shows an emergency-radio cooldown label via `_update_radio_cooldown`.
|
||
- Wand screening (Joke Shop): `WandScreener` classifies carried items as prohibited vs false-alarm vs clear. `NpcProfile.carried_items` is filled at spawn on wand venues. `GameManager` binds **`C`** to `_toggle_wand_screening` and `A`/`D` to `decide_wand` while inspect/wand is open. `decide_access` still blocks admission until `controller.screened` is true, and `verify` now takes that flag. Door-staff lines log through GameManager.
|
||
- Design change (2026-10-31): the selectable dialogue tone-of-voice system was **removed**.
|
||
`Rules.Tone`/`TONE_NAMES`, `NpcProfile.tone_modifiers`, and the tone param on
|
||
`NPCController.roll_reaction/apply_player_action` no longer exist; `NpcProfile.dialogue_lines`
|
||
is a flat `Array[String]`; `GameManager` uses `_apply_interaction(level, action_name)`;
|
||
the `T` key is gone. HUD `tone_label` removed and `hud.tone_prefix` string deleted from JSON.
|
||
- Polish/accessibility: venue cycling (`V` cycles `VENUE_ORDER` and restarts the shift), aim-free
|
||
targeting (`Tab` cycles nearby NPCs within `NEARBY_RADIUS`), color-coded escalation indicator
|
||
(`Hud.set_escalation`), procedural audio (`AudioFeedback` node generates sine tones for
|
||
confirm/radio/escalation/deny), and a centralized `Strings` table (`src/core/strings.gd`, `lookup`/`fmt`)
|
||
backed by `content/strings.en.json`. NOTE: `Strings.get` was renamed to `Strings.lookup` because
|
||
`get` collides with `Object.get`.
|
||
- Newly added: LATE shift phase (start 700s) with a reachability test; PROTESTER archetype + events + colors; CALL_SOMEONE reaction; `DoorState` core class + `L` binding + visual indicator; flashlight (`G`, SpotLight3D, perception bonus at evening/late); persistence wired (`Save.save_shift_result` on shift end, history + best score on the review screen); `NavigationRegion3D` flat navmesh + `NavigationAgent3D` NPC paths (steps now visual-only); strings moved to `res://content/strings.en.json` (41 keys, loader keeps `lookup`/`fmt`); `export_presets.cfg` (Linux/Windows/macOS) + `tools/build_releases.sh` + `docs/RELEASING.md`; README Downloads section.
|
||
- Run tests: `godot --headless --path . --script res://src/test/test_runner.gd`
|
||
- Boot check: `godot --headless --path . --quit-after 120`
|
||
- Screenshot tools: `godot --path . -s res://src/test/take_screenshot.gd` (single shot to `user://screenshot.png`); `godot --path . -s res://src/test/capture_screenshots.gd` (staged README gallery shots to `user://screenshots/`, copied to `docs/screenshots/`).
|
||
- Release build verified locally: export templates for 4.3 installed at `~/.local/share/godot/export_templates/4.3.stable/`; `./tools/build_releases.sh` produced Linux/Windows/macOS artifacts for 0.1.0 in `build/` and the Linux binary passed a headless smoke test. Publishing to Gitea still needs a token (see `tools/publish_release.sh`, `docs/RELEASING.md`).
|
||
- CI: the repo now carries a **Gitea Actions** workflow at `.gitea/workflows/build.yml` (checks on
|
||
push/PR; full export + artifact upload + optional release publish on tags and `workflow_dispatch`).
|
||
A repo secret `GITEA_TOKEN` is required for tag releases. The old `.github/workflows/ci.yml` was
|
||
removed so hosts scanning both paths do not double-run.
|
||
- Venue cycling: `V` switches Hollywood Blvd → The Joke Shop → TV Studio and restarts the shift.
|
||
- Venue door-staff dialogue: `VenueScenario.door_staff_line(context, rng)` and GameManager logging
|
||
(id/ticket/wand-check, refusal, prohibited-item) — Joke Shop flavor.
|
||
- CI cache: `.gitea/workflows/build.yml` no longer uses `actions/cache` (the runner can't reach
|
||
the GitHub cache service; import is fast). `GODOT_SILENCE_ROOT_WARNING=1` set on jobs.
|
||
Logs live in `.gitea/logs/` after each run.
|
||
- Visual polish (v0.2.0): phase lighting (GameManager.PHASE_LIGHTING + ProceduralSkyMaterial),
|
||
street lamps + night mode in WorldBuilder.set_night_mode, marquee Label3D, crosswalk/planters/
|
||
trees, NPC per-seed variation. capture_screenshots.gd shoots day + fast-forwarded LATE night
|
||
(6 shots in docs/screenshots/). Version bumped to 0.2.0; binaries rebuilt.
|
||
- Assets pipeline: `python3 tools/generate_textures.py` produces all authored PNGs under
|
||
`assets/textures/` (≤1024px, pow2); regenerate after changing the script, then run
|
||
`godot --headless --import` before tests. 57 headless tests guard the pipeline.
|
||
- General image pipeline: `python3 tools/asset_pipeline.py` defaults to
|
||
`openai/gpt-5-image-mini`, requests supported 1:1/3:2 sources, then fits each role's target.
|
||
It provides crop, resize, tile edge blending, alpha-glyph recoloring, and role-aware audits.
|
||
Screenshot auditing checks 1280x720 minimum, 16:9 aspect, and contrast; visual composition
|
||
remains a manual check. Python tools require Pillow from `requirements-tools.txt`.
|
||
- AI texture mode: `python3 tools/generate_textures.py --ai` generates textures via the
|
||
OpenRouter image API (`POST https://openrouter.ai/api/v1/images`, model
|
||
`bytedance-seed/seedream-4.5` by default, base64 PNG in `data[].b64_json`). Requires
|
||
`OPENROUTER_API_KEY` (a gitignored `.env` in the repo root holds it locally; source it
|
||
with `set -a && . ./.env && set +a`). `--prompt`/`--name`/`--model`/`--size` override the
|
||
default batch. Output is downscaled to ≤1024px. NOTE: seedream rejects sizes below
|
||
3,686,400 px (1920×1920), so the script requests `GEN_SIZE=2048` and downscales — do not
|
||
lower `GEN_SIZE` below 1920. Verified end-to-end 2026-10-31 (3 textures generated).
|
||
Legacy `--ai` remains on Seedream 4.5 and still ignores `background: transparent`; do not
|
||
use it for alpha-dependent assets. Gemini 3.1 Flash Image advertised broad ratios but the
|
||
current provider rejected authentication. The validated role-aware model is GPT-5 Image Mini.
|
||
- Runtime assets: all 15 loaded world/HUD/card textures now live in
|
||
`assets/textures/ai/game_ready/`; each passes role audit. Generated tile edges are blended,
|
||
icon alpha masks are recolored white where needed, and the vignette uses the existing
|
||
deterministic generator because the API returned an empty transparent image. The six
|
||
`docs/screenshots/` captures were regenerated after wiring and pass screenshot audit.
|
||
- Detailed graphics: textured facade/awning/skyline, humanoid NPC rigs + props + walk bob +
|
||
attention head-turn, inspection artifact cards, mode icons, escalation vignette, body-cam
|
||
overlay (REC + shift clock). Screenshot set regenerated (`docs/screenshots/`, day + night).
|
||
- Polish: wand scan cooldown (`WAND_COOLDOWN_S` + `wand.recharge` message).
|
||
- NOTE: collaborators occasionally add uncommitted work to this shared tree (e.g. the radio
|
||
easter egg); check `git status` before committing and leave foreign changes untouched unless asked.
|
||
|
||
- Screen-design references (`tools/screen_designs.py`, tests in `tools/test_screen_designs.py`):
|
||
`generate` turns the screen descriptions in `docs/` (README captions, ADRs 0007–0010,
|
||
PLAN, architecture) into reference images under `docs/screens/designs/` — `--draft`
|
||
draws offline Pillow wireframes, `--ai` generates realistic 3D scenes / in-game UI via
|
||
OpenRouter (default model `openai/gpt-5-image-mini`). Screens are tagged `scene` or
|
||
`ui`; prompts get a shared art-direction suffix, and requests use a 3:2 landscape
|
||
source cropped to 16:9 (not a square centre-crop). `compare` checks `docs/screenshots/*.png` for aspect/size/
|
||
blank-frame and coarse layout/palette/detail drift. Drift distances are warnings, not
|
||
failures: a mockup never matches a real render, so semantic composition is a follow-up
|
||
vision-model judge (tracked in TODO).
|
||
|
||
## Known Issues / Notes
|
||
|
||
- Reference cycles are fixed: `FSM` holds its owner via `WeakRef`, `ShiftCoordinator` is
|
||
`RefCounted`, and persistence tests free the `Save` node — the suite exits with no
|
||
"resources still in use" warnings (was ~13). If a future subsystem introduces a cycle,
|
||
repeat the WeakRef pattern.
|
||
- Shift length: `GameManager` uses the scenario's `shift_duration_s` (900s ≈ 10-15 min);
|
||
tests and the screenshot tool override elapsed time directly.
|
||
- Headless (dummy renderer) logs "Parameter m is null" from `mesh_get_surface_count`; it does not occur with a real GL driver and does not affect logic.
|
||
- Steps are visual-only (no collider) so NPCs can path to the entrance; the guard spawns on the landing so it never needs to climb.
|
||
- Persistence tests write into the real `user://shift_results.json` (harmless local pollution of the demo history).
|
||
- macOS export required `rendering/textures/vram_compression/import_etc2_astc=true` in `project.godot` (universal build demands ETC2 ASTC enabled).
|
||
- Joke Shop `door_staff_lines` are logged through `GameManager` (inspect / wand / refuse). They are not a separate dialogue UI.
|
||
- Access loadout is implemented in the working tree (see Current State); do not re-fix `override_access` as if it were still unread.
|
||
- `Save.SETTINGS_FILE` is 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: Dictionary` or Godot will not call them (`test_shift_coordinator_runs_a_shift` failed until `_on_spawn` took four args).
|
||
- GDScript inference trap that broke the main scene: a `var` typed as a base class
|
||
(`radial: CanvasLayer`) exposes script members as `Variant`, so `var x := radial.is_open` is a
|
||
parse error. Type it as the script type (`ActionRadial`) or annotate the local explicitly.
|
||
`--check-only --script` cannot see the `Save` autoload; use a real boot (`--quit-after 180`) to
|
||
verify a full-scene compile.
|
||
- Do not snapshot right after changing a transform in the same frame: `capture_screenshots.gd`
|
||
needs a state/frame gap or the capture uses the old camera orientation.
|
||
- `EventScheduler` probabilities are per simulated second, not per call. Never poll it per frame.
|
||
- Screenshot staging lives in `src/test/capture_screenshots.gd` (guard at z≈10.5 facing the door).
|
||
`docs/screenshots/` copies are manual: copy from `user://screenshots/`.
|
||
|
||
## Open Questions
|
||
|
||
- 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.
|
||
|
||
<!-- b64: RWFzdGVyIGVnZzogYSByYXJlIHJhZGlvIHRyYW5zbWlzc2lvbiAiwr9Ew7NuZGUgZXN0w6EgbGEgbW90YT8iIGZpcmVzIHZpYSBHYW1lTWFuYWdlci5fbWF5YmVfcmFkaW9fZWFzdGVyX2VnZyAoMi41JSBjaGFuY2UgYWZ0ZXIgYSBzdWNjZXNzZnVsIHJhZGlvIGNhbGwpLiBTdHJpbmcga2V5IHJhZGlvLmVhc3Rlcl9lZ2cgaW4gY29udGVudC9zdHJpbmdzLmVuLmpzb24uIERvIG5vdCByZW1vdmUu -->
|