Fix the portrait staging: face the rig toward the camera (the old yaw was 180
degrees out) and hide the focus disc, which was covering the face in close-ups.
Add the before/after Influencer portrait, fit measurements and judge scores in
docs/screens/realism/2026-11-01-gear. Re-run check_screenshots (11 good/0 bad);
headless 229/229.
Blender library
- Rebuild the face: sclera, iris and upper lid per eye, brow ridge, cheekbones,
nose bridge and tip, ears with lobes, split upper/lower lips.
- Give each hand four fingers plus a thumb and end every sleeve in a cuff ring.
- Raise CHAR_MAX_VERTS 4000 -> 5000 (16 avatars still cost ~6.9 ms of a 50 ms
frame), decimate the body to 26 %; characters are now 3,974-5,026 tris and
3,744-4,807 verts. Rebuild all 8 variants and re-bake their AO maps.
Capture tooling
- Add a `portrait` stage to capture_realism.gd: freeze one NPC, park the crowd
out of frame, stand the subject on open sidewalk, turn it into the camera and
put the guard on the sunlit side. New --portrait-type=N picks the role.
- Add the npc_portrait_day concept and map the shot to it, so avatar detail is
scored against a portrait instead of a wide street frame.
Flashlight
- Drop the fake additive beam (Materials.light_beam + the cone mesh): on GL
Compatibility a first-person camera inside the cone renders it as a filled
disc with a bright far ring. Keep the tight spot (14 deg, 20 m, energy 4.2)
and its headless test; the visible shaft is now planned behind a Forward+
preset.
Docs and task tracking
- Move the finished realism follow-ups (ground exposure, flashlight, NPC detail,
judge re-check) to DONE.md and expand TODO.md with step-by-step plans for NPC
gear fit, venue geometry plus the committed gallery, and the Forward+ preset.
- Record the decisions, gotchas and portrait recipe in MEMORY.md; update the
Blender contract, the concept README and the capture section of README.md.
- Refresh the gallery captures affected by the avatar rebuild.
Checks: headless 228/228, Python 86/86, blender_assets audit exit 0,
scene_concepts audit all GOOD, screen_designs compare 11 good / 0 bad,
smoke_npc_budget OK, boot clean (only the known headless mesh warning).
Narrow the spot to 11° with a 20 m range so it lights the person/door rather
than the whole facade, and regenerate the staged screenshots. Headless 227/227,
compare 11 good / 0 bad.
A GradientTexture1D varies along U, so the cone showed a bright ring; build a
1×64 vertical ramp for UV.y instead (faint at the lens, brightest near the
start, gone before the tip). Regenerate the affected captures; headless 227/227,
compare 11 good / 0 bad.
Jump the staged shift to 760 s before the night shots, so 05/06 are true night
scenes with a visible flashlight beam instead of dusk. Regenerate the affected
captures; compare stays 11 good / 0 bad.
Widen and lengthen the additive beam cone and raise its near-end opacity so the
night flashlight reads on screen. Regenerate the staged captures; compare stays
11 good / 0 bad, headless 227/227.
- add the v2 runtime picks: per-phase skyline panoramas, brick, sidewalk
pavers, dry/wet asphalt, awning canvas, and their normal maps
- add the realism tooling and evidence: tools/scene_concepts.py, capture_realism.gd,
concept frames, reports/sheets, and tests
- retune phase lighting/fog/exposure, add sun yaw, brighten the night glow,
add the flashlight beam mesh, and rebuild the planter/tree assets
- regenerate all eleven staged screenshots from the current build; compare is
11 good / 0 bad
- finish the visual-alignment closeout in DONE/TODO, update architecture,
blender-assets, PLAN, and ignore rules (editable Blender sources untracked)
- keep the judge retry and document the verified default judge model
Verification: headless 227/227; Python tooling 86/86; check_screenshots 11/11.
Phase 4 of the v0.6.0 graphics plan:
- tools/bake_ao.py bakes 256² AO maps for every Blender character/gear/prop
(Cycles, existing UVs) next to each GLB; Materials.apply_ao() wires the red
channel into character slots, gear, and props. Compatibility has no SSAO.
- Regenerated brick/concrete/asphalt at 1024² with OpenRouter, derived tangent
normal maps via tools/make_normal_maps.py, and wired them through
SurfaceMaterials.textured() without changing UV scales.
- Evaluated Forward+: boots on desktop Vulkan, but the renderer is project-wide
and Web must stay GL Compatibility; v0.6.0 keeps Compatibility (decision in
MEMORY/plan). A desktop-only override is deferred.
- Recaptured screenshots; compare 11 good / 0 bad. Fixed marquee text alignment.
- Tests: baked AO maps exist for all 46 assets; prop/character AO material
checks; headless 222/222, Python 69/69, all smokes pass.
- Version 0.6.0; ADR-0013 accepted; README/architecture/blender-assets/plan and
TODO/DONE updated.
NpcAvatar
- select one of 8 library character variants by role/seed (with npc_base and the
procedural rig as fallbacks), keep Rig/Head and the existing bone animation
- tint only skin/cloth/pants/accessory/hair; fixed detail materials keep their
colour, npc_vest keeps its hi-vis vest, firefighter/guard use dark uniform tints
- attach caps, hoods, helmets, backpacks, SCBA tanks, lanyards and hand items at
the documented head/back/chest/hand points (primitive fallbacks retained)
WorldBuilder
- prefer assets/models/blender/props/ for the six replacements and the new
street dressing (street lamp/tree, palms, planters, A-frame, cone, barrier,
stanchion, podium, handrail, wall lantern, double door, marquee, sedan)
- fit the same colliders/navigation obstacles and keep the placement validator;
collect library emissives (bulbs, marquee face, wall lanterns, door LED, sedan
lights) for set_night_mode()
- add a library triangle counter + 60k budget check (current ~27,986); toggle
prop gallery to the library models; fix the marquee label placement
Tests/smoke
- character selection/gear attach/fixed-material tint tests; library prop load,
emissive and triangle-budget tests; smoke_props routes a guest around a
stanchion in open sidewalk (the old bench route is now crowded by trees)
- headless 222/222, Python 68/68, all smokes pass, compare 11 good / 0 bad
Docs: TODO Phase 3.5 moved to DONE, README/architecture/blender-assets/plan
describe the integrated runtime, MEMORY records the integration rules.
Wire the skinned model from ADR-0013 into NpcAvatar:
- _build_skinned_body() instantiates the imported npc_base.glb, binds the 14
armature bones, and keeps the procedural _build_body() as the offline
fallback (use_skinned_model can force it in tests)
- outfit_for()/layer_for() tint the five material slots through the new
SurfaceMaterials.npc_slot(); accessories and hand items attach under Rig/Head
- the existing limb pivots (Hip/Knee/Shoulder/Elbow/Hand + Head marker) keep the
old animation math; _sync_skeleton() copies their rotations to bones. Bone
pose rotation is absolute local orientation, so it is composed with the rest
basis (otherwise the whole body snaps into a broken pose)
- skinned rigs scale uniformly (non-uniform parent scale shears the skin) and
keep Rig/Head for the focus cone, nametag, and reaction tag
- commit the editable npc_base.blend plus a .gdignore: Godot's editor otherwise
tries to import the blend and hangs during --import
- npc_pipeline audit tests (contract, textures, clean GLB, committed source)
Verification: headless 212/212, Python tooling 60/60, NPC budget 16 avatars =
144 meshes at ~6.9 ms, movement/focus/props smokes pass, compare 11 good / 0 bad.
TODO Phases 0-3 checked; environment lift and release remain.
Update the game name in project.godot, export presets, UI/marquee/title strings, packaging (AppImage/NSIS/desktop/appdata), tools, docs, and the generated prop GLBs. Packaging assets and README screenshots are regenerated. Headless suite 209/209 and Python tooling tests pass.
Ten of the eleven README captures are refreshed (the prop gallery is unchanged). README gains an NPC character-model subsection and a 'Confirmed game assets' section with linked, audited textures/models; architecture Level 2 lists npc_pipeline.py and asset_pipeline.py.
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.