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
@@ -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 quit_requested()
|
||||
|
||||
const UiArt := preload("res://src/game/ui/art.gd")
|
||||
|
||||
const VENUES := [
|
||||
{"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."},
|
||||
@@ -30,11 +32,39 @@ func _build() -> void:
|
||||
_root.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_child(_root)
|
||||
|
||||
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 art := UiArt.new()
|
||||
var backdrop_texture := art.texture("start_menu")
|
||||
if backdrop_texture != null:
|
||||
var backdrop := TextureRect.new()
|
||||
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()
|
||||
center.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||
@@ -66,6 +96,7 @@ func _build() -> void:
|
||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
button.custom_minimum_size = Vector2(460, 62)
|
||||
button.pressed.connect(_on_venue_pressed.bind(i))
|
||||
_style_venue_button(button)
|
||||
column.add_child(button)
|
||||
_buttons.append(button)
|
||||
|
||||
@@ -98,6 +129,35 @@ func _build() -> void:
|
||||
|
||||
_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:
|
||||
if _buttons.is_empty():
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user