diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index a7aa7b8c..8fa4572c 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -95,6 +95,16 @@ static func build_overlays(host: Control, db, base_atk: int, base_hp: int) -> Di # 单场遭遇用户入口已删(2026-09-13 用户裁决——单场原语仅探针经 debug_session.start_single 使用)) static func build_start_overlay(host: Control, db = null) -> Dictionary: var overlay := overlay_bg(host, UiKit.BG_OVERLAY) + # 石纹底噪底层(样张 v2 冻结批 T2——z 序=dim 之上/内容之下;缺位不挂即回退平色) + var bd_tex := UiKit.load_tex(UiKit.TEX_BACKDROP_STONE) + if bd_tex != null: + var bd := TextureRect.new() + bd.texture = bd_tex + bd.stretch_mode = TextureRect.STRETCH_TILE + bd.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + bd.set_anchors_preset(Control.PRESET_FULL_RECT) + bd.mouse_filter = Control.MOUSE_FILTER_IGNORE + overlay.add_child(bd) var box := overlay_box(overlay) var title := UiKit.label("Dungeon Sweeper", 34, true) title.add_theme_color_override("font_color", UiKit.WARM) diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index aa65e220..3fec96ec 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -41,9 +41,10 @@ var world: Node2D # StageWorld(preload 脚本实例——Node2D 世 func _init(db_ref) -> void: custom_minimum_size = Vector2(0, 252) - # 面板底(PanelContainer 面板样式等价平移——Control 壳不自绘 stylebox,Panel 子面承载) + # 面板底(PanelContainer 面板样式等价平移——Control 壳不自绘 stylebox,Panel 子面承载; + # 石板皮 = 样张 v2 冻结批 T2——缺位回退 PANEL_DATA flat,渲染观感不变) var bg := Panel.new() - var sb := UiKit.panel_flat(UiKit.PANEL_STAGE, UiKit.PANEL_BORDER, 0, 1, Vector4(6, 4, 6, 4)) + var sb := UiKit.panel_skinned(Vector4(6, 4, 6, 4), Color(0.956, 0.952, 0.945)) # tint=PANEL_STAGE 对 PANEL_DATA 比值 bg.add_theme_stylebox_override("panel", sb) bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.mouse_filter = Control.MOUSE_FILTER_IGNORE diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index fcc4167a..0341fc43 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -34,30 +34,39 @@ func _init() -> void: var center := CenterContainer.new() center.set_anchors_preset(Control.PRESET_FULL_RECT) dim.add_child(center) + var frame := PanelContainer.new() + frame.add_theme_stylebox_override("panel", UiKit.frame_skinned(Vector4(26, 22, 26, 22))) # 仪式石框(T2;缺位回退暗面板) + center.add_child(frame) var box := VBoxContainer.new() box.add_theme_constant_override("separation", 6) - center.add_child(box) + frame.add_child(box) var title := UiKit.label("营地 · 永久强化", 24, true) title.add_theme_color_override("font_color", UiKit.WARM) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) + var bal_row := HBoxContainer.new() + bal_row.add_theme_constant_override("separation", 6) + bal_row.alignment = BoxContainer.ALIGNMENT_CENTER + var keystone_icon := UiKit.icon_rect(UiKit.TEX_ICON_KEYSTONE, 18) # 缺位 null → 纯文本回退 + if keystone_icon != null: + bal_row.add_child(keystone_icon) _balance_label = UiKit.label("钥石 0", 18, true) _balance_label.add_theme_color_override("font_color", UiKit.GOLD) - _balance_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - box.add_child(_balance_label) + bal_row.add_child(_balance_label) + box.add_child(bal_row) box.add_child(HSeparator.new()) - box.add_child(_counter_header("属性锻造(钥石直购 · 每级 flat 整数 · 价格几何递增)")) + box.add_child(_counter_header("属性锻造(钥石直购 · 每级 flat 整数 · 价格几何递增)", UiKit.TEX_ICON_FORGE)) _forge_holder = VBoxContainer.new() _forge_holder.add_theme_constant_override("separation", 4) box.add_child(_forge_holder) - box.add_child(_counter_header("道具工坊(累计通关次数解锁 · 局内效果排 Alpha 魔法道具重设计批)")) + box.add_child(_counter_header("道具工坊(累计通关次数解锁 · 局内效果排 Alpha 魔法道具重设计批)", UiKit.TEX_ICON_WORKSHOP)) _workshop_holder = VBoxContainer.new() _workshop_holder.add_theme_constant_override("separation", 4) box.add_child(_workshop_holder) - box.add_child(_counter_header("奖励格图鉴")) + box.add_child(_counter_header("奖励格图鉴", UiKit.TEX_ICON_ATLAS)) var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false) lock_l.add_theme_color_override("font_color", UiKit.WARM_FAINT) box.add_child(lock_l) @@ -80,10 +89,17 @@ func _init() -> void: visible = false -func _counter_header(text: String) -> Label: +func _counter_header(text: String, icon_path := "") -> Control: + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 6) + if icon_path != "": + var ic := UiKit.icon_rect(icon_path, 16) # 缺位 null → 纯文本回退 + if ic != null: + row.add_child(ic) var l := UiKit.label(text, 15, true) l.add_theme_color_override("font_color", UiKit.WARM_SOFT) - return l + row.add_child(l) + return row # ---- 柜台行构建(open_camp 重建;行结构仿 ShopOverlay 商品卡)---- diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 0d2e0c19..9ec21b94 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -22,7 +22,8 @@ var _bonus_list: VBoxContainer func _init() -> void: custom_minimum_size = Vector2(240, 0) clip_contents = true - var sb := UiKit.panel_flat(UiKit.PANEL_HERO, UiKit.PANEL_BORDER, 0, 1, Vector4(8, 6, 8, 6)) + # 主面板穿石板戏服(样张 v2 冻结批 T2——纹理本色≈PANEL_HERO;数据框仍素面 §7.1;缺位回退 flat) + var sb := UiKit.panel_skinned(Vector4(8, 6, 8, 6)) add_theme_stylebox_override("panel", sb) var col := VBoxContainer.new() col.add_theme_constant_override("separation", 14) # 布局充实(2026-09-26 反馈③:区段间距放大) diff --git a/game/presentation/views/legend_overlay.gd b/game/presentation/views/legend_overlay.gd index 9e4640b9..ff70e7fd 100644 --- a/game/presentation/views/legend_overlay.gd +++ b/game/presentation/views/legend_overlay.gd @@ -33,12 +33,15 @@ func _init() -> void: center.set_anchors_preset(Control.PRESET_FULL_RECT) dim.add_child(center) _panel = PanelContainer.new() + _panel.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(12, 10, 12, 10))) # 石板皮(T2;缺位回退 flat) center.add_child(_panel) var box := VBoxContainer.new() box.custom_minimum_size = Vector2(520, 0) box.add_theme_constant_override("separation", 6) _panel.add_child(box) - box.add_child(UiKit.label("图例 · 格态", 20, true)) + var title := UiKit.label("图例 · 格态", 20, true) + title.add_theme_color_override("font_color", UiKit.INK_TITLE) # 亮底墨字(§7.2 数据素面) + box.add_child(title) box.add_child(HSeparator.new()) for e in ENTRIES: var row := HBoxContainer.new() @@ -48,6 +51,7 @@ func _init() -> void: chip.color = Color(e[0]) row.add_child(chip) var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false) + lab.add_theme_color_override("font_color", UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lab.custom_minimum_size = Vector2(440, 0) row.add_child(lab) diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index f58473ae..203a6d2a 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -98,6 +98,18 @@ const TEX_ENEMY_DUERGAR := "res://assets/art/character/character_enemy_duergar.p const TEX_ENEMY_HOOK_HORROR := "res://assets/art/character/character_enemy_hook_horror.png" # 钩爪恐魔 const TEX_ENEMY_DUERGAR_KING := "res://assets/art/character/character_enemy_duergar_king.png" # 灰矮人石君王 +# 框材组纹理槽(样张 v2 冻结批 T2 2026-09-28——♻ 复用件量化改色入包,产物 = tools/quantize_ui_skin.py, +# 全 CC0;缺位 = load_tex null → 程序化回退原路径,冒烟安全) +const TEX_PANEL_STONE := "res://assets/art/ui/panel_stone.png" # 石板亮面板九宫格 48×48 +const TEX_BUTTON_STONE := "res://assets/art/ui/button_stone.png" # 按钮九宫格·常态(可按的砖) +const TEX_BUTTON_STONE_PRESSED := "res://assets/art/ui/button_stone_pressed.png" # 按钮九宫格·按压(下沉一档) +const TEX_BACKDROP_STONE := "res://assets/art/ui/backdrop_stone.png" # 石纹底噪 256×256 无缝 +const TEX_BORDER_STONE := "res://assets/art/ui/border_stone.png" # 仪式石框(空心角括框) +const TEX_ICON_KEYSTONE := "res://assets/art/ui/icon_keystone.png" # 钥石图标(奖励金) +const TEX_ICON_FORGE := "res://assets/art/ui/icon_forge.png" # 柜台一·属性锻造 +const TEX_ICON_WORKSHOP := "res://assets/art/ui/icon_workshop.png" # 柜台二·道具工坊 +const TEX_ICON_ATLAS := "res://assets/art/ui/icon_atlas.png" # 柜台三·奖励格图鉴 + # 素材纹理守卫装载(exists 检查避开缺位树上 ResourceLoader 报错刷屏——冒烟/无素材检出静默回退) static func load_tex(path: String) -> Texture2D: @@ -133,4 +145,76 @@ static func label(text: String, size: int, bold: bool) -> Label: static func button(text: String) -> Button: var b := Button.new() b.text = text + _apply_button_skin(b) return b + + +# ── 框材纹理工厂(样张 v2 冻结批 T2——§7.1 容器穿戏服、数据素面;缺位回退 flat/主题默认) ── +# 九宫格 StyleBoxTexture(texture_margin 8px——quantize 脚本同值);tint 供消费点微调色调 +static func _tex_sb(tex: Texture2D, margin: float, tint := Color.WHITE) -> StyleBoxTexture: + var sb := StyleBoxTexture.new() + sb.texture = tex + sb.texture_margin_left = margin + sb.texture_margin_top = margin + sb.texture_margin_right = margin + sb.texture_margin_bottom = margin + sb.modulate_color = tint + return sb + + +# 石板亮面板(B 区舞台/C 区主面板/图例——容器穿戏服;数据框仍 panel_flat 素面 §7.1) +static func panel_skinned(margin: Vector4 = Vector4.ZERO, tint := Color.WHITE) -> StyleBox: + var tex := load_tex(TEX_PANEL_STONE) + if tex == null: + return panel_flat(PANEL_DATA, PANEL_BORDER, 4, 1, margin) + var sb := _tex_sb(tex, 8.0, tint) + sb.content_margin_left = margin.x + sb.content_margin_top = margin.y + sb.content_margin_right = margin.z + sb.content_margin_bottom = margin.w + return sb + + +# 仪式石框(空心角括框——营地容器框;缺位回退暗面板) +static func frame_skinned(margin: Vector4 = Vector4.ZERO) -> StyleBox: + var tex := load_tex(TEX_BORDER_STONE) + if tex == null: + return panel_flat(BG_OVERLAY, PANEL_BORDER, 6, 1, margin) + var sb := _tex_sb(tex, 12.0) + sb.content_margin_left = margin.x + sb.content_margin_top = margin.y + sb.content_margin_right = margin.z + sb.content_margin_bottom = margin.w + return sb + + +# 图标槽(营地钥石/柜台件——缺位返回 null,消费点不挂 TextureRect 即回退纯文本) +static func icon_rect(tex_path: String, px: float) -> TextureRect: + var t := load_tex(tex_path) + if t == null: + return null + var tr := TextureRect.new() + tr.texture = t + tr.custom_minimum_size = Vector2(px, px) + tr.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + tr.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + tr.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + return tr + + +# 按钮皮(常态/按压两态+墨字——缺位 = 引擎默认主题,素材批前逐位一致) +static func _apply_button_skin(b: Button) -> void: + var tex_n := load_tex(TEX_BUTTON_STONE) + if tex_n == null: + return + var tex_p := load_tex(TEX_BUTTON_STONE_PRESSED) + b.add_theme_stylebox_override("normal", _tex_sb(tex_n, 8.0)) + b.add_theme_stylebox_override("hover", _tex_sb(tex_n, 8.0)) + if tex_p != null: + b.add_theme_stylebox_override("pressed", _tex_sb(tex_p, 8.0)) + b.add_theme_stylebox_override("disabled", _tex_sb(tex_p, 8.0)) + b.add_theme_color_override("font_color", INK) + b.add_theme_color_override("font_hover_color", INK) + b.add_theme_color_override("font_pressed_color", INK) + b.add_theme_color_override("font_focus_color", INK) + b.add_theme_color_override("font_disabled_color", Color(INK, 0.45)) diff --git a/tools/quantize_ui_skin.py b/tools/quantize_ui_skin.py new file mode 100644 index 00000000..65ea7202 --- /dev/null +++ b/tools/quantize_ui_skin.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +"""UI 框材组量化改色脚本(M-1 迭代 2 T2,2026-09-28)。 + +♻ 复用件 → 复用闸①色板量化:全部产物按明度重映射到 art-bible §4.1/§7.2 token 渐变带, +不保留原包杂色;闸②禁入带=token 色天然合规;闸③灰度可辨=图标四形互异;闸④授权=全 CC0。 +源包 = artwork/ui-reuse-2026-09-23/(素材不入 git——先查本仓库,缺位回退主检出 D:/code/g-plan2): + oga-kenney-pixel-ui-750(CC0,OGA) 9-Slice/Ancient 面板/按钮九宫格 48×48 + kenney-fantasy-ui-borders(CC0) panel-border 石框 48×48 + 5-石纹底噪/stone_6.png(CC0,OGA Handpainted stone) 无缝底噪 1200×1200 + kenney-board-game-icons(CC0) 64px 图标四件 +产物 → game/assets/art/ui/(终稿素材目录,不入 git;消费 = UiKit.load_tex 守卫,缺位程序化回退)。 +用法:python tools/quantize_ui_skin.py +""" +from PIL import Image +import os + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +MAIN_ROOT = "D:/code/g-plan2" +SRC = ("artwork", "ui-reuse-2026-09-23") +OUT = ("game", "assets", "art", "ui") + + +def src_path(*parts): + for base in (ROOT, MAIN_ROOT): + p = os.path.join(base, *SRC, *parts) + if os.path.exists(p): + return p + raise FileNotFoundError("/".join(SRC + parts)) + + +def out_path(name): + p = os.path.join(ROOT, *OUT, name) + os.makedirs(os.path.dirname(p), exist_ok=True) + return p + + +# §4.1/§7.2 token 渐变带(量化目标值——样张 v2 冻结色板) +RAMP_PANEL = [(120, 106, 88), (226, 210, 182), (243, 234, 214)] # PANEL_BORDER→PANEL_DATA→PLATE 微亮 +RAMP_BUTTON = [(179, 154, 120), (201, 175, 131), (245, 234, 211)] # FLOOR→BRICK(掩尘"可按的砖")→PLATE +RAMP_PRESSED = [(120, 106, 88), (179, 154, 120)] # PANEL_BORDER→FLOOR +RAMP_BACKDROP = [(30, 25, 20), (120, 106, 88)] # BG_ROOT→PANEL_BORDER(低对比自然后退 §6.1) +RAMP_BORDER = [(120, 106, 88), (201, 175, 131)] # PANEL_BORDER→BRICK +ICON_GOLD = (240, 168, 40) # 奖励金(钥石) +ICON_WARM = (240, 226, 200) # 暖白(柜台件,暗卡上可读) + + +def ramp_color(ramp, t): + if t <= 0: + return ramp[0] + if t >= 1: + return ramp[-1] + seg = t * (len(ramp) - 1) + i = min(int(seg), len(ramp) - 2) + k = seg - i + a, b = ramp[i], ramp[i + 1] + return tuple(int(a[j] + (b[j] - a[j]) * k) for j in range(3)) + + +def remap_luma(img, ramp, gamma=1.0): + """明度→token 渐变带重映射(保留 alpha 与纹理结构——闸①量化)。""" + im = img.convert("RGBA") + px = im.load() + vals = [sum(px[x, y][:3]) / 3.0 for y in range(im.size[1]) for x in range(im.size[0]) + if px[x, y][3] > 8] + lo, hi = min(vals), max(vals) + span = max(1.0, hi - lo) + for y in range(im.size[1]): + for x in range(im.size[0]): + r, g, b, a = px[x, y] + if a <= 8: + continue + t = ((sum((r, g, b)) / 3.0) - lo) / span + px[x, y] = ramp_color(ramp, t ** gamma) + (a,) + return im + + +def recolor_flat(img, rgb): + """剪影图标单色重着色(保留 alpha 抗锯齿——形状承载识别,闸③)。""" + im = img.convert("RGBA") + px = im.load() + for y in range(im.size[1]): + for x in range(im.size[0]): + r, g, b, a = px[x, y] + if a > 0: + px[x, y] = rgb + (a,) + return im + + +def main(): + jobs = [] + # 面板/按钮九宫格(pixel-ui-750 Ancient;texture_margin 取 8px——消费侧同值) + jobs.append(("panel_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "brown.png")), RAMP_PANEL))) + jobs.append(("button_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan.png")), RAMP_BUTTON))) + jobs.append(("button_stone_pressed.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "oga-kenney-pixel-ui-750(CC0)", "9-Slice", "Ancient", "tan_pressed.png")), RAMP_PRESSED))) + # 石框(fantasy-ui-borders) + jobs.append(("border_stone.png", remap_luma(Image.open(src_path("1-UI面板与按钮", "kenney-fantasy-ui-borders(CC0)", "PNG", "Default", "Border", "panel-border-000.png")), RAMP_BORDER))) + # 石纹底噪(seamless 1200→256,暗带低对比——B 区/开始层背景) + stone = Image.open(src_path("5-石纹底噪", "stone_6.png")).convert("RGB").resize((256, 256), Image.LANCZOS) + jobs.append(("backdrop_stone.png", remap_luma(stone, RAMP_BACKDROP, gamma=1.6))) + # 营地图标四件(board-game-icons 64px;钥石=奖励金,柜台件=暖白) + icon_dir = ("3-节点图与营地图标", "kenney-board-game-icons(CC0)", "PNG", "Default (64px)") + jobs.append(("icon_keystone.png", recolor_flat(Image.open(src_path(*(icon_dir + ("suit_diamonds.png",)))), ICON_GOLD))) + jobs.append(("icon_forge.png", recolor_flat(Image.open(src_path(*(icon_dir + ("sword.png",)))), ICON_WARM))) + jobs.append(("icon_workshop.png", recolor_flat(Image.open(src_path(*(icon_dir + ("flask_full.png",)))), ICON_WARM))) + jobs.append(("icon_atlas.png", recolor_flat(Image.open(src_path(*(icon_dir + ("book_open.png",)))), ICON_WARM))) + + for name, im in jobs: + im.save(out_path(name)) + print("saved:", "/".join(OUT + (name,))) + print("=== 量化改色完成(%d 件,全 CC0;闸①②③④ 见脚本头注) ===" % len(jobs)) + return 0 + + +if __name__ == "__main__": + import sys + sys.exit(main())