refactor(ui): tokenize HUD notification colors off raw Color() literals

p2-74 cluster 2 (HUD panels + notifications), notifications sub-cluster.
Route turn_notification.gd / comms_toast.gd / capital_blackout_overlay.gd
inline Color() literals onto ThemeAssets.color() design tokens.

- turn_notification: const CATEGORY_COLORS dict (a const trap — token lookups
  can't sit in a const initializer) converted to var _category_colors populated
  in _ready(); category palette → semantic.negative/positive, accent.science,
  accent.goldResource, player.purple, semantic.diplomacy, text.primary. Panel
  bg/border → background.panel/border.panel; title/header → text.title; scrim
  dims → background.hud/overlay; muted hints → text.muted.
- comms_toast: panel bg/border → background.panel/border.panel; accent strip →
  accent.gold; title → text.title; body → text.primary. The configure() accent
  default Color literal (param defaults can't call the autoload) becomes a null
  sentinel resolved to accent.gold in the body.
- capital_blackout_overlay: dim/glitch scrims sourced from background.deepest /
  semantic.negative with explicit .a; title → semantic.negative; subtitle →
  text.title.

Adds tools/capture-proof.sh: reusable single-proof-scene capture under a private
headless weston on the RUN host, pulling the PNG back. Visual-only; no logic
change (Rail 3). 0 Color() remain in the three scripts.

Proof: hud_proof.tscn captured on apricot (headless weston) shows the themed
purple Turn Summary panel, gold border/title, copper filter checkboxes, and
semantic per-category entry coloring (red combat, green founding, blue tech,
gold economy, violet magic).
.project/screenshots/p2-74-cluster2-hud-notifications.png

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
autocommit 2026-06-04 20:57:41 -07:00
parent 3a04052385
commit d6c84e4b4f
5 changed files with 96 additions and 32 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View file

@ -8,16 +8,9 @@ const AUTO_DISMISS_SECONDS: float = 3.0
const MAX_ENTRIES: int = 8
## Category color palette for log entries. Keys must match the strings passed
## to `_add_entry` — see `_on_*` handlers below.
const CATEGORY_COLORS: Dictionary = {
"combat": Color(1.0, 0.55, 0.55, 1.0), # red-ish
"founding": Color(0.65, 1.0, 0.65, 1.0), # green-ish
"tech": Color(0.65, 0.85, 1.0, 1.0), # blue-ish
"economy": Color(1.0, 0.9, 0.55, 1.0), # gold
"magic": Color(0.85, 0.7, 1.0, 1.0), # violet
"event": Color(1.0, 0.75, 0.5, 1.0), # orange
"default": Color(0.9, 0.88, 0.78, 1.0),
}
## to `_add_entry` — see `_on_*` handlers below. Populated from design tokens in
## `_ready()` (ThemeAssets.color() is an autoload call, not a const expression).
var _category_colors: Dictionary = {}
## Filter buckets map one or more entry categories to a single checkbox. "all"
## is a meta-bucket that flips every other filter in lockstep.
@ -55,6 +48,15 @@ var _filter_checks: Dictionary = {}
func _ready() -> void:
layer = 20
visible = false
_category_colors = {
"combat": ThemeAssets.color("semantic.negative"),
"founding": ThemeAssets.color("semantic.positive"),
"tech": ThemeAssets.color("accent.science"),
"economy": ThemeAssets.color("accent.goldResource"),
"magic": ThemeAssets.color("player.purple"),
"event": ThemeAssets.color("semantic.diplomacy"),
"default": ThemeAssets.color("text.primary"),
}
_build_ui()
_connect_signals()
@ -62,7 +64,7 @@ func _ready() -> void:
func _build_ui() -> void:
_dim_rect = ColorRect.new()
_dim_rect.name = "DimRect"
_dim_rect.color = Color(0.0, 0.0, 0.0, 0.4)
_dim_rect.color = ThemeAssets.color("background.hud")
_dim_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
_dim_rect.mouse_filter = Control.MOUSE_FILTER_STOP
_dim_rect.gui_input.connect(_on_dim_clicked)
@ -72,7 +74,7 @@ func _build_ui() -> void:
_processing_label.name = "ProcessingLabel"
_processing_label.text = ThemeVocabulary.lookup("action_processing")
_processing_label.add_theme_font_size_override("font_size", 28)
_processing_label.add_theme_color_override("font_color", Color(1.0, 0.94, 0.75, 1.0))
_processing_label.add_theme_color_override("font_color", ThemeAssets.color("text.title"))
_processing_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_processing_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_processing_label.set_anchors_preset(Control.PRESET_CENTER)
@ -92,8 +94,8 @@ func _build_ui() -> void:
add_child(_log_panel)
var panel_style: StyleBoxFlat = StyleBoxFlat.new()
panel_style.bg_color = Color(0.05, 0.04, 0.02, 0.92)
panel_style.border_color = Color(0.8, 0.7, 0.3, 0.9)
panel_style.bg_color = ThemeAssets.color("background.panel")
panel_style.border_color = ThemeAssets.color("border.panel")
panel_style.set_border_width_all(2)
panel_style.set_corner_radius_all(6)
_log_panel.add_theme_stylebox_override("panel", panel_style)
@ -113,7 +115,7 @@ func _build_ui() -> void:
header.name = "LogHeader"
header.text = "%s Summary" % ThemeVocabulary.lookup("turn")
header.add_theme_font_size_override("font_size", 20)
header.add_theme_color_override("font_color", Color(1.0, 0.94, 0.75, 1.0))
header.add_theme_color_override("font_color", ThemeAssets.color("text.title"))
header.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
outer_vbox.add_child(header)
@ -142,7 +144,7 @@ func _build_ui() -> void:
hint.name = "DismissHint"
hint.text = ThemeVocabulary.lookup("dismiss_hint")
hint.add_theme_font_size_override("font_size", 12)
hint.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 0.8))
hint.add_theme_color_override("font_color", ThemeAssets.color("text.muted"))
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
outer_vbox.add_child(hint)
@ -189,7 +191,7 @@ func show_processing() -> void:
_is_processing = true
_processing_label.visible = true
_log_panel.visible = false
_dim_rect.color = Color(0.0, 0.0, 0.0, 0.4)
_dim_rect.color = ThemeAssets.color("background.hud")
visible = true
@ -200,7 +202,7 @@ func _show_log() -> void:
return
_processing_label.visible = false
_dim_rect.color = Color(0.0, 0.0, 0.0, 0.5)
_dim_rect.color = ThemeAssets.color("background.overlay")
_rebuild_log_entries()
_log_panel.visible = true
@ -225,13 +227,13 @@ func _rebuild_log_entries() -> void:
var more_label: Label = Label.new()
more_label.text = ThemeVocabulary.lookup("fmt_and_n_more") % hidden_count
more_label.add_theme_font_size_override("font_size", 13)
more_label.add_theme_color_override("font_color", Color(0.6, 0.6, 0.6, 0.8))
more_label.add_theme_color_override("font_color", ThemeAssets.color("text.muted"))
_log_vbox.add_child(more_label)
func _build_entry_node(entry: Dictionary) -> Control:
var category: String = entry.get("category", "default")
var color: Color = CATEGORY_COLORS.get(category, CATEGORY_COLORS["default"])
var color: Color = _category_colors.get(category, _category_colors["default"])
var text: String = entry.get("text", "")
if entry.has("hex_pos"):
var btn: Button = Button.new()

