feat(迭代25): UI 全局主题批 T0-T2——全局主题资源挂载+两处局部做法退役+冒烟 V1-V6 断言

- T0: game/assets/theme/main_theme.tres 挂 project.godot gui/theme/custom——
  15 类型全覆盖(V/HScrollBar·ScrollContainer·H/VSlider·TooltipPanel/Label·
  LineEdit·SpinBox·H/VSeparator)+预盖 CheckBox/CheckButton/OptionButton/
  PopupMenu 弹层;default_font 显式指现役同字体(V6 中文字形防御);
  位图生成脚本 tools/gen_theme_icons.py(9 张纯色/几何小位图→assets/art/ui/gen/,
  按 .gitignore 政策本地生成不入库);色值溯源表 game/assets/theme/README.md
- T1: ui_kit.gd 删 tooltip_theme()/_tip_theme 与 styled_slider()、ui_assembler.gd
  删挂载行(旧实现同批删除);settings_overlay 几何内联、debug_overlay 双轨消隐
- T2: 冒烟 _smoke_global_theme() 38 断言(V1 类型覆盖+Button/focus 双红线+
  V2 冻结值 5.49:1+V3 预盖证词+V4 退役零残留+V6 字体防御)
- 引擎事实(4.7.2 源码+默认主题链双查):CheckButton 与 CheckBox 同套
  checked/unchecked 图标名;Slider 无 grabber_pressed 槽;ScrollBar 柄=StyleBox
This commit is contained in:
DKChan committed 2026-10-13 21:39:01 +08:00
1 parent 5f8acd0f0d
commit af2d834e78
9 files changed
+527 -33

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""UI 全局主题批小位图生成脚本(迭代 25 T0,2026-10-13)。
产出(→ game/assets/art/ui/gen/,纯色/几何、像素风):
slider_grabber.png 滑轨柄·常 12×12 CANDLE #F6C667 + PANEL_BORDER #786E58 1px 描边
slider_grabber_highlight.png 滑轨柄·悬 12×12 CANDLE 提亮档(lightened≈12%) + 同描边
slider_grabber_disabled.png 滑轨柄·废 12×12 STONE_DEEP #554D41 + 同描边
spinbox_updown.png SpinBox 箭头 16×16 INK #33261B ▲▼ 合一(Godot 4 SpinBox 走 updown 合并图标槽)
option_arrow.png OptionButton 箭头 8×8 INK 向下三角
checkbox_unchecked.png 勾选框·空 14×14 PANEL_DATA #E9D9B8 + PANEL_BORDER 边
checkbox_checked.png 勾选框·选 14×14 同底边 + INK 对勾
checkbox_unchecked_disabled.png 勾选框·空·废 14×14 同底 + STONE_DEEP 边
checkbox_checked_disabled.png 勾选框·选·废 14×14 同底边 + STONE_DEEP 对勾
色值全部取自 game/presentation/views/ui_kit.gd 既有常量(溯源表见 game/assets/theme/README.md)——
不新增美术素材、不动美术圣经 §7(迭代 25 契约)。滚动条柄不走位图:Godot 4 ScrollBar 的
grabber 是 StyleBox(非 Icon),纯色 StyleBoxFlat 直接承载(见 main_theme.tres)。
♻ 可重跑:本脚本幂等,重复执行覆盖产出;改色值只改本脚本顶部常量后重跑 + 重导。
用法: python tools/gen_theme_icons.py
"""
import os
from PIL import Image
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT = os.path.join(ROOT, "game", "assets", "art", "ui", "gen")
# ── 色值(↔ ui_kit.gd 常量,冻结)────────────────────────────────────────────
INK = (51, 38, 27) # INK #33261B
PANEL_DATA = (233, 217, 184) # PANEL_DATA #E9D9B8
PANEL_BORDER = (120, 110, 88) # PANEL_BORDER #786E58
STONE_DEEP = (85, 77, 65) # STONE_DEEP #554D41
CANDLE = (246, 198, 103) # CANDLE #F6C667
def lighten(c, amount=0.12):
"""复刻 Godot Color.lightened:每通道 + (255-通道)×amount(sRGB 存储值域)"""
return tuple(min(255, round(v + (255 - v) * amount)) for v in c)
def save(img, name):
path = os.path.join(OUT, name)
img.save(path)
print("wrote %s (%dx%d)" % (name, img.width, img.height))
def slider_grabber(fill):
"""12×12 方柄:fill 实心 + PANEL_BORDER 1px 描边"""
img = Image.new("RGBA", (12, 12), (0, 0, 0, 0))
for x in range(12):
for y in range(12):
edge = x in (0, 11) or y in (0, 11)
img.putpixel((x, y), PANEL_BORDER + (255,) if edge else fill + (255,))
return img
def spinbox_updown():
"""16×16 ▲▼ 合一:上三角 y2-7、下三角 y9-14,各宽 12(INK)"""
img = Image.new("RGBA", (16, 16), (0, 0, 0, 0))
# 上三角:顶行中心 2px(x7-8),第 6 行满宽 x2-13
for row in range(6):
half = 1 + row # 第 row 行左右各扩 1
for x in range(8 - half, 8 + half):
img.putpixel((x, 2 + row), INK + (255,))
# 下三角:镜像(第 row 行满宽在底)
for row in range(6):
half = 6 - row
for x in range(8 - half, 8 + half):
img.putpixel((x, 9 + row), INK + (255,))
return img
def option_arrow():
"""8×8 向下三角(OptionButton 箭头)"""
img = Image.new("RGBA", (8, 8), (0, 0, 0, 0))
for row in range(5):
half = row + 1
for x in range(4 - half, 4 + half - 1):
img.putpixel((x, 1 + row), INK + (255,))
return img
def checkbox(border, mark):
"""14×14 勾选框:PANEL_DATA 底 + 1px 边;mark 非 None 时画 2px 对勾"""
img = Image.new("RGBA", (14, 14), (0, 0, 0, 0))
for x in range(14):
for y in range(14):
edge = x in (0, 13) or y in (0, 13)
img.putpixel((x, y), border + (255,) if edge else PANEL_DATA + (255,))
if mark is not None:
# 对勾:(3,7)→(6,10)→(11,3),2px 粗(内区 1..12)
seg = [(3, 7), (4, 7), (4, 8), (5, 8), (5, 9), (6, 9),
(6, 10), (7, 10), (7, 11), (8, 11), (8, 10), (9, 10),
(9, 9), (10, 9), (10, 8), (11, 8),
(11, 7), (10, 7), (10, 6), (9, 6), (9, 5), (8, 5),
(8, 4), (7, 4), (7, 3), (6, 3)]
for (x, y) in seg:
if 1 <= x <= 12 and 1 <= y <= 12:
img.putpixel((x, y), mark + (255,))
return img
def main():
os.makedirs(OUT, exist_ok=True)
save(slider_grabber(CANDLE), "slider_grabber.png")
save(slider_grabber(lighten(CANDLE)), "slider_grabber_highlight.png")
save(slider_grabber(STONE_DEEP), "slider_grabber_disabled.png")
save(spinbox_updown(), "spinbox_updown.png")
save(option_arrow(), "option_arrow.png")
save(checkbox(PANEL_BORDER, None), "checkbox_unchecked.png")
save(checkbox(PANEL_BORDER, INK), "checkbox_checked.png")
save(checkbox(STONE_DEEP, None), "checkbox_unchecked_disabled.png")
save(checkbox(STONE_DEEP, STONE_DEEP), "checkbox_checked_disabled.png")
print("done → %s" % OUT)
if __name__ == "__main__":
main()