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
+208
-10
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user