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.
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.
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.
- 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.
- makensis resolves relative -DICON/-DARTIFACT against the script directory,
so CI now passes $PWD-based absolute paths (reproduced locally in the same
ubuntu:24.04 container; installer builds, 7z shows step-line.exe +
uninstall.exe)
- Install as step-line.exe (no spaces: NSIS /oname splits on whitespace)
- OutFile overridable via -DOUT
- tools/generate_textures.py icon.ico now produced via ImageMagick with
classic BMP entries (NSIS-compatible); PIL PNG-compressed ICOs rejected by
NSIS
- Regenerate icon.ico with classic BMP entries (16/32/48) — PNG-compressed
ICO entries can fail NSIS' icon loader on some builds
- CI copies assets/textures/icon.ico onto the packaging path right before
makensis, removing any stale/missing-copy failures
- Recreate v0.2.1 tag after this fix
- 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)