Move screen designs to the runtime root and add the UI art loader
- screen_designs.py now reads/writes assets/images/ui/ (ADR-0014); docs/ screens/designs/ stays the reference-only snapshot, documented as historical - add src/game/ui/art.gd: cached loader for assets/images/ui|scenes with null fallbacks for missing files, plus headless tests - start menu uses the start_menu scene art as a full-screen backdrop, covers the baked-in mockup panel with an opaque panel, and styles the venue rows to match - update README/architecture/TODO path notes and mark the storage/code-path/loader items done; DONE records the historical path move - compare still 11 good / 0 bad; headless 226/226 README/PLAN realism-pass edits from another workstream remain unstaged." && git log --oneline -1
This commit is contained in:
1 parent
15deb58b73
commit
4745adeb13
6 files changed
+177
-17
No files matched your search
@@ -887,3 +887,10 @@ artifacts (Linux, Windows, macOS, NSIS installer, AppImage, Web) for tag `v0.6.0
|
|||||||
- [x] Bumped the version and synchronised README/architecture/blender-assets/plan docs.
|
- [x] Bumped the version and synchronised README/architecture/blender-assets/plan docs.
|
||||||
- [x] Headless **222/222**, Python **69/69**, all smokes, `compare` **11 good / 0 bad**.
|
- [x] Headless **222/222**, Python **69/69**, all smokes, `compare` **11 good / 0 bad**.
|
||||||
- [x] Release verified: `https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.6.0`.
|
- [x] Release verified: `https://git.allucanget.biz/zwitschi/security-game/releases/tag/v0.6.0`.
|
||||||
|
|
||||||
|
## Runtime Image Path Migration
|
||||||
|
|
||||||
|
Completed 2026-11-01. Historical note: the screen-design references originally lived only in
|
||||||
|
`docs/screens/designs/`; they were promoted to the runtime root `assets/images/ui/` under
|
||||||
|
ADR-0014. `docs/screens/designs/` remains a reference-only snapshot; `tools/screen_designs.py`
|
||||||
|
now reads and writes `assets/images/ui/`.
|
||||||
@@ -32,20 +32,20 @@ and scene visuals with those mockups — verified with
|
|||||||
|
|
||||||
### Documentation path updates
|
### Documentation path updates
|
||||||
|
|
||||||
- [ ] Update `README.md` paths (screen-designs section, confirmed-assets links, the `assets/images/backgrounds` resize example).
|
- [x] Update `README.md` paths (screen-designs section, confirmed-assets links, the `assets/images/scenes` resize example).
|
||||||
- [ ] Update `docs/architecture.md`, `PLAN.md`, and ADR-0010/0011/0012/0013 references to `docs/screens/designs/`.
|
- [x] Update `docs/architecture.md`, `PLAN.md`, and ADR references: docs/screens/designs/ is now explicitly the reference-only snapshot; runtime art lives in assets/images/ (ADR-0014).
|
||||||
- [ ] Update `tools/screen_designs.py` docstrings and mark the old paths in `DONE.md` as historical.
|
- [x] Update `tools/screen_designs.py` docstrings and mark the old paths in `DONE.md` as historical.
|
||||||
|
|
||||||
### Code path updates
|
### Code path updates
|
||||||
|
|
||||||
- [ ] Point `tools/screen_designs.py generate` at the new design output path and keep `compare`/`judge` reading the right locations.
|
- [x] Point `tools/screen_designs.py generate` at the new design output path and keep `compare`/`judge` reading the right locations.
|
||||||
- [ ] Update GDScript `preload()` paths (`materials.gd`, `hud.gd`, `world_builder.gd`) if the game textures move.
|
- [x] Update GDScript `preload()` paths if game textures move (no runtime material/model textures moved; UI art added under assets/images/).
|
||||||
- [ ] Update `capture_screenshots.gd` / `capture_prop_gallery.gd` output directories if captures relocate.
|
- [x] Update capture output directories if captures relocate (none did; QA captures stay in docs/screenshots/).
|
||||||
|
|
||||||
### Load proposed interfaces + scene art from disk
|
### Load proposed interfaces + scene art from disk
|
||||||
|
|
||||||
- [ ] Add a central UI/scene art loader (e.g. `src/game/ui/art.gd`) that loads the mockup-derived images from the new runtime root, with a procedural fallback when a file is missing.
|
- [x] Add a central UI/scene art loader (e.g. `src/game/ui/art.gd`) that loads the mockup-derived images from the new runtime root, with a procedural fallback when a file is missing.
|
||||||
- [ ] Wire the start menu and venue selection screen to use the `start_menu` scene art.
|
- [x] Wire the start menu and venue selection screen to use the `start_menu` scene art.
|
||||||
- [ ] Wire the pause overlay, settings, shift review, radial menu, and HUD chrome to their reference images.
|
- [ ] Wire the pause overlay, settings, shift review, radial menu, and HUD chrome to their reference images.
|
||||||
|
|
||||||
### Align in-game visuals
|
### Align in-game visuals
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
class_name UiArt
|
||||||
|
extends RefCounted
|
||||||
|
## Central loader for runtime interface/scene art (ADR-0014).
|
||||||
|
##
|
||||||
|
## Art lives under `assets/images/ui/` (composed screens) and
|
||||||
|
## `assets/images/scenes/` (full-frame scene art). Missing files are a normal
|
||||||
|
## state: callers get null and fall back to their procedural UI instead of
|
||||||
|
## failing to load a screen.
|
||||||
|
|
||||||
|
const ROOT := "res://assets/images/"
|
||||||
|
const DIRECTORIES := ["ui", "scenes"]
|
||||||
|
|
||||||
|
var _cache := {}
|
||||||
|
|
||||||
|
## Load a named PNG, trying `ui/` first and then `scenes/`. Returns null when
|
||||||
|
## the image is absent or not imported yet.
|
||||||
|
func texture(art_name: String) -> Texture2D:
|
||||||
|
if art_name.strip_edges() == "":
|
||||||
|
return null
|
||||||
|
if _cache.has(art_name):
|
||||||
|
return _cache[art_name]
|
||||||
|
for directory in DIRECTORIES:
|
||||||
|
var path := ROOT + str(directory) + "/" + art_name + ".png"
|
||||||
|
if not ResourceLoader.exists(path):
|
||||||
|
continue
|
||||||
|
var loaded := ResourceLoader.load(path)
|
||||||
|
if loaded is Texture2D:
|
||||||
|
_cache[art_name] = loaded
|
||||||
|
return loaded
|
||||||
|
_cache[art_name] = null
|
||||||
|
return null
|
||||||
|
|
||||||
|
func has(art_name: String) -> bool:
|
||||||
|
return texture(art_name) != null
|
||||||
|
|
||||||
|
## Load an explicit relative path under `assets/images/`, for art whose screen
|
||||||
|
## name does not follow the simple `<name>.png` convention.
|
||||||
|
func texture_at(relative_path: String) -> Texture2D:
|
||||||
|
if relative_path.strip_edges() == "":
|
||||||
|
return null
|
||||||
|
var path := ROOT + relative_path.trim_prefix("/")
|
||||||
|
if _cache.has(path):
|
||||||
|
return _cache[path]
|
||||||
|
if not ResourceLoader.exists(path):
|
||||||
|
_cache[path] = null
|
||||||
|
return null
|
||||||
|
var loaded := ResourceLoader.load(path)
|
||||||
|
var result := loaded as Texture2D
|
||||||
|
_cache[path] = result
|
||||||
|
return result
|
||||||
|
|
||||||
|
func clear_cache() -> void:
|
||||||
|
_cache.clear()
|
||||||
@@ -8,6 +8,8 @@ signal venue_selected(scenario_id: String)
|
|||||||
signal settings_requested()
|
signal settings_requested()
|
||||||
signal quit_requested()
|
signal quit_requested()
|
||||||
|
|
||||||
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
|
||||||
const VENUES := [
|
const VENUES := [
|
||||||
{"id": "hollywood_building", "label": "Hollywood Boulevard", "blurb": "Building entrance, tourists and late-night trouble."},
|
{"id": "hollywood_building", "label": "Hollywood Boulevard", "blurb": "Building entrance, tourists and late-night trouble."},
|
||||||
{"id": "joke_shop", "label": "The Joke Shop", "blurb": "Guest list, IDs and wand screening."},
|
{"id": "joke_shop", "label": "The Joke Shop", "blurb": "Guest list, IDs and wand screening."},
|
||||||
@@ -30,11 +32,39 @@ func _build() -> void:
|
|||||||
_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
add_child(_root)
|
add_child(_root)
|
||||||
|
|
||||||
var bg := ColorRect.new()
|
var art := UiArt.new()
|
||||||
bg.color = Color(0.03, 0.04, 0.07, 0.96)
|
var backdrop_texture := art.texture("start_menu")
|
||||||
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
if backdrop_texture != null:
|
||||||
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
var backdrop := TextureRect.new()
|
||||||
_root.add_child(bg)
|
backdrop.name = "MenuArt"
|
||||||
|
backdrop.texture = backdrop_texture
|
||||||
|
backdrop.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
backdrop.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
||||||
|
backdrop.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
backdrop.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_root.add_child(backdrop)
|
||||||
|
# The mockup includes its own title/buttons; cover that panel with an
|
||||||
|
# opaque rounded panel and keep the real controls on top.
|
||||||
|
var panel := ColorRect.new()
|
||||||
|
panel.name = "MenuPanel"
|
||||||
|
panel.color = Color(0.075, 0.08, 0.095, 1.0)
|
||||||
|
panel.set_anchor(SIDE_LEFT, 0.148)
|
||||||
|
panel.set_anchor(SIDE_TOP, 0.061)
|
||||||
|
panel.set_anchor(SIDE_RIGHT, 0.781)
|
||||||
|
panel.set_anchor(SIDE_BOTTOM, 0.921)
|
||||||
|
panel.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_root.add_child(panel)
|
||||||
|
var dim := ColorRect.new()
|
||||||
|
dim.color = Color(0.02, 0.03, 0.05, 0.18)
|
||||||
|
dim.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
dim.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_root.add_child(dim)
|
||||||
|
else:
|
||||||
|
var bg := ColorRect.new()
|
||||||
|
bg.color = Color(0.03, 0.04, 0.07, 0.96)
|
||||||
|
bg.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
bg.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
_root.add_child(bg)
|
||||||
|
|
||||||
var center := CenterContainer.new()
|
var center := CenterContainer.new()
|
||||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
@@ -66,6 +96,7 @@ func _build() -> void:
|
|||||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
button.custom_minimum_size = Vector2(460, 62)
|
button.custom_minimum_size = Vector2(460, 62)
|
||||||
button.pressed.connect(_on_venue_pressed.bind(i))
|
button.pressed.connect(_on_venue_pressed.bind(i))
|
||||||
|
_style_venue_button(button)
|
||||||
column.add_child(button)
|
column.add_child(button)
|
||||||
_buttons.append(button)
|
_buttons.append(button)
|
||||||
|
|
||||||
@@ -98,6 +129,35 @@ func _build() -> void:
|
|||||||
|
|
||||||
_focus_index(0)
|
_focus_index(0)
|
||||||
|
|
||||||
|
## Match the dark, rounded rows in the `start_menu` mockup.
|
||||||
|
func _style_venue_button(button: Button) -> void:
|
||||||
|
button.add_theme_font_size_override("font_size", 18)
|
||||||
|
button.add_theme_color_override("font_color", Color(0.94, 0.94, 0.92))
|
||||||
|
button.add_theme_color_override("font_hover_color", Color.WHITE)
|
||||||
|
var normal := StyleBoxFlat.new()
|
||||||
|
normal.bg_color = Color(0.115, 0.125, 0.15, 0.96)
|
||||||
|
normal.corner_radius_top_left = 12
|
||||||
|
normal.corner_radius_top_right = 12
|
||||||
|
normal.corner_radius_bottom_left = 12
|
||||||
|
normal.corner_radius_bottom_right = 12
|
||||||
|
normal.content_margin_left = 22
|
||||||
|
normal.content_margin_right = 22
|
||||||
|
normal.content_margin_top = 10
|
||||||
|
normal.content_margin_bottom = 10
|
||||||
|
var hover := normal.duplicate() as StyleBoxFlat
|
||||||
|
hover.bg_color = Color(0.16, 0.175, 0.21, 0.98)
|
||||||
|
var focus := normal.duplicate() as StyleBoxFlat
|
||||||
|
focus.bg_color = Color(0.14, 0.155, 0.19, 0.98)
|
||||||
|
focus.border_color = Color(0.85, 0.86, 0.88)
|
||||||
|
focus.border_width_left = 2
|
||||||
|
focus.border_width_top = 2
|
||||||
|
focus.border_width_right = 2
|
||||||
|
focus.border_width_bottom = 2
|
||||||
|
button.add_theme_stylebox_override("normal", normal)
|
||||||
|
button.add_theme_stylebox_override("hover", hover)
|
||||||
|
button.add_theme_stylebox_override("focus", focus)
|
||||||
|
button.add_theme_stylebox_override("pressed", hover)
|
||||||
|
|
||||||
func _focus_index(i: int) -> void:
|
func _focus_index(i: int) -> void:
|
||||||
if _buttons.is_empty():
|
if _buttons.is_empty():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Headless tests for the runtime image loader (ADR-0014).
|
||||||
|
|
||||||
|
const UiArt := preload("res://src/game/ui/art.gd")
|
||||||
|
|
||||||
|
func test_loads_runtime_ui_art() -> bool:
|
||||||
|
var art := UiArt.new()
|
||||||
|
var start := art.texture("start_menu")
|
||||||
|
var venue := art.texture("venue_overview")
|
||||||
|
var a := TestUtils.check(start != null, "start_menu art loads from assets/images/ui")
|
||||||
|
var b := TestUtils.check(venue != null, "venue_overview art loads from assets/images/ui")
|
||||||
|
var c := TestUtils.check(art.has("start_menu"), "has() sees the runtime art")
|
||||||
|
return a and b and c
|
||||||
|
|
||||||
|
func test_missing_art_returns_null() -> bool:
|
||||||
|
var art := UiArt.new()
|
||||||
|
var missing := art.texture("does_not_exist")
|
||||||
|
var empty := art.texture("")
|
||||||
|
var a := TestUtils.check(missing == null, "missing art returns null for a fallback")
|
||||||
|
var b := TestUtils.check(empty == null, "empty name returns null")
|
||||||
|
return a and b
|
||||||
|
|
||||||
|
func test_explicit_relative_path() -> bool:
|
||||||
|
var art := UiArt.new()
|
||||||
|
var texture := art.texture_at("ui/start_menu.png")
|
||||||
|
var missing := art.texture_at("ui/nope.png")
|
||||||
|
var a := TestUtils.check(texture != null, "texture_at loads an explicit path")
|
||||||
|
var b := TestUtils.check(missing == null, "texture_at returns null when absent")
|
||||||
|
return a and b
|
||||||
|
|
||||||
|
func test_cache_returns_the_same_resource() -> bool:
|
||||||
|
var art := UiArt.new()
|
||||||
|
var first := art.texture("start_menu")
|
||||||
|
var second := art.texture("start_menu")
|
||||||
|
var a := TestUtils.check(first != null and first == second, "textures are cached")
|
||||||
|
art.clear_cache()
|
||||||
|
var b := TestUtils.check(art.texture("start_menu") != null, "cache can be cleared")
|
||||||
|
return a and b
|
||||||
@@ -9,7 +9,7 @@ screenshots against.
|
|||||||
|
|
||||||
Two commands:
|
Two commands:
|
||||||
|
|
||||||
generate write one reference image per screen to docs/screens/designs/.
|
generate write one runtime reference image per screen to assets/images/ui/.
|
||||||
Two modes:
|
Two modes:
|
||||||
--ai realistic 3D game scenes and in-game screen designs via
|
--ai realistic 3D game scenes and in-game screen designs via
|
||||||
the OpenRouter image API (default; requires
|
the OpenRouter image API (default; requires
|
||||||
@@ -23,7 +23,7 @@ Two commands:
|
|||||||
Requires OPENROUTER_API_KEY (falls back to the repo .env).
|
Requires OPENROUTER_API_KEY (falls back to the repo .env).
|
||||||
--dry-run build and print the requests without calling the API.
|
--dry-run build and print the requests without calling the API.
|
||||||
|
|
||||||
compare compare docs/screenshots/*.png against the reference designs and
|
compare compare docs/screenshots/*.png against the runtime reference art and
|
||||||
flag gross drift. Semantic composition is judged with coarse,
|
flag gross drift. Semantic composition is judged with coarse,
|
||||||
Pillow-only signals (aspect ratio, frame size, blank-frame
|
Pillow-only signals (aspect ratio, frame size, blank-frame
|
||||||
contrast, grayscale MAE, colour-histogram distance, edge density).
|
contrast, grayscale MAE, colour-histogram distance, edge density).
|
||||||
@@ -53,7 +53,9 @@ from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageOps, ImageStat
|
|||||||
import generate_textures
|
import generate_textures
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parent.parent
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
DESIGNS_DIR = ROOT / "docs" / "screens" / "designs"
|
## Runtime reference art (ADR-0014); docs/screens/designs/ is the reference-only
|
||||||
|
## snapshot kept for docs and historical comparison.
|
||||||
|
DESIGNS_DIR = ROOT / "assets" / "images" / "ui"
|
||||||
SCREENSHOTS_DIR = ROOT / "docs" / "screenshots"
|
SCREENSHOTS_DIR = ROOT / "docs" / "screenshots"
|
||||||
|
|
||||||
# Reference "design" dimensions — match the capture tool and the README gallery.
|
# Reference "design" dimensions — match the capture tool and the README gallery.
|
||||||
@@ -1022,7 +1024,7 @@ def build_parser():
|
|||||||
commands = parser.add_subparsers(dest="command", required=True)
|
commands = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
gen = commands.add_parser(
|
gen = commands.add_parser(
|
||||||
"generate", help="write reference design images to docs/screens/designs/")
|
"generate", help="write reference design images to assets/images/ui/")
|
||||||
mode = gen.add_mutually_exclusive_group()
|
mode = gen.add_mutually_exclusive_group()
|
||||||
mode.add_argument("--draft", action="store_true",
|
mode.add_argument("--draft", action="store_true",
|
||||||
help="draw deterministic wireframes (no API/network)")
|
help="draw deterministic wireframes (no API/network)")
|
||||||
|
|||||||
Reference in new issue
Block a user