NPC characters
- Articulated rig (hips/knees, shoulders/elbows/hands) replaces rigid capsules;
the walk cycle bends knees and elbows and flees with a longer stride.
- Layered outfits (jacket/vest/coat/dress), extended accessories and hand items
(bag, umbrella, phone, camera, clipboard, cup), face details and five hair
silhouettes, all deterministic per profile seed.
- Idle behaviour layers (phone, clipboard, crossed arms, look-around), breathing
and weight shift; escalation raises hands or points, and noticed NPCs turn
toward the guard.
- Reaction symbols above the head (!!, ?!, !, ?), door-queue offsets assigned by
GameManager, three LOD tiers with hysteresis and a billboard impostor.
- smoke_npc_budget.gd: 16 avatars = 534 meshes, ~6.9 ms average (50 ms budget).
Player movement
- Separate accel/decel, step-up assist for kerbs and the entrance steps, and a
forward-axis fix: W was inverted (get_axis(forward, back) is negative).
- Head bob, landing dip and precision-walk FOV, gated by persisted head-bob and
invert-Y settings; precision chip and tick; surface-aware footsteps and a
landing cue forwarded to AudioFeedback.
- Gamepad support (sticks, deadzone, face buttons, bumpers), a control legend
that follows the active device, and named actions for the remaining keys.
- Key/button remapping in Settings, persisted as input_bindings and applied by
InputBindings (mouse and gamepad axes are never removed).
- smoke_movement.gd (windowed): climbs the steps, checks precision speed, chip,
footsteps and landing; new 11-precision-walk capture and catalogue entry.
Verification: headless 208/208, Python tooling 56/56, all 11 smokes pass,
compare 11 good / 0 bad, movement judge match. Version 0.5.0. Only optional
Future ideas remain in TODO.
Props were decoration: the guard and guests walked straight through benches,
bins, bollards and planters. They now block movement and steer guests around.
Collision
- _add_prop_model() fits a StaticBody3D from the model's mesh bounds in
holder-local space: a cylinder for bollard/bin/hydrant, a box otherwise, so
placement rotation and variant scale carry over.
- Fallback primitives go through the same fit, so a missing .glb cannot silently
remove collision.
- Each prop also publishes a NavigationObstacle3D; NpcAvatar enables agent
avoidance and moves with the safe velocity from velocity_computed. The navmesh
stays a flat walkable plane and avoidance does the steering.
Placement
- PROP_PLACEMENTS groups furniture 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.
- variant_for(index) gives deterministic yaw jitter, subtle scale, and optional
clutter (bag by the bin, crate by the newspaper boxes, cone by the hydrant).
Verification
- New windowed smoke_props.gd: guard walks into a bench and stops; a guest
headed beyond the bench routes around it and never enters the collider.
- Headless 182 -> 187: placement validation and its failure modes, variant
determinism/range, collider + obstacle on every placed prop, fallback collider.
- Other smokes (PLAN verbs, crowd, emergency, timeline) still pass; Python
tooling 53/53; screenshots recaptured; compare 9 good / 0 bad.
- Version 0.4.0. TODO: collision/placement section moved to DONE; prop pipeline
checks remain.
The three newest catalogue screens had AI mockups but no staged capture, so
compare and judge skipped them. capture_screenshots.gd now continues past the
night set:
- switches to The Joke Shop at 620 s (EVENING still has routine spawns), frames a
guest, and retries the wand until it alerts; if nobody carries an alerting item
it stages the documented keys false-alarm case.
- applies a formal warning (arms the body camera) then blocking, jumps to 760 s
for night lighting, and captures the HUD at L3.
- closes the open incident and opens the body-camera timeline, which now shows
the warning / body-camera / blocking rows.
Wired the refs for wand_screening, escalation_hud and body_camera_timeline, and
regenerated the escalation mockup as a third-person over-the-shoulder night shot
so the reference matches the game camera. Added Hud.clear_log() to drop
target-cycling noise from staged shots. README gallery lists the three captures.
Verification: headless 182/182, Python tooling 53/53, compare 9 good / 0 bad,
judge: wand match 0.90, timeline match 1.00, escalation review, 0 diff.
Version 0.3.3.
The design catalogue grew to 19 screens, but only the original twelve had an AI
reference; the rest still showed "design -" in `list`. Generated the missing set
so every screen can be reviewed against an intended look:
- wand screening (metal-detected panel with carried items and allow/deny)
- Joke Shop queue, TV Studio arrival, crowd panic, responder arrival
- body-camera timeline, escalation HUD
`generate` now loads `.env` the same way `judge` does, so `--ai` no longer needs
the API key exported by hand. Each reference is 1280x720; the twelve existing
mockups were kept (no `--force`).
Verification: `list` reports 19 designs, Python tooling 53/53, headless suite
182/182, `compare` still 6 good / 0 bad on the captured screens. Version 0.3.2.
Each generated prop used one material per part (body, accent, metal...). The
model pipeline now packs a prop's palette into one power-of-two colour atlas plus a
matching metallic/roughness atlas and emits a single material:
- atlas_for(): 8x8 cells per palette role, colours in sRGB, roughness in the G and
metallic in the B channel of the second atlas; NEAREST sampling keeps cells flat.
- build_glb(): every part merges into one primitive with constant per-vertex atlas
UVs; the material uses baseColorTexture + metallicRoughnessTexture with factors 1.0.
- audit_glb(): fails unless a prop has exactly one material, both atlas textures,
two images, and TEXCOORD_0 on every primitive.
- Removed the now-unused sRGB->linear factor helpers; colour travels in the texture.
Verification: headless 181 -> 182 (new "one material per prop scene" test), Python
tooling 53/53 (atlas determinism/size/UV/palette-override tests replace the old
factor tests), model audit OK for all six props, screenshots regenerated and
screen_designs.py compare still 6 good / 0 bad. Version 0.3.1.
Rendering and presentation
- Shared surface-material module (src/game/materials.gd) with per-surface
roughness/metallic/uv profiles; ACES tonemapping, glow, fog and phase-driven sky
energy. WorldBuilder rebuilt on those profiles with facade windows that light up,
door trim/handles, curb, lane dashes, bollards, bench, bin and planter foliage.
- NPC silhouettes: per-role clothing palettes, caps/hoods/backpacks/sunglasses,
hands and shoes, arm/leg walk swing.
- Six generated low-poly GLB props (bollards, bench, bin, planter, newspaper box,
hydrant) built by tools/model_pipeline.py, loaded by WorldBuilder with a
procedural fallback; ADR-0012.
Selection and movement
- Focus ranking by view angle then distance (NpcFocus.rank_indices); Tab and the
mouse wheel cycle nearby people initially, right click clears focus with a
re-acquire grace, HUD shows the candidate count, Shift walks at half speed.
ADR-0011. Windowed smoke extended to cover wheel cycling and clearing.
QA tooling
- screen_designs.py catalogue grown to 19 screens (8 scene, 11 UI) plus a `list`
subcommand; draft generation keeps existing AI references unless --force.
Web export
- New Web preset using the non-threaded Godot template (static hosting, no
COOP/COEP). tools/build_releases.sh exports build/web and packages
build/step-line-web-<ver>.zip; publish_release.sh uploads it with the desktop
artifacts. test_web_export.gd covers the preset and release wiring.
Verification
- Headless suite 177 -> 181, Python tooling 53 tests, all eight smokes pass
(including the windowed focus/wheel smoke). Web bundle serves index.html,
index.wasm (35 MB) and index.pck (30 MB) with HTTP 200 from a static server.
- Version 0.3.0; README, PLAN, RELEASING, TODO/DONE and MEMORY updated.
- tools/export_venues.gd (SceneTree script) saves the three factory scenarios to
content/venues/<id>.tres via ResourceSaver. The factory stays the source of truth;
the exported files are what an editor workflow would edit and ship.
- New tests load the files back and check that id, name, event pool, profiles,
policy (ID/wand), equipment, door-staff lines, shift phases, and locations all
survive. They also fail when the exported files are missing or stale, so the
export is re-run after content changes.
- GUT is consciously not adopted: the custom headless runner already covers 157
assertions without an editor plugin and keeps CI dependency-free. Recorded in
DONE.md.
- TODO: the planned backlog is complete; only optional future ideas remain.
- README/MEMORY updated. Version 0.2.16.
`tools/screen_designs.py judge` sends each capture together with its concept
mockup to a vision model and asks whether the capture realises the same screen and
layout intent. This covers what pixel/perceptual metrics cannot judge.
- Prompt explicitly ignores art style, fidelity, fonts, and placeholder text, since
the references are stylised mockups.
- Verdicts are classed match / review (mismatch below --fail-confidence, default
0.6) / diff (confident mismatch, exits non-zero) / unknown; --json, --only,
--dry-run, --model supported.
- Reads the repo .env for OPENROUTER_API_KEY without overriding real env vars.
- Default judge model google/gemini-2.5-flash-lite; openai/gpt-4o-mini verified too.
Tests/docs
- 7 new offline tests in tools/test_screen_designs.py (request payload embeds both
images, JSON/fenced/parts/non-JSON parsing, threshold classification, .env
loading); 30 Python tests pass.
- Live run: 3 captures match, 3 review, 0 confident diffs.
- README, TODO section moved to DONE, MEMORY. Version 0.2.15.
Host (applied with root@192.168.88.30)
- [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 at full size (50.8 MB AppImage).
Localization safety (Open Polish)
- Strings.placeholder_count() counts conversions and ignores %% escapes.
- Strings.fmt() validates the count and fails safe (warns, returns the template)
instead of raising an engine error when a key's placeholders drift.
- Strings.validate() lints the table for a trailing %, unsupported conversions, and
mixed positional/sequential placeholders.
- Tests cover the counter, the fail-safe path, a clean table, and a formatting pass
over every placeholder key with type-correct arguments.
Docs
- TODO: distribution, release-pipeline, and open-polish sections are complete and
moved to DONE; the v0.2.4-v0.2.8 backfill is closed as superseded.
- README/MEMORY updated. Version 0.2.14.
Joke Shop
- Reservation list without ID (guest list valid, ID missing) — denied.
- Borrowed/mismatched ID (ID valid but mismatched) — denied.
- Guest who refuses the wand screen: new `refuses_screening` event flag, copied
onto NPCController, handled in _toggle_wand_screening (logs wand.refused, records
a wand_refused event, small prop hit, panel stays closed).
- Late arrival during the performance (ID + ticket, "arrive" intent).
Studio
- Filming-boundary event and a later repeat, both using the new `film_boundary`
intent which maps to `steps_end` so the influencer stands past the posted line.
- Celebrity arrival now pulls in a small crowd via a new `extra_spawns` event field
(two tourists + an influencer), spawned through the normal path.
Building
- Filming-boundary event whose hint points at the body camera and request-reason
verbs, plus an unclear-intent event that hints at asking for a reason.
Engine support
- Events may carry `hint` (string key logged on spawn), `extra_spawns`, and
`refuses_screening`; 9 new localized hint strings.
Tests/docs
- Headless suite 139 -> 149 (test_scenario_content.gd) and a new
smoke_scenario_content.gd that spawns the refusal and celebrity events through the
real signal path.
- README, TODO section moved to DONE, MEMORY. Version 0.2.13.
- Host root filesystem was expanded from 7.8G to 16G (~7.8G free), so the disk
blocker is gone.
- Re-published v0.2.0 (4 artifacts) and v0.2.9 (5 artifacts) from the local build/
outputs; both releases verify.
- Root SSH from the workstation is not available (publickey denied, password auth
closes the banner); the host is administered via the zwitschi login.
- Version 0.2.12 exists to verify the tag pipeline end to end: the workflow should
create the release via the checkout-credential fallback and upload all five
artifacts.
The v0.2.10 tag build still failed to publish: its env dump showed no token at all,
because this Gitea/act runner does not provide `github.token`, and the repo has no
GITEA_TOKEN secret.
publish_release.sh now resolves credentials in order: GITEA_TOKEN,
GITEA_AUTH_HEADER, ~/.git-credentials, then the local git extraheader that
actions/checkout writes for the job. That header carries the runner's basic-auth
credentials for the same server, so tag builds publish with no repo secret.
Verified by adding the extraheader locally and publishing v0.2.10 (all five
artifacts: Linux/Windows/macOS ports, NSIS installer, AppImage), then removing it.
Docs: RELEASING describes the four credential sources and warns that github.token is
not available here; MEMORY records both failed attempts and the final fix; TODO keeps
the "verify on the next tag" follow-up. Version 0.2.11.
Tag builds since v0.2.2 built all artifacts but never published a release:
- tools/publish_release.sh only assembled AUTH in the GITEA_TOKEN branch; the
~/.git-credentials branch set CRED and then used an unbound ${AUTH[@]} under
`set -u`, so every API call failed and the job ended "could not create or find
release". It now sets AUTH=(-u "$CRED").
- The repo has no GITEA_TOKEN secret and the runner has no git credentials, so the
workflow now falls back to the built-in workflow token:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN || github.token }}.
v0.2.9 was then built and published locally with all five artifacts (portable
Linux/Windows/macOS, NSIS installer, AppImage) and the release page verified via
the API. v0.2.4-v0.2.8 remain unpublished; they are superseded by the latest.
Docs
- docs/RELEASING.md records the incident and the local-publish fallback.
- TODO gains a release-pipeline follow-up (verify CI on the next tag).
- MEMORY notes the root cause, the missing secret, and which releases exist.
- Version 0.2.10.
- Escalation cue now includes a shape meter and numeric level
("Escalation: Blocking [###--] L3/5") in addition to the colour and name, so it
reads in grayscale. Hud.escalation_meter(level) is pure and tested.
- HUD font scale from settings actually works: set_font_scale() resizes fonts via
theme overrides instead of Control.scale, which pushed the right-anchored clock
label off-screen. It now covers the top, log, target, hint, time, mode,
escalation, cooldown, and debug labels.
- Settings load before the HUD exists, so _ready() re-applies player settings after
_build_ui(); previously a saved font scale was ignored until the next apply.
Tests/docs
- Headless suite 134 -> 139 (test_accessibility.gd).
- Verified visually at 1.4x scale: no overflow, meter readable.
- README, TODO section moved to DONE, MEMORY. Version 0.2.9.
- AudioFeedback.start_crowd_bed() builds a 4 s looping AudioStreamWAV at runtime
(one-pole filtered noise plus a 68/103 Hz drone, seeded) and set_crowd_level()
maps crowd mood 0..1 to -34..-12 dB, so the street gets louder as it gets
agitated or panics. The bed starts with the shift and stops at shift end.
- play_radio_open()/play_radio_close() push short filtered-noise bursts; wired to
opening the radio panel, submitting a report, cancelling, and closing the panel.
- Still asset-free and headless-safe: with a dummy audio server the calls no-op.
_free_player() returns null before _ready and callers handle it.
Tests/docs
- Headless suite 127 -> 134 (test_audio_feedback.gd).
- New smoke_audio.gd: bed playing, level follows mood events, radio crackles once
per open/close, bed stops at shift end. Verified windowed (real audio server).
- README, TODO section moved to DONE, MEMORY. Version 0.2.8.
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.
- New src/game/ui/event_timeline.gd flattens every recorded incident event
(ADR-0005) into one time-ordered list and shows time, person, and a one-line
summary. Step with Left/Right or the buttons; Esc returns to the review. No live
3D replay in this first version.
- Shift review gains a "Body camera timeline" button that hands the review to the
timeline; the review dim is hidden while it is open (stacked overlays made the
screen nearly black) and restored on close.
- ShiftCoordinator records started_at_ms so the review can stamp events as mm:ss.
- 8 new string keys for the timeline UI and the review button.
Tests/docs
- Headless suite 113 -> 120 (test_event_timeline.gd), new smoke_timeline.gd that
opens the timeline from the review, steps through every event, and closes back.
- README, TODO section moved to DONE, MEMORY notes. Version 0.2.6.
- Responder outcomes now reach the shift review
(ShiftCoordinator.responder_outcomes), and the review screen names them.
- "evacuation" (emergency call, low danger, no weapon) actually clears the entrance:
the door is locked, every NPC flees and is marked as leaving, and a
security_failure reputation hit applies.
- An emergency call the situation did not justify (no weapon, danger <= 1) appends
an investigation note, records investigation_opened, and applies the existing
false_report hit. The review lists the investigations.
- Fix dispatch dying 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. _connect_radio() re-wires it beside _connect_crowd().
Tests/docs
- Headless suite 105 -> 113 (test_emergency_aftermath.gd), new smoke_emergency.gd
covering the investigation, evacuation aftermath, and review copy.
- README controls/status/smoke checklist, TODO section moved to DONE, MEMORY notes.
- Version 0.2.5.
NPC navigation (previously dead in the live game)
- Only assign NavigationAgent3D.target_position when it changes: re-assigning every
frame invalidated the path, so get_next_path_position() kept returning the current
point and avatars never moved.
- Follow the first path point that is closer to the destination than the avatars are.
Nav points sit on the navmesh plane (y=0) while bodies stand at y~0.5, so the old
3D distance test always looked "already there"; path[0] is also the old start point,
so a naive "far enough away" pick steered backwards and oscillated.
- Fix fsm.state -> fsm.current_state in the idle-lifetime check.
Crowd presentation
- Aggressive behavior, visible weapons, and responder arrivals make idle bystanders
stop and face the incident for 12 s; someone waiting at the entrance keeps focus.
- Panic (mood >= 0.75) walks people away from the door at 2.4 m/s, then they despawn.
- Wire reactions to EventBus.crowd_changed: CrowdSystem.apply_event() emits on the bus,
not on its own `changed` signal (the first wiring received nothing).
- Measured a 16-NPC entrance at ~1.2 ms/frame (824 fps, vsync off), so no LOD/culling.
World and HUD
- Skyline is now a ring of four alpha-scissored planes at 200 m instead of a single
plane; PlaneMesh defaults to horizontal, which is why the backdrop rendered as a band
overhead. Larger ground plane, mipmapped texture, scissor threshold 0.5 (0.3 left
mip dashes in the sky), lifted albedo so the city is not pure black.
- Flashlight aims straight ahead and lights the person/door instead of the ground.
- Shorter HUD controls line so it stops overlapping itself.
Tests, tooling, docs
- Headless suite 98 -> 105 (new test_crowd_presentation.gd, 7 tests).
- New smoke_crowd.gd: rubbernecking, panic, and flee-target sanity.
- capture_screenshots.gd starts a shift from the venue menu and stages the night set
properly; all six README screenshots regenerated.
- README, TODO/DONE (crowd section moved), MEMORY updated. Version 0.2.4.
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.
- Venue-correct access artifacts (AccessLoadout) and screened-aware verify
- Cone focus on NPC heads with billboarded ring + display name (ADR-0008)
- Action wheel with de-escalation-first slices (ADR-0009); Esc/RMB cancel
- Procedural NPC face/hair/neck/shoes (ADR-0010); named input actions
- Fix GameManager parse error (radial typed as ActionRadial)
- Fix first-person camera height and guard spawning inside the building
- Move building volume back so the door/awning/marquee are visible
- Pull the event scheduler once per simulated second; cap live NPCs
- Regenerate README screenshots; update TODO/DONE/MEMORY/README; 77 tests
Adds src/test/smoke_focus_radial.gd for the windowed focus/radial smoke.
- Add a 'Verify Windows installer contents' step (7z l shows Step Line.exe +
uninstall.exe) so the TODO verification items are proven by CI output
- Rebuild 0.2.1 binaries (embedded PCK) and AppImage locally; smoke clean;
57/57 tests
- Phase-based lighting: sun direction/energy/color, ProceduralSkyMaterial
gradient, and ambient per shift phase (GameManager.PHASE_LIGHTING),
applied on phase transitions and shift start
- WorldBuilder: street lamp poles with emissive bulbs + OmniLights,
set_night_mode(), glowing STEP LINE marquee (Label3D + emissive bars),
crosswalk stripes, planters, low-poly street trees
- NPC avatars: per-seed height/brightness variation (deterministic LCG)
- Denser routine traffic probabilities
- capture_screenshots.gd: fast-forward to LATE phase and capture night set;
README gallery is now 2x3 day+night; docs/screenshots regenerated
- Version bumped to 0.2.0; build/ artifacts rebuilt; Linux binary smoke-tested
- New headless tests for phased lighting config and street dressing (56 total)
- Install Godot 4.3 export templates locally and fix macOS export
(enable rendering/textures/vram_compression/import_etc2_astc)
- Build Linux/Windows/macOS artifacts into build/ via tools/build_releases.sh;
Linux binary passes headless smoke test
- Add tools/publish_release.sh (creates Gitea release + uploads assets with a token)
- TODO: mark build/install steps done; publishing v0.1.0 + link verification
remain as the single action blocker (needs GITEA_TOKEN)