v0.3.0: rendering pass, generated props, selection model and Web build
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.
This commit is contained in:
1 parent
f763cff744
commit
afa1d6beec
48 files changed
+2602
-147
No files matched your search
@@ -0,0 +1,48 @@
|
||||
# ADR-0011: Aim-free target selection, precision walk, and focus clearing
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-10-31
|
||||
|
||||
## Context
|
||||
|
||||
ADR-0007 and ADR-0008 split input into locomotion, look, and a focus cone. A pass over the resulting controls found four problems in play:
|
||||
|
||||
1. **The cone ignored what the player sees.** `NpcFocus.pick_index` returned the first head inside the cone in group order, not the one nearest the crosshair. Two people in the cone gave an arbitrary pick.
|
||||
2. **`Tab` cycled in spawn order.** `GameManager._cycle_nearby_target` walked the `npcs` array, so "next" meant "next spawned", not "next nearest".
|
||||
3. **A cycled target was dropped within a few frames.** The cone re-evaluated every physics frame and cleared a target that was not under the crosshair after the 8-frame miss grace. Aim-free selection therefore only worked while the player also aimed.
|
||||
4. **There was no way to dismiss someone.** `Esc` pauses the shift and right click only cancelled modals.
|
||||
|
||||
The movement pass also left one gap at the door: the only walking speed is 3.2 m/s, which overshoots when the player is trying to stand at a register or read an artifact at conversation distance.
|
||||
|
||||
## Decision
|
||||
|
||||
**Rank the cone by angle, then distance.** `NpcFocus.rank_indices` scores every head by its angle from the camera forward vector and breaks ties by distance; `pick_index` is the first ranked entry. The cone therefore picks the person closest to the crosshair, which is what the player is looking at.
|
||||
|
||||
**Make aim-free selection authoritative.** `GuardController.cycle_target(direction)` walks `nearby_candidates()` — everyone within 12 m, nearest first — starting from the current target and wrapping in both directions. The chosen avatar is *pinned*: while a pinned target is alive the cone does not re-evaluate, so `Tab` and the mouse wheel produce a stable selection the player can then act on.
|
||||
|
||||
**Bind the mouse wheel to the same ring.** `cycle_target_next` (wheel down) and `cycle_target_prev` (wheel up) are new input actions handled in `GameManager._handle_mouse_button`. `Tab` remains the keyboard path. The HUD prints how many people are in the ring (`hud.nearby`), so the player knows the control exists before they need it.
|
||||
|
||||
**Right click clears focus, with a short grace.** With no modal open, right click clears the target and suppresses cone acquisition for `FOCUS_SUPPRESS_S = 0.75 s`. Without the grace the cone re-acquired the same person on the next frame, which made the control feel broken. Any explicit pick (cone or cycle) cancels the suppression.
|
||||
|
||||
**Add a precision walk.** Holding `Shift` (`walk_slow`) multiplies the walk speed by `SLOW_WALK_FACTOR = 0.45`. This is a fine-positioning aid, not a sprint: PLAN.md deliberately excludes sprint and crouch, and this does not add a faster move.
|
||||
|
||||
Escape keeps its current meaning (close the top modal, otherwise pause). Selection is not overloaded onto the same key.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- The focused person is predictable: the one nearest the crosshair, or the one the player explicitly cycled to.
|
||||
- Aim-free play works: cycle, act, cycle again, without fighting the cone.
|
||||
- Right click has an unambiguous "never mind" meaning while remaining a modal cancel.
|
||||
- Standing on a mark while reading an artifact is possible.
|
||||
|
||||
### Negative
|
||||
|
||||
- Two more input actions and one more piece of guard state (`target_pinned`, `focus_suppress_s`).
|
||||
- A cycled target stays selected until the player clears it, acts, or the avatar is freed; it does not expire on its own.
|
||||
- The 0.75 s suppression is a feel constant that may need tuning against play testing.
|
||||
|
||||
## Follow-up
|
||||
|
||||
- The action wheel (ADR-0009) and inspection panels still take `A`/`D`; the wheel owns those keys while it is open, and the guard is frozen then, so no conflict is observable.
|
||||
@@ -0,0 +1,44 @@
|
||||
# ADR-0012: Surface material profiles and generated prop models
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-10-31
|
||||
|
||||
## Context
|
||||
|
||||
The procedural street was built from `BoxMesh` primitives with `StandardMaterial3D` defaults: albedo colour (or a texture) and everything else stock. Every surface therefore shared one roughness, street furniture was a set of scaled boxes, and the sky was a flat `ProceduralSkyMaterial` with no tonemapping, glow, or fog. Captures looked like a blockout: brick tiles were metres wide, the sidewalk blew out to white, the facade had no windows, and night mode only switched the lamp bulbs and the marquee.
|
||||
|
||||
Two constraints shaped the fix:
|
||||
|
||||
- The project renders with the **GL Compatibility** backend so it stays small and portable, which rules out features that only exist in Forward+ (`SSAO`, `SSIL`).
|
||||
- The repo must build offline in CI, so any asset step has to work without a network or an API key.
|
||||
|
||||
## Decision
|
||||
|
||||
**One material module.** `src/game/materials.gd` (`SurfaceMaterials`) owns a named profile table (`brick`, `concrete`, `asphalt`, `awning`, `stone`, `metal`, `painted_metal`, `wood`, `cloth`, `skin`, `glass`, `rubber`) with roughness/metallic pairs, plus the recipes the world and avatars use (`brick()`, `concrete()`, `asphalt()`, `awning()`, `flat()`, `emissive()`, `glass()`, `window_pane()`). The world builder, the NPC avatars, and the prop fallbacks all call it, so a surface has one definition.
|
||||
|
||||
**One environment tune-up.** `SurfaceMaterials.configure_environment()` applies ACES tonemapping, soft glow (for the marquee, lamp bulbs, and lit windows), aerial fog, and a mild contrast/saturation lift. `GameManager.PHASE_LIGHTING` now carries the per-phase values (`exposure`, `glow`, `fog_color`, `fog_density`, `fog_energy`, `contrast`, `saturation`, `ambient_energy`) next to the existing sun/sky entries, so a phase change still happens in one place. Features unavailable in Compatibility are ignored by the driver rather than gated in code.
|
||||
|
||||
**Generated prop models.** `tools/model_pipeline.py` describes street furniture (bollard, bench, bin, planter, newspaper box, hydrant) as boxes and cylinders and writes binary glTF (`.glb`) into `assets/models/generated/`. Godot imports each file as a `PackedScene`, and `WorldBuilder._add_prop_model()` instantiates it with a procedural fallback when the file or its import is missing. The build is deterministic and needs no network.
|
||||
|
||||
**AI only supplies colour.** `model_pipeline.py concept` asks an image model (OpenRouter, same backend as the texture tools) for one studio-style concept per prop. `dominant_colors()` reads the centre of that image and ranks colours by frequency weighted by saturation (a grey backdrop otherwise wins), and `assign_roles()` maps the result onto material roles by hue and value: darkest tone → metal, warm saturated tone → wood, light desaturated tone → stone, green tone → foliage. Colours are converted from sRGB to **linear** before they are written into glTF `baseColorFactor`, because the factor is linear space and writing sRGB made every prop render pastel.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Surfaces read as materials: brick has fine, correctly scaled tiling, asphalt is smoother than brick, glass is smooth and slightly metallic, and the night sky carries warm lit windows.
|
||||
- Street furniture is real geometry with per-part materials instead of scaled boxes, and it costs ~700 triangles for the whole set.
|
||||
- The prop pipeline works with no key and no network; the concept step is optional and only changes colours.
|
||||
- Tests can assert material profiles, environment values, and GLB structure without rendering.
|
||||
|
||||
### Negative
|
||||
|
||||
- The generated models are low-poly primitives, not sculpted assets; they are an improvement in silhouette, not photoreal props.
|
||||
- Two colour spaces now exist in the tooling. `model_pipeline.srgb_to_linear` must be used for every colour written into glTF, which is easy to forget.
|
||||
- Concept images are per-prop inputs that have to be regenerated (and reviewed) when the art direction changes; a poor concept now produces a poor palette.
|
||||
- Compatibility rendering means no ambient occlusion, so contact shadows rely on the directional light only.
|
||||
|
||||
## Follow-up
|
||||
|
||||
- A 3D-model API (image-to-mesh) could replace the primitive builder behind the same `assets/models/generated/<prop>.glb` contract without touching `WorldBuilder`.
|
||||
- Texture atlases for the props would let them share one material instead of one per part.
|
||||
Reference in new issue
Block a user