View file

@ -24,7 +24,8 @@ func _ready() -> void:
func _build_ui() -> void:
_dim = ColorRect.new()
_dim.set_anchors_preset(Control.PRESET_FULL_RECT)
_dim.color = Color(0.04, 0.0, 0.02, 0.88)
_dim.color = ThemeAssets.color("background.deepest")
_dim.color.a = 0.88
_dim.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(_dim)
@ -33,7 +34,8 @@ func _build_ui() -> void:
_glitch_band.anchor_right = 1.0
_glitch_band.anchor_top = 0.42
_glitch_band.anchor_bottom = 0.58
_glitch_band.color = Color(0.55, 0.05, 0.05, 0.35)
_glitch_band.color = ThemeAssets.color("semantic.negative")
_glitch_band.color.a = 0.35
add_child(_glitch_band)
var center: CenterContainer = CenterContainer.new()
@ -47,14 +49,14 @@ func _build_ui() -> void:
_title_label = Label.new()
_title_label.text = TITLE_DEFAULT
_title_label.add_theme_font_size_override("font_size", 42)
_title_label.add_theme_color_override("font_color", Color(1.0, 0.45, 0.45, 1.0))
_title_label.add_theme_color_override("font_color", ThemeAssets.color("semantic.negative"))
_title_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(_title_label)
_subtitle_label = Label.new()
_subtitle_label.text = SUBTITLE_DEFAULT
_subtitle_label.add_theme_font_size_override("font_size", 22)
_subtitle_label.add_theme_color_override("font_color", Color(0.9, 0.85, 0.65, 0.95))
_subtitle_label.add_theme_color_override("font_color", ThemeAssets.color("text.title"))
_subtitle_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(_subtitle_label)

View file

