Add body-camera timeline to the shift review
Full build / Import + headless tests (push) Successful in 30s
Full build / Full desktop build + optional release (push) Failing after 3m38s

- New src/game/ui/event_timeline.gd flattens every recorded incident event
  (ADR-0005) into one time-ordered list and shows time, person, and a one-line
  summary. Step with Left/Right or the buttons; Esc returns to the review. No live
  3D replay in this first version.
- Shift review gains a "Body camera timeline" button that hands the review to the
  timeline; the review dim is hidden while it is open (stacked overlays made the
  screen nearly black) and restored on close.
- ShiftCoordinator records started_at_ms so the review can stamp events as mm:ss.
- 8 new string keys for the timeline UI and the review button.

Tests/docs
- Headless suite 113 -> 120 (test_event_timeline.gd), new smoke_timeline.gd that
  opens the timeline from the review, steps through every event, and closes back.
- README, TODO section moved to DONE, MEMORY notes. Version 0.2.6.
This commit is contained in:
zwitschi committed 2026-10-31 21:02:25 +01:00
1 parent 921bab796e
commit 1b976bf351
14 files changed
+614 -71

No files matched your search

+93 -51
View File
@@ -9,11 +9,11 @@ screenshots against.
Two commands:
generate write one reference design per screen to docs/screens/designs/.
generate write one reference image per screen to docs/screens/designs/.
Two modes:
--ai photorealistic mockups via the OpenRouter image API
(default; requires OPENROUTER_API_KEY, same as
tools/asset_pipeline.py).
--ai realistic 3D game scenes and in-game screen designs via
the OpenRouter image API (default; requires
OPENROUTER_API_KEY, same as tools/asset_pipeline.py).
--draft deterministic wireframe sketches drawn with Pillow
(no network or API key; good for CI and layout checks).
@@ -54,6 +54,23 @@ ASPECT = 16 / 9
# the model asset_pipeline.py already trusts for non-alpha roles).
DEFAULT_MODEL = "openai/gpt-5-image-mini"
# Shared art direction, appended to every prompt so the generated frames read
# as one coherent game. "scene" entries are first-person 3D world views;
# "ui" entries are in-game panels overlaid on a softened scene.
SCENE_STYLE = (
"rendered in a modern 3D game engine, first-person perspective, cinematic "
"lighting, physically based materials, soft shadows, ambient occlusion, "
"subtle depth of field, photorealistic detail, no watermark or UI clutter"
)
UI_STYLE = (
"clean modern in-game user interface, dark translucent panels, readable "
"sans-serif labels, consistent game UI colour theme, crisp vector text"
)
# Landscape source ratio requested from the API before cropping to 16:9.
# 3:2 is the validated non-square ratio for the default model (see MEMORY.md).
SOURCE_ASPECT = "3:2"
# ---------------------------------------------------------------------------
# Screen catalogue. Each entry is grounded in a doc; `prompt` feeds --ai and
# `regions` feed --draft (normalised rects + a short label). `ref` names the
@@ -63,6 +80,7 @@ DEFAULT_MODEL = "openai/gpt-5-image-mini"
SCREENS = [
{
"name": "venue_overview",
"kind": "scene",
"title": "Venue overview",
"source": "README screenshot 01",
"ref": "01-venue-overview.png",
@@ -71,11 +89,11 @@ SCREENS = [
"sidewalk, street lamps, planters, crosswalk, and building "
"signage/marquee under early-morning light.",
"prompt": (
"First-person video game screenshot, security guard standing at the "
"top of wide concrete steps outside a Hollywood Boulevard building "
"entrance, daytime early morning light, sidewalk with street lamps, "
"planters and a crosswalk, building facade with awning and marquee "
"signage, clean UI-free gameplay view, 16:9"),
"Realistic 3D first-person game scene: a security guard's view from "
"the top of wide concrete steps outside a grand Hollywood Boulevard "
"building entrance, early-morning golden light, sidewalk with street "
"lamps, planters, a crosswalk and distant pedestrians, detailed brick "
"facade with a striped awning and glowing marquee sign"),
"regions": [
(0.00, 0.55, 1.00, 1.00, "steps + sidewalk"),
(0.20, 0.08, 0.80, 0.60, "building facade / marquee"),
@@ -85,6 +103,7 @@ SCREENS = [
},
{
"name": "targeting_npc",
"kind": "scene",
"title": "Targeting an NPC",
"source": "README screenshot 02 · ADR-0008 cone focus + head ring",
"ref": "02-targeting-npc.png",
@@ -93,10 +112,10 @@ SCREENS = [
"the ring, and the HUD shows a target line with the "
"interaction hint.",
"prompt": (
"First-person video game screenshot, a pedestrian NPC walking toward "
"a security guard on a city street, a thin pale-gold ring floating "
"around the NPC's head as a selection marker, small name label under "
"the ring, minimal HUD text at the bottom of the screen, daytime, 16:9"),
"Realistic 3D first-person game scene: a pedestrian NPC walking "
"toward the camera on a city sidewalk, a thin pale-gold ring floating "
"around the NPC's head as a selection marker, a small name label "
"under the ring, minimal HUD target text at the bottom, daytime"),
"regions": [
(0.35, 0.15, 0.65, 0.60, "NPC in focus"),
(0.44, 0.10, 0.56, 0.16, "head ring"),
@@ -105,6 +124,7 @@ SCREENS = [
},
{
"name": "access_inspection",
"kind": "ui",
"title": "Access inspection",
"source": "README screenshot 03 · ADR-0003 access verification",
"ref": "03-access-inspection.png",
@@ -113,11 +133,11 @@ SCREENS = [
"pattern, date, and event marking, plus allow and deny "
"prompts.",
"prompt": (
"First-person video game screenshot with a centered access inspection "
"panel: a card showing a wristband or ID credential with a photo "
"placeholder, colour band, date and event marking, and two buttons "
"labelled allow and deny, dark translucent panel over a city street "
"scene, 16:9"),
"Realistic 3D game screenshot with an in-game access inspection panel "
"overlaid: a centered card showing a wristband or ID credential with a "
"photo placeholder, colour band, date and event marking, and two "
"buttons labelled Allow and Deny, dark translucent panel over a "
"softly blurred city street background"),
"regions": [
(0.25, 0.10, 0.75, 0.90, "inspection panel"),
(0.30, 0.18, 0.70, 0.60, "artifact card"),
@@ -127,6 +147,7 @@ SCREENS = [
},
{
"name": "radio_report",
"kind": "ui",
"title": "Radio report",
"source": "README screenshot 04 · PLAN level 4 radio assistance",
"ref": "04-radio-report.png",
@@ -134,10 +155,10 @@ SCREENS = [
"description, behaviour, whether weapons were seen, and "
"whether anyone is in immediate danger, plus a send action.",
"prompt": (
"First-person video game screenshot with a radio dispatch report "
"panel open: form fields for location, description, behaviour, "
"weapons seen, and immediate danger, with a send button, dark "
"translucent panel over a city street scene, 16:9"),
"Realistic 3D game screenshot with an in-game radio dispatch panel "
"overlaid: form fields for location, description, behaviour, weapons "
"seen and immediate danger, and a Send button, dark translucent panel "
"over a softly blurred city street background"),
"regions": [
(0.30, 0.08, 0.70, 0.92, "radio panel"),
(0.34, 0.16, 0.66, 0.30, "location / description"),
@@ -147,15 +168,17 @@ SCREENS = [
},
{
"name": "late_night_avenue",
"kind": "scene",
"title": "Late night avenue",
"source": "README screenshot 05",
"ref": "05-late-night-avenue.png",
"description": "Night-time view down Hollywood Boulevard: street lamps "
"lit, marquee glowing, dark blue street, no flashlight.",
"prompt": (
"First-person video game screenshot at night, a dark Hollywood "
"Boulevard avenue with lit street lamps, a glowing marquee sign, "
"deep blue night sky, no flashlight beam, 16:9"),
"Realistic 3D first-person game scene at night: a dark Hollywood "
"Boulevard avenue receding into the distance, lit street lamps, a "
"glowing neon marquee sign, deep blue night sky, wet reflective "
"pavement, no flashlight"),
"regions": [
(0.00, 0.55, 1.00, 1.00, "dark avenue"),
(0.15, 0.25, 0.85, 0.55, "glowing marquee"),
@@ -165,15 +188,17 @@ SCREENS = [
},
{
"name": "night_flashlight",
"kind": "scene",
"title": "Night flashlight",
"source": "README screenshot 06",
"ref": "06-night-flashlight.png",
"description": "Night shift: the guard's flashlight beam cuts through the "
"dark toward the entrance, illuminating the steps and door.",
"prompt": (
"First-person video game screenshot at night, a flashlight beam "
"cutting through darkness toward a building entrance and steps, "
"bright spotlight pool on the door, dark surroundings, 16:9"),
"Realistic 3D first-person game scene at night: a flashlight beam "
"cutting through darkness toward a building entrance and steps, a "
"bright spotlight pool illuminating the door, dark surroundings and "
"deep shadows"),
"regions": [
(0.35, 0.30, 0.65, 0.80, "flashlight beam pool"),
(0.40, 0.20, 0.60, 0.35, "lit door"),
@@ -182,6 +207,7 @@ SCREENS = [
},
{
"name": "radial_menu",
"kind": "ui",
"title": "Radial action menu",
"source": "ADR-0009 · content/strings.en.json",
"ref": None,
@@ -190,11 +216,11 @@ SCREENS = [
"Wand, Warn, Block, Reason, Recognize, Radio. The focused "
"person's display name sits in the centre.",
"prompt": (
"First-person video game screenshot with a radial pie menu centered "
"on screen, twelve labelled slices in a circle reading Stop, Look, "
"Point, No, Ask, Inspect, Wand, Warn, Block, Reason, Recognize, "
"Radio, a highlighted slice, a name in the center of the ring, "
"over a city street scene, 16:9"),
"Realistic 3D game screenshot with an in-game radial action menu "
"overlaid: a round doughnut wheel centered on screen with twelve "
"labelled slices reading Stop, Look, Point, No, Ask, Inspect, Wand, "
"Warn, Block, Reason, Recognize, Radio, one slice highlighted, a name "
"in the center of the ring, over a softly blurred city street scene"),
"regions": [
(0.38, 0.12, 0.62, 0.88, "radial wheel (12 slices)"),
(0.46, 0.44, 0.54, 0.56, "name"),
@@ -203,6 +229,7 @@ SCREENS = [
},
{
"name": "shift_review",
"kind": "ui",
"title": "Shift review",
"source": "PLAN reputation / shift review · architecture.md ShiftReview",
"ref": None,
@@ -210,10 +237,10 @@ SCREENS = [
"escalation choices, incident list, battery and crowd "
"outcomes, and reputation/score feedback.",
"prompt": (
"Video game end-of-shift review screen: a performance summary panel "
"listing incident decisions, escalation choices, reputation score, "
"crowd reaction and battery use, with a continue button, dark themed "
"UI, 16:9"),
"Realistic 3D game end-of-shift review screen: a full-screen "
"performance summary panel listing incident decisions, escalation "
"choices, a reputation score, crowd reaction and battery use, with a "
"Continue button, dark themed game UI"),
"regions": [
(0.30, 0.06, 0.70, 0.94, "review panel"),
(0.36, 0.14, 0.64, 0.24, "score / reputation"),
@@ -223,15 +250,16 @@ SCREENS = [
},
{
"name": "start_menu",
"kind": "ui",
"title": "Start menu",
"source": "MEMORY session shell (StartMenu, layer 20)",
"ref": None,
"description": "The boot screen: a venue-selection menu (Hollywood "
"Boulevard, The Joke Shop, TV Studio) before a shift.",
"prompt": (
"Video game start menu screen: a title and three venue selection "
"buttons labelled Hollywood Boulevard, The Joke Shop, and TV Studio, "
"dark themed menu over a city street backdrop, 16:9"),
"Realistic 3D game start menu screen: a game title and three venue "
"selection buttons labelled Hollywood Boulevard, The Joke Shop and TV "
"Studio, dark themed menu over a softly blurred city street backdrop"),
"regions": [
(0.25, 0.10, 0.75, 0.26, "title"),
(0.36, 0.36, 0.64, 0.44, "Hollywood Boulevard"),
@@ -241,14 +269,15 @@ SCREENS = [
},
{
"name": "pause_overlay",
"kind": "ui",
"title": "Pause overlay",
"source": "MEMORY session shell (PauseOverlay, layer 18)",
"ref": None,
"description": "Pause menu: resume, restart, settings, and quit-to-menu.",
"prompt": (
"Video game pause menu overlay: buttons labelled Resume, Restart, "
"Settings, and Quit to Menu, dark translucent overlay over a paused "
"city street scene, 16:9"),
"Realistic 3D game pause menu overlay: buttons labelled Resume, "
"Restart, Settings and Quit to Menu, dark translucent overlay over a "
"paused, softly blurred city street scene"),
"regions": [
(0.38, 0.26, 0.62, 0.74, "pause menu"),
(0.42, 0.32, 0.58, 0.38, "Resume"),
@@ -259,13 +288,14 @@ SCREENS = [
},
{
"name": "settings_panel",
"kind": "ui",
"title": "Settings",
"source": "MEMORY session shell (SettingsPanel, layer 22)",
"ref": None,
"description": "Settings: look sensitivity and HUD font scale controls.",
"prompt": (
"Video game settings screen: sliders labelled look sensitivity and "
"HUD font scale, dark themed settings panel, 16:9"),
"Realistic 3D game settings screen: sliders labelled look sensitivity "
"and HUD font scale, dark themed settings panel"),
"regions": [
(0.30, 0.16, 0.70, 0.84, "settings panel"),
(0.36, 0.30, 0.64, 0.40, "look sensitivity"),
@@ -274,15 +304,17 @@ SCREENS = [
},
{
"name": "emergency_radio",
"kind": "ui",
"title": "Emergency radio",
"source": "PLAN level 5 · strings radio.emergency_dispatched",
"ref": None,
"description": "Hold-to-confirm emergency radio panel with a confirmation "
"prompt and a cooldown indicator.",
"prompt": (
"Video game emergency radio hold-to-confirm panel: a large warning "
"prompt with a hold progress bar and a cooldown label, red accent, "
"dark translucent overlay over a city street, 16:9"),
"Realistic 3D game emergency radio hold-to-confirm panel: a large "
"warning prompt with a hold progress bar and a cooldown label, red "
"accent, dark translucent overlay over a softly blurred city street "
"scene"),
"regions": [
(0.30, 0.28, 0.70, 0.72, "emergency panel"),
(0.36, 0.36, 0.64, 0.46, "confirm prompt"),
@@ -349,10 +381,19 @@ def _draw_wireframe(screen, path):
# AI generation (reuses the OpenRouter backend from generate_textures).
# ---------------------------------------------------------------------------
def _compose_prompt(screen):
"""Append the shared art direction to a screen's content prompt."""
style = UI_STYLE if screen.get("kind") == "ui" else SCENE_STYLE
return f"{screen['prompt']}. {style}. 16:9 widescreen composition."
def _generate_ai(screen, model, request_size):
prompt = _compose_prompt(screen)
# Request a landscape source and crop to the 16:9 capture dimensions rather
# than a square that loses framing when centre-cropped.
image = generate_textures.generate_image(
screen["prompt"], model, request_size, transparent=False,
aspect_ratio="1:1")
prompt, model, request_size, transparent=False,
aspect_ratio=SOURCE_ASPECT)
image = ImageOps.fit(image, (WIDTH, HEIGHT),
method=Image.Resampling.LANCZOS)
if image.mode in ("RGBA", "LA", "P"):
@@ -360,7 +401,8 @@ def _generate_ai(screen, model, request_size):
path = _design_path(screen["name"])
path.parent.mkdir(parents=True, exist_ok=True)
image.save(path)
print(f"wrote {path.relative_to(ROOT)} {image.size}")
shown = path.relative_to(ROOT) if path.is_relative_to(ROOT) else path
print(f"wrote {shown} {image.size}")
def generate(args):
+10
View File
@@ -25,6 +25,7 @@ class ScreenDesignsTests(unittest.TestCase):
"screenshot refs must be unique")
for screen in screen_designs.SCREENS:
self.assertTrue(screen["prompt"].strip())
self.assertIn(screen["kind"], ("scene", "ui"))
self.assertTrue(screen["regions"])
for region in screen["regions"]:
x0, y0, x1, y1, label = region
@@ -42,6 +43,15 @@ class ScreenDesignsTests(unittest.TestCase):
self.assertEqual(image.size,
(screen_designs.WIDTH, screen_designs.HEIGHT))
def test_compose_prompt_appends_kind_specific_style(self):
scene = {"kind": "scene", "prompt": "a street"}
ui = {"kind": "ui", "prompt": "a panel"}
self.assertIn(screen_designs.SCENE_STYLE,
screen_designs._compose_prompt(scene))
self.assertIn(screen_designs.UI_STYLE,
screen_designs._compose_prompt(ui))
self.assertIn("16:9", screen_designs._compose_prompt(scene))
def test_mae_is_zero_for_identical_images(self):
path = self.root / "identical.png"
Image.new("RGB", (1280, 720), (40, 80, 120)).save(path)