v0.3.0: rendering pass, generated props, selection model and Web build
Full build / Import + headless tests (push) Successful in 36s
Full build / Full desktop build + optional release (push) Successful in 4m17s

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:
zwitschi committed 2026-10-31 23:07:41 +01:00
1 parent f763cff744
commit afa1d6beec
48 files changed
+2602 -147

No files matched your search

+77
View File
@@ -0,0 +1,77 @@
extends RefCounted
## Headless tests for the generated prop models (tools/model_pipeline.py) and
## their placement in the procedural world.
const WorldBuilder := preload("res://src/game/world_builder.gd")
const MODEL_DIR := "res://assets/models/generated/"
func test_prop_glb_files_exist() -> bool:
var missing: Array[String] = []
for prop in WorldBuilder.MODEL_PROPS:
if not FileAccess.file_exists(MODEL_DIR + str(prop) + ".glb"):
missing.append(str(prop))
return TestUtils.check(missing.is_empty(), "all prop models built (missing: %s)" % str(missing))
func test_prop_glb_files_are_imported_scenes() -> bool:
## `load()` only returns a PackedScene once Godot has imported the `.glb`;
## a null here means the checkout is missing the import or the model is invalid.
var problems: Array[String] = []
for prop in WorldBuilder.MODEL_PROPS:
var path := MODEL_DIR + str(prop) + ".glb"
var packed := ResourceLoader.load(path) as PackedScene
if packed == null:
problems.append("%s not imported as a scene" % prop)
elif not FileAccess.file_exists(path + ".import"):
problems.append("%s has no .import file" % prop)
return TestUtils.check(problems.is_empty(), "prop models import as scenes (%s)" % str(problems))
func test_prop_scenes_instantiate_with_meshes() -> bool:
var problems: Array[String] = []
for prop in WorldBuilder.MODEL_PROPS:
var packed := ResourceLoader.load(MODEL_DIR + str(prop) + ".glb") as PackedScene
if packed == null:
problems.append("%s: no scene" % prop)
continue
var instance := packed.instantiate()
var meshes := instance.find_children("*", "MeshInstance3D", true, false)
if meshes.is_empty():
problems.append("%s: no mesh instances" % prop)
instance.free()
return TestUtils.check(problems.is_empty(), "every prop scene has geometry (%s)" % str(problems))
func test_world_places_generated_props() -> bool:
var root := Node3D.new()
var builder := WorldBuilder.new(root)
builder.build()
var detail := root.get_node_or_null("StreetDetail")
var a := TestUtils.check(detail != null, "StreetDetail exists")
var placed: int = detail.get_child_count() if detail else 0
var b := TestUtils.check(placed >= 10, "street furniture placed (%d)" % placed)
var c := TestUtils.check(builder.loaded_models.size() >= WorldBuilder.MODEL_PROPS.size(),
"generated models loaded instead of fallbacks")
var d := TestUtils.check(builder.fallback_props.is_empty(),
"no procedural fallback used (fallbacks: %s)" % str(builder.fallback_props))
root.free()
return a and b and c and d
func test_world_facade_windows_light_at_night() -> bool:
var root := Node3D.new()
var builder := WorldBuilder.new(root)
builder.build()
var a := TestUtils.check(builder._window_mats.size() >= 8, "window panes built")
builder.set_night_mode(true)
var lit: bool = builder._window_mats[0].emission_energy_multiplier > 0.0
builder.set_night_mode(false)
var dark: bool = is_zero_approx(builder._window_mats[0].emission_energy_multiplier)
root.free()
return a and TestUtils.check(lit, "windows light up at night") and TestUtils.check(dark, "windows are dark by day")
func test_fallback_prop_is_usable() -> bool:
var root := Node3D.new()
var builder := WorldBuilder.new(root)
var fallback := builder._fallback_prop("bench")
var meshes := fallback.find_children("*", "MeshInstance3D", true, false)
var ok := TestUtils.check(meshes.size() >= 3, "fallback bench has geometry")
fallback.free()
root.free()
return ok