Files
security-game/MEMORY.md
T
zwitschi f5ad1642b5
Full build / Import + headless tests (push) Successful in 26s
Full build / Full desktop build + optional release (push) Failing after 3m31s
Add PLAN verbs and fix dead incident recording
Verbs (PLAN levels 1-3)
- Eye contact: own Level 1 radial slice, logged as "eye contact".
- Recognition is a player decision: "Recognize" sets NPCController.player_recognized,
  writes access evidence, and GameManager only then appends a RECOGNITION artifact.
  It is no longer implied by NPC type.
- Body camera: real flag (off by default) driving the REC overlay; B toggles it and
  a formal warning arms it automatically, writing a body_camera incident event.
- Blocking stations the guard 0.9 m from the person toward the entrance and turns
  to face them, instead of only emitting the escalation level.
- Request reason: Level 3 ask answered from the NPC's seeded dialogue_lines.
- Radial grows to 12 slices (174 px outer radius, centered labels).

Incident recording fix
- IncidentRecorder.start_incident() was never called, so add_event /
  add_access_evidence / add_escalation silently no-opped and the shift review
  always reported zero incidents. Incidents now open on focus (guarded by
  has_open_incident), verbs call _ensure_incident, and ShiftCoordinator closes
  leftovers as "unresolved" before building the review.

Copy and docs
- 9 new string keys (eye contact, body camera, recognition, reason, station).
- README controls, 26-step smoke checklist, test count, status lists.
- TODO section moved to DONE; MEMORY records the verb design and the recorder bug.

Verification: headless 98/98, smoke_plan_verbs.gd + smoke_session_shell.gd +
windowed smoke_focus_radial.gd all pass. Version bumped to 0.2.3.
2026-10-31 20:29:50 +01:00

20 KiB
Raw Permalink Blame History

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 crowd presentation (rubberneck + panicked NPCs), then emergency aftermath (evacuation, unjustified-call investigation).
  • 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.
  • 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.
  • 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.

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

  • 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.