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

+208 -10
View File
@@ -31,8 +31,12 @@ Two commands:
regressions; they are NOT a pixel-exact or semantic match. A
vision-model pass is the documented next step (see PLAN.md).
list print the catalogue: name, kind, whether a reference design
exists, and which capture pairs with it.
Run from the repo root:
python3 tools/screen_designs.py list
python3 tools/screen_designs.py generate
python3 tools/screen_designs.py generate --draft
python3 tools/screen_designs.py compare
@@ -333,6 +337,162 @@ SCREENS = [
(0.36, 0.64, 0.64, 0.68, "cooldown"),
],
},
{
"name": "wand_screening",
"kind": "ui",
"title": "Wand screening",
"source": "README screenshot gallery · PLAN The Joke Shop · src/core/wand_screener.gd",
"ref": None,
"description": "Handheld wand screening panel for The Joke Shop: the alert "
"result, the items the guest is carrying, and allow/deny "
"buttons. A phone, keys or a belt buckle can raise a false "
"alarm, so the alert alone never proves a weapon.",
"prompt": (
"Realistic 3D game screenshot with an in-game security wand screening "
"panel overlaid: a handheld metal-detector alert indicator, a short "
"list of carried items such as keys, phone and belt buckle, a warning "
"note that an alert can be a false alarm, and two buttons labelled "
"Allow and Deny, dark translucent panel over a softly blurred comedy "
"club entrance at night"),
"regions": [
(0.26, 0.12, 0.74, 0.88, "wand panel"),
(0.32, 0.20, 0.68, 0.32, "alert result"),
(0.32, 0.38, 0.68, 0.56, "carried items"),
(0.30, 0.70, 0.48, 0.78, "allow"),
(0.52, 0.70, 0.70, 0.78, "deny"),
],
},
{
"name": "joke_shop_queue",
"kind": "scene",
"title": "Joke Shop door queue",
"source": "PLAN.md The Joke Shop — long line, ID and ticket checks",
"ref": None,
"description": "Evening view of a small comedy-club entrance: a short queue "
"on the sidewalk, a door person checking an ID and a ticket "
"under a warm bulb, a red awning, and the show board.",
"prompt": (
"Realistic 3D first-person game scene: a narrow comedy club entrance at "
"dusk with a short queue of guests on the sidewalk, a door person "
"checking an ID and a paper ticket under a warm lamp, red fabric awning, "
"a hand-lettered show board, wet pavement reflections, warm interior "
"light spilling through the door"),
"regions": [
(0.33, 0.30, 0.67, 0.86, "door + door person"),
(0.00, 0.55, 0.30, 0.95, "queue"),
(0.24, 0.16, 0.76, 0.36, "awning + show board"),
(0.72, 0.55, 1.00, 0.95, "sidewalk"),
],
},
{
"name": "studio_arrival",
"kind": "scene",
"title": "TV Studio celebrity arrival",
"source": "PLAN.md Late Night TV Studio — crowd gathers for an arrival",
"ref": None,
"description": "A celebrity arrival at the studio entrance: tourists and "
"influencers pressing toward the door, cameras raised, the "
"guard holding a line on the steps, studio marquee behind.",
"prompt": (
"Realistic 3D first-person game scene: a busy late-night television "
"studio entrance with a small crowd of tourists and influencers raising "
"cameras toward a black car at the curb, a security guard holding a line "
"on the steps, glowing studio marquee and lit canopy, night time"),
"regions": [
(0.30, 0.30, 0.70, 0.80, "crowd + cameras"),
(0.40, 0.10, 0.60, 0.34, "studio marquee"),
(0.00, 0.60, 0.25, 0.95, "car at the curb"),
(0.78, 0.55, 1.00, 0.90, "guard line"),
],
},
{
"name": "crowd_panic",
"kind": "scene",
"title": "Crowd panic",
"source": "PLAN.md Public Crowd Reaction · CrowdSystem.reaction_for_event",
"ref": None,
"description": "After a panic event the sidewalk empties away from the "
"door: people running toward the street, one person still "
"near the steps, the guard between them and the entrance.",
"prompt": (
"Realistic 3D first-person game scene: bystanders scattering away from a "
"building entrance across a wide sidewalk, motion blur on the fleeing "
"figures, one person left near the steps, a security guard standing "
"between them and the door, tense evening light, long shadows"),
"regions": [
(0.00, 0.45, 0.42, 0.90, "fleeing crowd"),
(0.55, 0.40, 0.78, 0.85, "guard between crowd and door"),
(0.38, 0.10, 0.80, 0.45, "building entrance"),
(0.72, 0.50, 1.00, 0.95, "empty sidewalk"),
],
},
{
"name": "responder_arrival",
"kind": "scene",
"title": "Responder arrival",
"source": "PLAN.md Level 5 Emergency Response · radio_system.gd responder dispatch",
"ref": None,
"description": "A dispatched responder walks up the steps toward a locked "
"door while the entrance is being cleared; blue light washes "
"the facade and the lock indicator reads red.",
"prompt": (
"Realistic 3D first-person game scene: an emergency responder in dark "
"uniform walking up wide concrete steps toward a locked building door, "
"blue light washing the brick facade, a red lock indicator beside the "
"door, people clearing from the entrance, night"),
"regions": [
(0.40, 0.28, 0.60, 0.85, "responder on the steps"),
(0.42, 0.14, 0.58, 0.32, "locked door + indicator"),
(0.00, 0.00, 1.00, 0.30, "facade + blue light"),
(0.00, 0.70, 0.28, 0.98, "people clearing"),
],
},
{
"name": "body_camera_timeline",
"kind": "ui",
"title": "Body-camera timeline",
"source": "README screenshot gallery · ADR-0005 · src/game/ui/event_timeline.gd",
"ref": None,
"description": "The shift review's body-camera timeline: a text list of every "
"recorded incident event stamped mm:ss with the person and the "
"action or outcome, one row highlighted, and previous/next "
"buttons.",
"prompt": (
"Realistic 3D game screenshot with an in-game body-camera event timeline "
"overlaid: a tall dark panel listing time-stamped rows such as "
"02:14 formal warning, 02:31 body camera on and 03:02 access denied, one "
"row highlighted, Previous and Next buttons at the bottom, over a "
"softly blurred night street background"),
"regions": [
(0.28, 0.10, 0.72, 0.90, "timeline panel"),
(0.32, 0.16, 0.68, 0.26, "timeline header"),
(0.32, 0.28, 0.68, 0.72, "timestamped rows"),
(0.32, 0.78, 0.68, 0.85, "previous / next"),
],
},
{
"name": "escalation_hud",
"kind": "ui",
"title": "Escalation HUD",
"source": "README accessibility note · Hud.escalation_meter (shape meter · level)",
"ref": None,
"description": "The in-shift HUD: a shape meter plus numeric escalation level "
"(`Escalation: Blocking [###--] L3/5`), the targeted person's "
"name, flashlight / door / body-camera indicators, shift clock, "
"and the reaction log.",
"prompt": (
"Realistic 3D game screenshot with a minimal in-game HUD overlaid: an "
"escalation meter drawn as a shape bar with the text Blocking and L3/5, "
"a target name at the bottom centre, small indicator icons for flashlight, "
"door lock and body camera, a shift clock at the top right and a short "
"reaction log at the left, over a softly blurred daytime street"),
"regions": [
(0.30, 0.02, 0.70, 0.10, "escalation shape meter"),
(0.30, 0.86, 0.70, 0.96, "target name + hint"),
(0.78, 0.02, 0.98, 0.14, "shift clock"),
(0.02, 0.60, 0.26, 0.95, "reaction log"),
],
},
]
@@ -341,6 +501,20 @@ def resolve_path(value):
return path if path.is_absolute() else ROOT / path
def list_screens(args):
"""Print the screen catalogue and what state each entry is in."""
for screen in SCREENS:
design = _design_path(screen["name"])
state = "design" if design.is_file() else "- "
capture = screen["ref"] or "-"
print(f"{screen['name']:22} {screen['kind']:5} {state} "
f"capture={capture:24} {screen['title']}")
scenes = sum(1 for screen in SCREENS if screen["kind"] == "scene")
print(f"Total: {len(SCREENS)} screens "
f"({scenes} scene, {len(SCREENS) - scenes} ui)")
return 0
def _design_path(name):
return DESIGNS_DIR / f"{name}.png"
@@ -378,10 +552,11 @@ def _draw_wireframe(screen, path):
int(x1 * WIDTH), int(y1 * HEIGHT))
draw.rounded_rectangle(box, radius=10,
outline=(120, 130, 170), width=2)
cx = (box[0] + box[2]) / 2
cy = (box[1] + box[3]) / 2
draw.text((cx - draw.textlength(label, font=label_font) / 2,
cy - 14), label, font=label_font, fill=(170, 180, 210))
# Anchor the label to the top-left corner: nested regions (a panel plus
# the cards inside it) would otherwise stack their centred labels on top
# of each other.
draw.text((box[0] + 10, box[1] + 6), label,
font=label_font, fill=(170, 180, 210))
image.save(path)
shown = path.relative_to(ROOT) if path.is_relative_to(ROOT) else path
@@ -419,11 +594,23 @@ def _generate_ai(screen, model, request_size):
def generate(args):
DESIGNS_DIR.mkdir(parents=True, exist_ok=True)
failed = []
for screen in SCREENS:
selected = [screen for screen in SCREENS
if not args.only or args.only in screen["name"]]
if not selected:
print(f"no screen matches --only {args.only!r}", file=sys.stderr)
return 2
for screen in selected:
path = _design_path(screen["name"])
# Draft mode is the offline fallback; do not clobber existing AI-crafted
# references unless the caller explicitly asks for it.
if path.is_file() and not args.force:
print(
f"skip {screen['name']} (reference exists; --force to replace)")
continue
print(f"generating {screen['name']} ...")
try:
if args.draft:
_draw_wireframe(screen, _design_path(screen["name"]))
_draw_wireframe(screen, path)
else:
_generate_ai(screen, args.model, args.request_size)
except SystemExit as exc:
@@ -643,8 +830,10 @@ def build_judge_request(model, screen, capture_path, design_path):
"role": "user",
"content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": _data_url(design_path)}},
{"type": "image_url", "image_url": {"url": _data_url(capture_path)}},
{"type": "image_url", "image_url": {
"url": _data_url(design_path)}},
{"type": "image_url", "image_url": {
"url": _data_url(capture_path)}},
],
}],
"temperature": 0,
@@ -725,7 +914,8 @@ def judge(args):
_load_env_file()
if not SCREENSHOTS_DIR.is_dir():
sys.exit(f"error: no screenshots directory at {SCREENSHOTS_DIR}")
refs = {screen["name"]: screen["ref"] for screen in SCREENS if screen["ref"]}
refs = {screen["name"]: screen["ref"]
for screen in SCREENS if screen["ref"]}
by_name = {screen["name"]: screen for screen in SCREENS}
results = []
for capture in sorted(SCREENSHOTS_DIR.glob("*.png")):
@@ -749,7 +939,7 @@ def judge(args):
if args.dry_run:
results.append({
"capture": capture.name, "screen": design_name, "match": None,
"confidence": 0.0, "model": body["model"],
"confidence": 0.0, "model": body["model"], "class": "unknown",
"differences": [f"dry run: {len(body['messages'][0]['content'])} parts"],
})
continue
@@ -795,6 +985,10 @@ def build_parser():
gen.add_argument("--model", default=DEFAULT_MODEL)
gen.add_argument("--request-size", type=int,
default=generate_textures.GEN_SIZE)
gen.add_argument("--only", default="",
help="only generate screens whose name contains this text")
gen.add_argument("--force", action="store_true",
help="replace reference designs that already exist")
gen.set_defaults(handler=generate, draft=False)
judge_cmd = commands.add_parser(
@@ -814,6 +1008,10 @@ def build_parser():
cmp.add_argument("--json", action="store_true")
cmp.set_defaults(handler=compare)
listing = commands.add_parser(
"list", help="print the screen catalogue and its state")
listing.set_defaults(handler=list_screens)
return parser