@ -22,8 +22,8 @@ func _ready() -> void:
mouse_filter = Control.MOUSE_FILTER_IGNORE
var style: StyleBoxFlat = StyleBoxFlat.new()
style.bg_color = Color(0.06, 0.06, 0.10, 0.92)
style.border_color = Color(0.85, 0.78, 0.40, 0.95)
style.bg_color = ThemeAssets.color("background.panel")
style.border_color = ThemeAssets.color("border.panel")
style.set_border_width_all(2)
style.set_corner_radius_all(6)
style.content_margin_left = 12
@ -39,7 +39,7 @@ func _ready() -> void:
_icon_rect = ColorRect.new()
_icon_rect.custom_minimum_size = Vector2(8, 0)
_icon_rect.size_flags_vertical = Control.SIZE_EXPAND_FILL
_icon_rect.color = Color(0.85, 0.78, 0.40, 1.0)
_icon_rect.color = ThemeAssets.color("accent.gold")
row.add_child(_icon_rect)
var col: VBoxContainer = VBoxContainer.new()
@ -49,12 +49,12 @@ func _ready() -> void:
_title = Label.new()
_title.add_theme_font_size_override("font_size", 16)
_title.add_theme_color_override("font_color", Color(1.0, 0.94, 0.75, 1.0))
_title.add_theme_color_override("font_color", ThemeAssets.color("text.title"))
col.add_child(_title)
_body = Label.new()
_body.add_theme_font_size_override("font_size", 13)
_body.add_theme_color_override("font_color", Color(0.88, 0.88, 0.84, 1.0))
_body.add_theme_color_override("font_color", ThemeAssets.color("text.primary"))
_body.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
col.add_child(_body)
@ -66,16 +66,19 @@ func _ready() -> void:
## Parameterise the toast at spawn time. Call after add_child / before showing.
func configure(title: String, body: String, accent: Color = Color(0.85, 0.78, 0.40, 1.0)) -> void:
## A null accent falls back to the gold design token (param defaults can't call
## the ThemeAssets autoload, so the sentinel is resolved here in the body).
func configure(title: String, body: String, accent: Variant = null) -> void:
_title.text = title
_body.text = body
if _title.text.is_empty():
_title.visible = false
_icon_rect.color = accent
var accent_color: Color = accent if accent is Color else ThemeAssets.color("accent.gold")
_icon_rect.color = accent_color
var style: StyleBoxFlat = get_theme_stylebox("panel") as StyleBoxFlat
if style != null:
style.border_color = accent
style.border_color = accent_color
modulate.a = 0.0
var tween: Tween = create_tween()

57
tools/capture-proof.sh Executable file
View file

@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Capture a single proof scene under a private headless weston on the RUN host,
# then pull the resulting PNG back to the EDIT host.
#
# Reusable across UI proof work (p2-74 de-hardcode clusters etc). The proof
# scene must self-capture via get_viewport().get_image().save_png() and print a
# line "SCREENSHOT_PATH:<abs>" (the convention used by scenes/tests/*_proof.gd).
#
# Usage: tools/capture-proof.sh <scene_res_path> <local_out_png> [timeout_s]
# scene_res_path: e.g. res://engine/scenes/tests/hud_proof.tscn
# local_out_png : destination on the EDIT host
#
# Env: AUTOPLAY_HOST (default lilith@apricot.lan), PROJECT_ROOT_REMOTE.
set -uo pipefail
SCENE="${1:?scene res:// path required}"
OUT="${2:?local output png path required}"
TIMEOUT="${3:-180}"
RH="${AUTOPLAY_HOST:-lilith@apricot.lan}"
RROOT="${PROJECT_ROOT_REMOTE:-/var/home/lilith/Code/@projects/@magic-civilization}"
remote_cmd=$(cat <<REMOTE
set -uo pipefail
cd "$RROOT/src/game" || exit 3
WSOCK="proof-\$\$"
weston --backend=headless --no-config --socket="\$WSOCK" --width=1280 --height=720 \
>/tmp/weston-\$WSOCK.log 2>&1 &
WPID=\$!
sleep 1.5
XDG_RUNTIME_DIR="\${XDG_RUNTIME_DIR:-/run/user/\$(id -u)}" \
timeout "$TIMEOUT" flatpak run --user \
--filesystem=home \
--socket=wayland \
--unset-env=DISPLAY \
--env=WAYLAND_DISPLAY="\$WSOCK" \
--filesystem=xdg-run/"\$WSOCK" \
org.godotengine.Godot \
--path . \
--display-driver wayland --rendering-driver opengl3 --rendering-method gl_compatibility \
"$SCENE" 2>&1 | tee /tmp/proof-\$WSOCK.log
kill "\$WPID" 2>/dev/null || true
grep -m1 '^SCREENSHOT_PATH:' /tmp/proof-\$WSOCK.log | sed 's/^SCREENSHOT_PATH://'
REMOTE
)
echo "=== capture-proof: $SCENE on $RH ==="
REMOTE_PNG="$(ssh "$RH" "$remote_cmd" | tail -n1)"
if [ -z "$REMOTE_PNG" ]; then
echo "ERROR: no SCREENSHOT_PATH emitted by proof scene" >&2
exit 1
fi
echo "Remote PNG: $REMOTE_PNG"
mkdir -p "$(dirname "$OUT")"
scp "$RH:$REMOTE_PNG" "$OUT"
echo "Saved: $OUT"