From af2d834e78e3e9094df04e61557100d195ee8aac Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Tue, 13 Oct 2026 21:39:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E8=BF=AD=E4=BB=A325):=20UI=20=E5=85=A8?= =?UTF-8?q?=E5=B1=80=E4=B8=BB=E9=A2=98=E6=89=B9=20T0-T2=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E4=B8=BB=E9=A2=98=E8=B5=84=E6=BA=90=E6=8C=82?= =?UTF-8?q?=E8=BD=BD+=E4=B8=A4=E5=A4=84=E5=B1=80=E9=83=A8=E5=81=9A?= =?UTF-8?q?=E6=B3=95=E9=80=80=E5=BD=B9+=E5=86=92=E7=83=9F=20V1-V6=20?= =?UTF-8?q?=E6=96=AD=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- game/assets/theme/README.md | 59 ++++++ game/assets/theme/main_theme.tres | 222 ++++++++++++++++++++ game/presentation/ui_assembler.gd | 4 +- game/presentation/views/debug_overlay.gd | 2 +- game/presentation/views/settings_overlay.gd | 6 +- game/presentation/views/ui_kit.gd | 33 +-- game/project.godot | 1 + game/smoke/smoke_all.gd | 113 ++++++++++ tools/gen_theme_icons.py | 120 +++++++++++ 9 files changed, 527 insertions(+), 33 deletions(-) create mode 100644 game/assets/theme/README.md create mode 100644 game/assets/theme/main_theme.tres create mode 100644 tools/gen_theme_icons.py diff --git a/game/assets/theme/README.md b/game/assets/theme/README.md new file mode 100644 index 00000000..31386817 --- /dev/null +++ b/game/assets/theme/README.md @@ -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)。 diff --git a/game/assets/theme/main_theme.tres b/game/assets/theme/main_theme.tres new file mode 100644 index 00000000..558372d7 --- /dev/null +++ b/game/assets/theme/main_theme.tres @@ -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 diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index d4cb9e3e..d6e11dcd 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -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() diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 1a1b51b4..3800013f 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -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) diff --git a/game/presentation/views/settings_overlay.gd b/game/presentation/views/settings_overlay.gd index 96733f8a..f44918a4 100644 --- a/game/presentation/views/settings_overlay.gd +++ b/game/presentation/views/settings_overlay.gd @@ -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) diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 42680f90..2b63f6c1 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -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) diff --git a/game/project.godot b/game/project.godot index 488ca4bd..c3d1b8bc 100644 --- a/game/project.godot +++ b/game/project.godot @@ -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] diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 1dc37529..c023dcd0 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -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(红线)。 diff --git a/tools/gen_theme_icons.py b/tools/gen_theme_icons.py new file mode 100644 index 00000000..b5ff694c --- /dev/null +++ b/tools/gen_theme_icons.py @@ -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()