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

+59
View File
@@ -0,0 +1,59 @@
# game/assets/theme/ —— UI 全局主题资源(迭代 25 · UI 全局主题批)
- 主文件:`main_theme.tres`(Godot 4 Theme 资源,挂 `project.godot` 的 `gui/theme/custom`)
- 生成脚本:`tools/gen_theme_icons.py`(可重跑,幂等)→ 小位图落 `../art/ui/gen/`
- 依据:需求卡片 `design/research/需求卡片-UI全局主题批-2026-10-13.md` v1.2(2026-10-13)
本目录不含内联注释(.tres 注释语法不赌),全部溯源信息以本表为准。**色值一律取自
`game/presentation/views/ui_kit.gd` 既有常量,零新增色值**(迭代 25 契约:不新增美术素材)。
## 色值溯源表(字面量 ↔ ui_kit 常量)
| ui_kit 常量 | 值 | 本主题用途 |
|---|---|---|
| BG_OVERLAY | #362B20 α0.35 | 滚动条槽(V/H) |
| PANEL_BORDER | #786E58 | 滚动条柄·常、滑轨槽描边、LineEdit/PopupMenu 边、分隔线 α0.6、勾选框边、滑轨柄描边、SpinBox 箭头底系 |
| STONE_FAINT | #8C7C64 | 滚动条柄·悬、SpinBox 箭头底·悬(α0.6,白名单#5) |
| STONE_DEEP | #554D41 | 滚动条柄·按、滑轨柄·废、勾选框·废、SpinBox 箭头底·按、LineEdit 占位字色 |
| BG_ROOT | #3A2E22 | 滑轨已填充段 grabber_area(+highlight) |
| 暖石(无常量,ui_kit.gd:743 字面量) | #B8A582 | 滑轨槽 slider(承 styled_slider 现值) |
| CANDLE | #F6C667 | 滑轨柄·常 |
| CANDLE 提亮档(lightened≈12%,≈#F7E08C) | — | 滑轨柄·悬(白名单#5;Godot lightened 同式) |
| GOLD_DEEP | #B07A14 | 滑轨柄·按——**Godot 4.7 Slider 无 grabber_pressed 图标槽**(实证:引擎源码+默认主题链双查),值留档未落位 |
| WARM | #F0E2C8 | TooltipLabel 字色(承 tooltip_theme() 现值) |
| tooltip 面板底/边(ui_kit.gd:757-759 现值) | Color(0.09,0.07,0.05,0.96) / #5A5044 | TooltipPanel |
| PANEL_DATA | #E9D9B8 | LineEdit 底、勾选框空框底、PopupMenu 面板底 |
| INK | #33261B | LineEdit/PopupMenu/OptionButton 字色、SpinBox 箭头、勾选框对勾 |
| INK α0.45 | — | OptionButton 废字色(同 _apply_button_skin 惯例) |
| GOLD_LIT | #E6BE5A | PopupMenu 条目 hover 底 |
| INK_ON_GOLD | #3C2C14 | PopupMenu 条目 hover 字色 |
| RADIUS_OBJECT | 5 | LineEdit 圆角(同数据框惯例 ui_kit.gd:476) |
| 默认字体 | res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf | 主题 `default_font`(与 project.godot `theme/custom_font` 同文件——v1.2 防御条款,防挂主题后中文字形缺失) |
## 词条落位总览
| 类型 | 词条 | 载体 |
|---|---|---|
| VScrollBar / HScrollBar | scroll / grabber / grabber_highlight / grabber_pressed | StyleBoxFlat 纯色(**Godot 4 滚动条柄是 StyleBox 非 Icon**,无需位图;content margin 4 = 8px 粗细,柄最小 8px) |
| ScrollContainer | panel | StyleBoxFlat 全透明(默认即 Empty 不可见,显式接管) |
| HSlider / VSlider | slider / grabber_area / grabber_area_highlight / grabber / grabber_highlight / grabber_disabled | StyleBoxFlat + 生成位图(12×12 方柄;VSlider 边距为 H 的旋转换位) |
| TooltipPanel / TooltipLabel | panel / font_color / font_size | 承 tooltip_theme() 现值原样迁入 |
| LineEdit | normal / font_color / font_placeholder_color | StyleBoxFlat + 色词条(**占位色词条名 = `font_placeholder_color`**,4.7.2 实证;`font_color_placeholder` 不存在) |
| SpinBox | updown / up·down_background_hovered·pressed | **Godot 4 SpinBox 走 `updown` 合并图标槽**(定义了它就不再画 up/down 分件,引擎源码实证);hover/pressed 箭头底默认主题可见,按白名单#5 覆盖 |
| HSeparator / VSeparator | separator | StyleBoxLine(PANEL_BORDER α0.6 单线,1px) |
| CheckBox / CheckButton | unchecked / checked / unchecked_disabled / checked_disabled | **两类共用同一套图标名**(checked/unchecked,4.7.2 源码实证——卡片 v1.2 注③「CheckButton 用 on/off」不成立,已按引擎事实落位并呈报追认) |
| OptionButton | normal / hover / pressed / disabled / font_* / arrow | button_stone 九宫格(8px 边距,同 _apply_button_skin 观感)+ INK 字 + 生成箭头 |
| PopupMenu | panel / hover / font_color / font_hover_color / font_size | StyleBoxFlat(预盖件,含弹层) |
## 红线遵守记录
- Button 类型词条**零接触**(五态矩阵归 06 第一层第二条);OptionButton 为独立主题类型(预盖件)。
- focus 族词条**零定义**(scroll_focus / LineEdit focus / font_focus_color 等一律不写)。
- 全局纹理过滤未动(`rendering/textures/canvas_textures/default_texture_filter` 归 06 第一层第三条);
Godot 4.7.2 导入参数已无逐贴图 filter 标志(现存 art/ui/*.import 实证),小位图均为原生尺寸消费,无滤波伪影。
- default_font_size **未设**(全站字号走 UiKit.FS_* 逐实例 override)。
## 专项观察点(v1.2 增补,归用户目检)
1. 滑轨柄 CANDLE 色是否比「关闭」按钮抢眼(违反焦点预算嫌疑)——若是走 L2 微调(候选 STONE 灰石系)。
2. 滚动条槽 BG_OVERLAY α0.35 叠 BG_ROOT 上是否可见——若否走 L2 微调(候选提不透明度或 SLOT_BACKDROP #3E362C)。
+222
View File
@@ -0,0 +1,222 @@
[gd_resource type="Theme" format=3]
[ext_resource type="FontFile" path="res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf" id="1_font"]
[ext_resource type="Texture2D" path="res://assets/art/ui/button_stone.png" id="2_btn"]
[ext_resource type="Texture2D" path="res://assets/art/ui/button_stone_pressed.png" id="3_btnp"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/slider_grabber.png" id="4_sg"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/slider_grabber_highlight.png" id="5_sgh"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/slider_grabber_disabled.png" id="6_sgd"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/spinbox_updown.png" id="7_spu"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/option_arrow.png" id="8_oa"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/checkbox_unchecked.png" id="9_cbu"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/checkbox_checked.png" id="10_cbc"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/checkbox_unchecked_disabled.png" id="11_cbud"]
[ext_resource type="Texture2D" path="res://assets/art/ui/gen/checkbox_checked_disabled.png" id="12_cbcd"]
[sub_resource type="StyleBoxFlat" id="SBF_vscroll_track"]
content_margin_left = 4.0
content_margin_right = 4.0
bg_color = Color(0.211765, 0.168627, 0.12549, 0.35)
[sub_resource type="StyleBoxFlat" id="SBF_vscroll_grab"]
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.470588, 0.431373, 0.345098, 1)
[sub_resource type="StyleBoxFlat" id="SBF_vscroll_grab_h"]
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.54902, 0.486275, 0.392157, 1)
[sub_resource type="StyleBoxFlat" id="SBF_vscroll_grab_p"]
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.333333, 0.301961, 0.254902, 1)
[sub_resource type="StyleBoxFlat" id="SBF_hscroll_track"]
content_margin_top = 4.0
content_margin_bottom = 4.0
bg_color = Color(0.211765, 0.168627, 0.12549, 0.35)
[sub_resource type="StyleBoxFlat" id="SBF_hscroll_grab"]
content_margin_left = 4.0
content_margin_right = 4.0
bg_color = Color(0.470588, 0.431373, 0.345098, 1)
[sub_resource type="StyleBoxFlat" id="SBF_hscroll_grab_h"]
content_margin_left = 4.0
content_margin_right = 4.0
bg_color = Color(0.54902, 0.486275, 0.392157, 1)
[sub_resource type="StyleBoxFlat" id="SBF_hscroll_grab_p"]
content_margin_left = 4.0
content_margin_right = 4.0
bg_color = Color(0.333333, 0.301961, 0.254902, 1)
[sub_resource type="StyleBoxFlat" id="SBF_sc_panel"]
bg_color = Color(0, 0, 0, 0)
[sub_resource type="StyleBoxFlat" id="SBF_hslider_track"]
content_margin_top = 6.0
content_margin_bottom = 6.0
bg_color = Color(0.721569, 0.647059, 0.509804, 1)
border_color = Color(0.470588, 0.431373, 0.345098, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
[sub_resource type="StyleBoxFlat" id="SBF_hslider_fill"]
content_margin_top = 6.0
content_margin_bottom = 6.0
bg_color = Color(0.227451, 0.180392, 0.133333, 1)
[sub_resource type="StyleBoxFlat" id="SBF_vslider_track"]
content_margin_left = 6.0
content_margin_right = 6.0
bg_color = Color(0.721569, 0.647059, 0.509804, 1)
border_color = Color(0.470588, 0.431373, 0.345098, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
[sub_resource type="StyleBoxFlat" id="SBF_vslider_fill"]
content_margin_left = 6.0
content_margin_right = 6.0
bg_color = Color(0.227451, 0.180392, 0.133333, 1)
[sub_resource type="StyleBoxFlat" id="SBF_tooltip"]
content_margin_left = 8.0
content_margin_top = 6.0
content_margin_right = 8.0
content_margin_bottom = 6.0
bg_color = Color(0.09, 0.07, 0.05, 0.96)
border_color = Color(0.352941, 0.313725, 0.266667, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
[sub_resource type="StyleBoxFlat" id="SBF_lineedit"]
content_margin_left = 6.0
content_margin_top = 3.0
content_margin_right = 6.0
content_margin_bottom = 3.0
bg_color = Color(0.913725, 0.85098, 0.721569, 1)
border_color = Color(0.470588, 0.431373, 0.345098, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
corner_radius_top_left = 5
corner_radius_top_right = 5
corner_radius_bottom_right = 5
corner_radius_bottom_left = 5
[sub_resource type="StyleBoxFlat" id="SBF_spin_btn_h"]
bg_color = Color(0.54902, 0.486275, 0.392157, 0.6)
[sub_resource type="StyleBoxFlat" id="SBF_spin_btn_p"]
bg_color = Color(0.333333, 0.301961, 0.254902, 1)
[sub_resource type="StyleBoxLine" id="SBLine_hsep"]
color = Color(0.470588, 0.431373, 0.345098, 0.6)
thickness = 1
[sub_resource type="StyleBoxLine" id="SBLine_vsep"]
color = Color(0.470588, 0.431373, 0.345098, 0.6)
thickness = 1
vertical = true
[sub_resource type="StyleBoxTexture" id="SBTex_btn"]
texture = ExtResource("2_btn")
texture_margin_left = 8.0
texture_margin_top = 8.0
texture_margin_right = 8.0
texture_margin_bottom = 8.0
[sub_resource type="StyleBoxTexture" id="SBTex_btnp"]
texture = ExtResource("3_btnp")
texture_margin_left = 8.0
texture_margin_top = 8.0
texture_margin_right = 8.0
texture_margin_bottom = 8.0
[sub_resource type="StyleBoxFlat" id="SBF_popup_panel"]
content_margin_left = 8.0
content_margin_top = 6.0
content_margin_right = 8.0
content_margin_bottom = 6.0
bg_color = Color(0.913725, 0.85098, 0.721569, 1)
border_color = Color(0.470588, 0.431373, 0.345098, 1)
border_width_left = 1
border_width_top = 1
border_width_right = 1
border_width_bottom = 1
[sub_resource type="StyleBoxFlat" id="SBF_popup_hover"]
content_margin_left = 6.0
content_margin_top = 2.0
content_margin_right = 6.0
content_margin_bottom = 2.0
bg_color = Color(0.901961, 0.745098, 0.352941, 1)
[resource]
default_font = ExtResource("1_font")
VScrollBar/styles/scroll = SubResource("SBF_vscroll_track")
VScrollBar/styles/grabber = SubResource("SBF_vscroll_grab")
VScrollBar/styles/grabber_highlight = SubResource("SBF_vscroll_grab_h")
VScrollBar/styles/grabber_pressed = SubResource("SBF_vscroll_grab_p")
HScrollBar/styles/scroll = SubResource("SBF_hscroll_track")
HScrollBar/styles/grabber = SubResource("SBF_hscroll_grab")
HScrollBar/styles/grabber_highlight = SubResource("SBF_hscroll_grab_h")
HScrollBar/styles/grabber_pressed = SubResource("SBF_hscroll_grab_p")
ScrollContainer/styles/panel = SubResource("SBF_sc_panel")
HSlider/styles/slider = SubResource("SBF_hslider_track")
HSlider/styles/grabber_area = SubResource("SBF_hslider_fill")
HSlider/styles/grabber_area_highlight = SubResource("SBF_hslider_fill")
HSlider/icons/grabber = ExtResource("4_sg")
HSlider/icons/grabber_highlight = ExtResource("5_sgh")
HSlider/icons/grabber_disabled = ExtResource("6_sgd")
VSlider/styles/slider = SubResource("SBF_vslider_track")
VSlider/styles/grabber_area = SubResource("SBF_vslider_fill")
VSlider/styles/grabber_area_highlight = SubResource("SBF_vslider_fill")
VSlider/icons/grabber = ExtResource("4_sg")
VSlider/icons/grabber_highlight = ExtResource("5_sgh")
VSlider/icons/grabber_disabled = ExtResource("6_sgd")
TooltipPanel/styles/panel = SubResource("SBF_tooltip")
TooltipLabel/colors/font_color = Color(0.941176, 0.886275, 0.784314, 1)
TooltipLabel/font_sizes/font_size = 14
LineEdit/styles/normal = SubResource("SBF_lineedit")
LineEdit/colors/font_color = Color(0.2, 0.14902, 0.105882, 1)
LineEdit/colors/font_placeholder_color = Color(0.333333, 0.301961, 0.254902, 1)
SpinBox/icons/updown = ExtResource("7_spu")
SpinBox/styles/up_background_hovered = SubResource("SBF_spin_btn_h")
SpinBox/styles/up_background_pressed = SubResource("SBF_spin_btn_p")
SpinBox/styles/down_background_hovered = SubResource("SBF_spin_btn_h")
SpinBox/styles/down_background_pressed = SubResource("SBF_spin_btn_p")
HSeparator/styles/separator = SubResource("SBLine_hsep")
VSeparator/styles/separator = SubResource("SBLine_vsep")
CheckBox/icons/unchecked = ExtResource("9_cbu")
CheckBox/icons/checked = ExtResource("10_cbc")
CheckBox/icons/unchecked_disabled = ExtResource("11_cbud")
CheckBox/icons/checked_disabled = ExtResource("12_cbcd")
CheckButton/icons/unchecked = ExtResource("9_cbu")
CheckButton/icons/checked = ExtResource("10_cbc")
CheckButton/icons/unchecked_disabled = ExtResource("11_cbud")
CheckButton/icons/checked_disabled = ExtResource("12_cbcd")
OptionButton/styles/normal = SubResource("SBTex_btn")
OptionButton/styles/hover = SubResource("SBTex_btn")
OptionButton/styles/pressed = SubResource("SBTex_btnp")
OptionButton/styles/disabled = SubResource("SBTex_btnp")
OptionButton/colors/font_color = Color(0.2, 0.14902, 0.105882, 1)
OptionButton/colors/font_hover_color = Color(0.2, 0.14902, 0.105882, 1)
OptionButton/colors/font_pressed_color = Color(0.2, 0.14902, 0.105882, 1)
OptionButton/colors/font_disabled_color = Color(0.2, 0.14902, 0.105882, 0.45)
OptionButton/icons/arrow = ExtResource("8_oa")
PopupMenu/styles/panel = SubResource("SBF_popup_panel")
PopupMenu/styles/hover = SubResource("SBF_popup_hover")
PopupMenu/colors/font_color = Color(0.2, 0.14902, 0.105882, 1)
PopupMenu/colors/font_hover_color = Color(0.235294, 0.172549, 0.078431, 1)
PopupMenu/font_sizes/font_size = 16
+2 -2
View File
@@ -99,10 +99,10 @@ static func build_overlays(host: Control, db) -> Dictionary:
# UI 落码批 M2(2026-10-04):单列 VBox→双列(左操作 56% + 右舞台);按钮动词化+悬停解释
#(解释全迁 tooltip,G5);lvbtn 升主操作钮(烛光黄 CANDLE);右列=立绘看台。**玩法速览牌已退役
#(2026-10-06 用户令:界面禁玩法文字墙——教学内容归 Beta 帮助手册〔教学引导定稿批 P4 丙〕,历史文案在 git)**;
# root 挂 UiKit.tooltip_theme() 全树生效。返回键集不变,组合根接线零改)
# tooltip 主题归全局主题单点(迭代 25 2026-10-13——旧 root 挂程序化 tooltip 主题的做法退役)。
# 返回键集不变,组合根接线零改)
static func build_start_overlay(host: Control, db = null) -> Dictionary:
var overlay := overlay_bg(host, UiKit.BG_BRIGHT)
overlay.theme = UiKit.tooltip_theme()
# 石纹弱质感+斜光带+暗角(UI 对齐批 T5 2026-10-06——demo .tilebg 0.14/.beams/.vignette 三层;
# 旧满铺石纹不透退役。缺位不挂即回退平色)
var bd := UiKit.backdrop_tile()
+1 -1
View File
@@ -278,7 +278,7 @@ func _slider_row(box: VBoxContainer, title: String) -> Dictionary:
s.min_value = 0
s.max_value = 300
s.step = 5
s.custom_minimum_size = Vector2(320, 24)
s.custom_minimum_size = Vector2(320, 24) # 皮肤走全局主题(迭代 25:与设置屏音量滑轨同一构建方式,消双轨)
row.add_child(s)
var v := UiKit.label("+0%",UiKit.FS_BODY, true)
v.custom_minimum_size = Vector2(70, 0)
+5 -1
View File
@@ -56,7 +56,11 @@ func _build() -> void:
var vol_lab := UiKit.label("主音量",UiKit.FS_BODY, false, UiKit.INK_LABEL)
vol_lab.custom_minimum_size = Vector2(140, 0) # T8 label 列定宽(两列对齐)
vol_row.add_child(vol_lab)
_volume = UiKit.styled_slider(100.0, 0.0, 100.0)
_volume = HSlider.new()
_volume.min_value = 0.0
_volume.max_value = 100.0
_volume.value = 100.0
_volume.custom_minimum_size = Vector2(200, 24) # 几何/量程内联(迭代 25:样式滑杆工厂退役,皮肤走全局主题)
_volume.step = 1
_volume.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_volume.value_changed.connect(_on_volume_changed)
+4 -29
View File
@@ -731,32 +731,7 @@ static func switch_toggle(initial: bool) -> SwitchToggle:
return SwitchToggle.new(initial)
# 样式滑杆(设置屏音量——轨道暖石+默认柄;范围/读数由消费方接)
static func styled_slider(value: float, minv := 0.0, maxv := 100.0) -> HSlider:
var s := HSlider.new()
s.min_value = minv
s.max_value = maxv
s.value = value
s.custom_minimum_size = Vector2(200, 24)
# 槽轨/填充实高(2026-10-06 判官实checkpoint:HSlider 槽=按 stylebox min 高绘制——零 margin
# StyleBoxFlat min 高 0-2px=轨道整条不可见;6px 上下 content margin=12px 实高)
s.add_theme_stylebox_override("slider",
panel_flat(Color8(184, 165, 130), PANEL_BORDER, 0, 1, Vector4(0, 6, 0, 6)))
s.add_theme_stylebox_override("grabber_area",
panel_flat(Color8(122, 82, 50), Color8(0, 0, 0, 0), 0, 0, Vector4(0, 6, 0, 6)))
return s
# tooltip 全站主题(深底多行素牌——§7.2 文字可读硬指标;消费方=任一祖先 Control.theme 挂一次全树生效)
static var _tip_theme: Theme = null
static func tooltip_theme() -> Theme:
if _tip_theme != null:
return _tip_theme
var th := Theme.new()
th.set_stylebox("panel", "TooltipPanel",
panel_flat(Color(0.09, 0.07, 0.05, 0.96), Color8(90, 80, 68), 0, 1, Vector4(8, 6, 8, 6)))
th.set_color("font_color", "TooltipLabel", WARM)
th.set_font_size("font_size", "TooltipLabel", 14)
_tip_theme = th
return _tip_theme
# 样式滑杆/tooltip 主题两处局部做法已退役(迭代 25 UI 全局主题批 2026-10-13——词条迁
# game/assets/theme/main_theme.tres 全局主题单点:滑轨 slider/grabber_area/grabber、
# TooltipPanel/TooltipLabel;旧实现同批删除,禁新旧并存。消费方几何内联:
# settings_overlay 音量滑轨 / debug_overlay _slider_row)
+1
View File
@@ -28,6 +28,7 @@ window/stretch/mode="canvas_items"
[gui]
theme/custom_font="res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf"
theme/custom="res://assets/theme/main_theme.tres"
[rendering]
+113
View File
@@ -25,6 +25,19 @@ func check(cond: bool, label: String, quiet: bool = false) -> void:
print(" [FAIL] " + label)
# WCAG 相对亮度/对比度(迭代 25 UI 全局主题批 V2——滑轨两段对比度断言用)
func _theme_rel_luminance(c: Color) -> float:
var lin := func(ch: float) -> float:
return ch / 12.92 if ch <= 0.03928 else pow((ch + 0.055) / 1.055, 2.4)
return 0.2126 * lin.call(c.r) + 0.7152 * lin.call(c.g) + 0.0722 * lin.call(c.b)
func _theme_contrast(a: Color, b: Color) -> float:
var la := _theme_rel_luminance(a)
var lb := _theme_rel_luminance(b)
return (maxf(la, lb) + 0.05) / (minf(la, lb) + 0.05)
func _initialize() -> void:
print("=== Dungeon Sweeper 垂直切片冒烟 ===")
var db = ConfigDBScript.new()
@@ -93,6 +106,7 @@ func _run_core_smoke(db, rng) -> void:
_smoke_damage_types()
_smoke_playtest_logger()
_smoke_battle_log_view()
_smoke_global_theme()
_smoke_encounter_roll()
_smoke_attack_chain_fresh_pool()
_smoke_clear_reward_chain()
@@ -5343,6 +5357,105 @@ func _smoke_battle_log_view() -> void:
check(view.parsed_text().length() > before, "行型 %s 格式化落地(解析文本增长)" % String(row_type))
# ---- UI 全局主题冒烟(迭代 25 UI 全局主题批 2026-10-13——需求卡片 §7 验收清单 V1-V6)----
# V1 挂载覆盖 / V2 滑轨两段修复 / V3 预盖件运行时证词 / V4 换代退役零残留 /
# V5 回归(本冒烟全绿即回归网)/ V6 中文字体防御(挂主题后 default_font 顶掉 custom_font 也不丢字形)
func _smoke_global_theme() -> void:
print("--- UI 全局主题(main_theme.tres 挂载与词条断言)---")
var th := ThemeDB.get_project_theme()
check(th != null, "V1 项目主题挂载(project.godot gui/theme/custom → main_theme.tres)")
if th == null:
return
# V1:类型集覆盖 §4「做」清单全部类型(含 PopupMenu 弹层预盖)
var need_types := ["VScrollBar", "HScrollBar", "ScrollContainer", "HSlider", "VSlider",
"TooltipPanel", "TooltipLabel", "LineEdit", "SpinBox", "HSeparator", "VSeparator",
"CheckBox", "CheckButton", "OptionButton", "PopupMenu"]
var type_set := th.get_type_list()
for t in need_types:
check(type_set.has(t), "V1 主题类型覆盖 %s" % t)
# V1 红线:Button 类型词条零接触(五态矩阵归 06 第一层第二条)
check(not type_set.has("Button"), "V1 红线 Button 类型词条零接触")
# V1 红线:focus 族词条一律零定义(扫本主题自有条目,不走默认主题链)
var focus_hits := ""
for t in type_set:
for n in (th.get_stylebox_list(t) + th.get_icon_list(t) + th.get_color_list(t)
+ th.get_font_list(t) + th.get_font_size_list(t) + th.get_constant_list(t)):
if "focus" in String(n):
focus_hits += "%s/%s " % [t, n]
check(focus_hits == "", "V1 红线 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"))
# V1:关键词条抽查在位(实例走主题链解析)
var vsb := VScrollBar.new()
check(vsb.has_theme_stylebox("scroll") and vsb.has_theme_stylebox("grabber")
and vsb.has_theme_stylebox("grabber_highlight") and vsb.has_theme_stylebox("grabber_pressed"),
"V1 滚动条槽/柄三态词条在位")
check(not th.has_stylebox("scroll_focus", "VScrollBar") and not th.has_stylebox("scroll_focus", "HScrollBar"),
"V1 滚动条 scroll_focus 未定义(红线)")
var hsl := HSlider.new()
check(hsl.has_theme_stylebox("slider") and hsl.has_theme_stylebox("grabber_area")
and hsl.has_theme_stylebox("grabber_area_highlight")
and hsl.has_theme_icon("grabber") and hsl.has_theme_icon("grabber_highlight")
and hsl.has_theme_icon("grabber_disabled"), "V1 滑轨槽/填充/柄词条在位")
var le := LineEdit.new()
check(le.has_theme_stylebox("normal") and le.has_theme_color("font_color")
and le.has_theme_color("font_placeholder_color"), "V1 LineEdit 词条在位(含占位色 font_placeholder_color)")
var sp := SpinBox.new()
check(sp.has_theme_icon("updown"), "V1 SpinBox 箭头词条在位(updown 合并槽)")
var hsep := HSeparator.new()
var vsep := VSeparator.new()
check(hsep.has_theme_stylebox("separator") and vsep.has_theme_stylebox("separator"), "V1 分隔线词条在位")
var tpanel := PanelContainer.new()
var tlabel := Label.new()
check(tpanel.has_theme_stylebox("panel"), "V1 TooltipPanel 词条在位")
check(tlabel.has_theme_color("font_color") and tlabel.has_theme_font_size("font_size"), "V1 TooltipLabel 词条在位")
# V2:滑轨修复——已填充段 BG_ROOT 对槽暖石 ≈5.5:1(冻结值锚定)
var fill_sb: StyleBox = hsl.get_theme_stylebox("grabber_area")
var track_sb: StyleBox = hsl.get_theme_stylebox("slider")
if fill_sb is StyleBoxFlat and track_sb is StyleBoxFlat:
var fill_c: Color = (fill_sb as StyleBoxFlat).bg_color
var track_c: Color = (track_sb as StyleBoxFlat).bg_color
check(fill_c.is_equal_approx(Color8(58, 46, 34)) and track_c.is_equal_approx(Color8(184, 165, 130)),
"V2 冻结值锚定(已填充段=BG_ROOT #3A2E22,槽=暖石 #B8A582)")
var ratio := _theme_contrast(fill_c, track_c)
check(ratio >= 4.5, "V2 滑轨两段对比 %.2f:1 ≥4.5:1(拖到中点一眼可辨)" % ratio)
else:
check(false, "V2 滑轨 stylebox 类型均为 StyleBoxFlat")
# V3:预盖件运行时证词(临时实例化读词条——无实役控件的在役证据)
var cbx := CheckBox.new()
check(cbx.get_theme_icon("checked") != null and cbx.get_theme_icon("unchecked") != null
and cbx.get_theme_icon("checked_disabled") != null and cbx.get_theme_icon("unchecked_disabled") != null,
"V3 CheckBox 四态图标非空")
var cbt := CheckButton.new()
check(cbt.get_theme_icon("checked") != null and cbt.get_theme_icon("unchecked") != null
and cbt.get_theme_icon("checked_disabled") != null and cbt.get_theme_icon("unchecked_disabled") != null,
"V3 CheckButton 四态图标非空(与 CheckBox 同套 checked/unchecked 名,4.7 源码实证)")
var pm := PopupMenu.new()
check(pm.has_theme_stylebox("panel") and pm.has_theme_stylebox("hover")
and pm.has_theme_font_size("font_size"), "V3 PopupMenu 词条存在(弹层预盖)")
var ob := OptionButton.new()
check(ob.has_theme_stylebox("normal") and ob.has_theme_stylebox("hover")
and ob.has_theme_stylebox("pressed") and ob.has_theme_color("font_color"),
"V3 OptionButton 词条存在(预盖)")
# V4:换代退役——源码零残留(剥注释行后扫;ui_kit 两函数+挂载行+滑轨工厂)
for path in ["res://presentation/views/ui_kit.gd", "res://presentation/ui_assembler.gd",
"res://presentation/views/settings_overlay.gd", "res://presentation/views/debug_overlay.gd"]:
var src: String = FileAccess.get_file_as_string(path)
var bad := ""
for ln in src.split("\n", false):
var s := ln.strip_edges()
if s.begins_with("#"):
continue
if "styled_slider" in s or "tooltip_theme" in s or "_tip_theme" in s or "overlay.theme" in s:
bad += s + " "
check(bad == "", "V4 退役实现零残留(%s)%s" % [path, "" if bad == "" else "违例:" + bad])
# V6:中文字体防御——default_font 非空且 Label 实取字体非空
check(th.default_font != null, "V6 主题 default_font 非空(挂主题顶掉 custom_font 也不丢中文字形)")
var lbl := Label.new()
lbl.text = "测试"
check(lbl.get_theme_font("font") != null, "V6 Label 实取字体非空(中文字形防御实证)")
vsb.free(); hsl.free(); le.free(); sp.free(); hsep.free(); vsep.free()
tpanel.free(); tlabel.free(); cbx.free(); cbt.free(); pm.free(); ob.free(); lbl.free()
# ---- 存档序列化冒烟(SaveManager——读写往返/新档合法/余额拒花/锻造出域拒/高版本拒读禁写/
# 损坏兜底/迁移链/域界干净/摘要 cap/摘要集成/生涯终身计数域 v6)。测试文件统一 user://smoke_save_*.json 隔离,
# 段首清场+段尾清理——绝不碰 user://save.json(红线)。
+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()