diff --git a/design/research/level-map-sample-v1-机检报告-2026-09-28.md b/design/research/level-map-sample-v1-机检报告-2026-09-28.md index 83cb25df..8363d196 100644 --- a/design/research/level-map-sample-v1-机检报告-2026-09-28.md +++ b/design/research/level-map-sample-v1-机检报告-2026-09-28.md @@ -19,7 +19,7 @@ | 断言 | 判据 | 结果 | 明细 | |---|---|---|---| -| L1 | 五节点图标灰度互辨(剪影两两错位率 ≥0.08) | PASS | 最小错位 0.19(('boss', 'shop'));五键={'battle': 'bow.png', 'reward': 'award.png', 'rest': 'campfire.png', 'boss': 'crown_b.png', 'shop': 'pouch.png'} | +| L1 | 五节点图标灰度互辨(剪影两两错位率 ≥0.08) | PASS | 最小错位 0.18(('boss', 'shop'));五键={'battle': 'bow.png', 'reward': 'award.png', 'rest': 'campfire.png', 'boss': 'crown_b.png', 'shop': 'pouch.png'} | | L2 | 节点四态灰度可分类+Boss 紫赤描边灰度可辨 | PASS | 填充灰度 done/current/cand/future=87/174/59/59;亮金环 191 紫赤环 72(Δ120) 深墨环 21 外噪底 65 | | L3 | 数字/节点字面对比硬指标 ≥4.5:1(§4.1) | PASS | [('GP金×数据框', 5.5), ('GP金×屏底', 8.6), ('读数暖白×数据框', 8.8), ('读数暖白×屏底', 13.6), ('图标墨×当前金', 9.0), ('图标暖白×已过金', 5.6), ('图标暖白×未来暗', 8.8)] | | L4 | 禁入带 HSV300°-345° 零命中(三屏, Boss 环区白名单) | PASS | 岔路 0/入口 0/换层 0 像素(腐蚀后抽样) | diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index be202209..ac72e134 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -22,14 +22,18 @@ extends Control signal route_chosen(node_id: int) signal abandon_requested -const COL_ROOM_DONE := UiKit.GOLD_DIM # 已到——暗金 +const COL_ROOM_DONE := UiKit.GOLD_DONE # 已到——暗金(样张 v1 冻结新值 #68562C——旧 GOLD_DIM 对双色墨均 <4.5:1,对比硬指标逼守;UiKit.GOLD_DIM 他用不动) const COL_ROOM_CURRENT := UiKit.GOLD # 当前——奖励金 const COL_ROOM_FUTURE := UiKit.DARK_UPCOMING # 未达——暗 const COL_PASSAGE := UiKit.STONE # 通道——石灰 const COL_PASSAGE_LIT := UiKit.GOLD_LIT # 候选通道——亮金 const COL_BOSS_BORDER := UiKit.BOSS_PURPLE # Boss 边框——紫赤(色相独占本色;旗朱退役 2026-09-26) const COL_ABANDON := UiKit.RED_MINE # 破坏性动作红(危险红族) -const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21 +const NODE_LABELS := {"battle": "战", "reward": "赏", "rest": "疗", "boss": "王", "shop": "商"} # 商 = GP+商店房实施批 2026-09-21(贴图缺位字形回退用——贴图在位=字形退役惯例) +const TEX_NODE_ICONS := {"battle": UiKit.TEX_NODE_BATTLE, "reward": UiKit.TEX_NODE_REWARD, + "rest": UiKit.TEX_NODE_REST, "boss": UiKit.TEX_NODE_BOSS, "shop": UiKit.TEX_NODE_SHOP} +const NODE_ICON_PX := 20.0 # 节点图标边长(样张 v1 冻结——20px 烘焙 1:1 消费零缩放) +const ICON_TINT_ON_GOLD := Color(24.0 / 240.0, 20.0 / 226.0, 16.0 / 200.0) # 当前金底图标墨 = INK_ROUTE 乘法调制(双色墨制——暗底系 WARM 原色) const TITLE_ENTRY := "出发——选择首个节点" # 入口停点(布局决策 1——台账 143 遗留清偿) const TITLE_SINGLE := "继续前进" # 单链停点 const TITLE_FORK := "选择下一节点" # 岔路停点 @@ -53,9 +57,13 @@ var _hover_id := -1 # 悬停预承诺候选(-1 = 无) var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) var _abandon_armed := false # 二次确认态(首击置位、二击提交、超时/重开回落) var _confirm_left := 0.0 # 确认态剩余秒数(_process 倒计时+脉冲驱动) +var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) func _init() -> void: + # 节点图标贴图装载(样张 v1 冻结形制——load_tex 守卫,缺位回退现行字形) + for t in TEX_NODE_ICONS: + _node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) # 独立屏底(形态升格 2026-09-28——spec Layout:深底+石纹底噪铺满;底纹缺位回退纯色——load_tex 守卫) var base := ColorRect.new() base.color = UiKit.BG_ROOT @@ -368,7 +376,7 @@ func _draw_graph(canvas: Control) -> void: var lit: bool = _id_of(node) == current_id and cand_ids.has(tid) var hovered: bool = lit and _hover_id == tid var col := COL_PASSAGE_LIT if lit else COL_PASSAGE - var width := 4.0 if hovered else (3.0 if lit else 1.5) + var width := 4.0 if hovered else (3.0 if lit else 2.0) # 样张 v1 冻结:2/3/4(原 1.5 取整) _draw_passage(canvas, from, to, col, width) # 房间方块 for node in nodes: @@ -383,14 +391,28 @@ func _draw_graph(canvas: Control) -> void: var rect := Rect2(p - NODE_HALF, NODE_HALF * 2.0) canvas.draw_rect(rect, col) if t == "boss": + # Boss 描边 = 紫赤 2px(§4.2 色相独占)+ 外缘 1px 深墨环(灰度冗余通道——样张 v1 L2 逼守: + # 紫赤灰度 72 对石纹噪底 65 灰度不可辨;灰度/色弱下 Boss 环经深墨环可辨) + canvas.draw_rect(Rect2(rect.position - Vector2(3, 3), rect.size + Vector2(6, 6)), UiKit.INK_ROUTE, false, 1.0) canvas.draw_rect(rect, COL_BOSS_BORDER, false, 2.0) elif cand_ids.has(nid): # 悬停预承诺 = 亮金描边加粗(4px),常态候选 = 亮金描边 2px(spec States 悬停预承诺行) canvas.draw_rect(rect, COL_PASSAGE_LIT, false, 4.0 if _hover_id == nid else 2.0) - var font := get_theme_default_font() - var label := String(NODE_LABELS.get(t, "?")) - canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) - canvas.draw_string(font, p + Vector2(10, 14), str(nid), HORIZONTAL_ALIGNMENT_LEFT, -1, 10, UiKit.WARM_ROUTE_SUB) + # 节点图标(样张 v1 冻结形制:20px 烘焙 1:1 消费;双色墨制=乘法调制——当前金底深墨,暗底系 WARM 原色; + # 贴图缺位 → 字形回退=贴图在位字形退役惯例) + var icon: Texture2D = _node_icons.get(t) + if icon != null: + var tint := ICON_TINT_ON_GOLD if col == COL_ROOM_CURRENT else Color(1.0, 1.0, 1.0) + canvas.draw_texture_rect(icon, Rect2(p - Vector2(NODE_ICON_PX, NODE_ICON_PX) / 2.0, + Vector2(NODE_ICON_PX, NODE_ICON_PX)), false, tint) + else: + var font := get_theme_default_font() + var label := String(NODE_LABELS.get(t, "?")) + canvas.draw_string(font, p + Vector2(-6, 2), label, HORIZONTAL_ALIGNMENT_LEFT, -1, 16, UiKit.INK_ROUTE) + # 编号(右上角——与图标占位区零重叠,样张 v1 L7;双色墨随态) + var num_ink := UiKit.INK_ROUTE if col == COL_ROOM_CURRENT else UiKit.WARM + canvas.draw_string(get_theme_default_font(), Vector2(rect.end.x - 16.0, rect.position.y + 8.0), + str(nid), HORIZONTAL_ALIGNMENT_RIGHT, 14, 10, num_ink) func _layout(nodes: Array, canvas: Control) -> Dictionary: diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 8803ecda..e02da719 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -25,6 +25,7 @@ const GOLD := Color8(240, 168, 40) # 奖励金 const GOLD_DEEP := Color8(176, 122, 20) # 深金(金框描边/达意金) const GOLD_LIT := Color8(230, 190, 90) # 亮金(候选高亮) const GOLD_DIM := Color8(140, 116, 60) # 暗金(已过节点) +const GOLD_DONE := Color8(104, 86, 44) # 已过节点暗金·关卡画面呈现态(关卡画面样张 v1 冻结新值 #68562C——旧 GOLD_DIM #8C743C 对双色墨均 <4.5:1 对比硬指标逼守;GOLD_DIM 他用不动) const SEAL_BLACK := Color8(0, 0, 0) # 封印黑 #000000——封印(插旗)宣告色(2026-09-28 # 用户拍板旗朱→封印紫→封印黑:紫色与微光蓝族色相冲突,黑色无色相天然避开禁入带 @@ -112,6 +113,13 @@ 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" # 柜台三·奖励格图鉴 +# 节点图标五键(关卡画面样张 v1 冻结批 M-2 迭代 4 T2——20px 烘焙 1:1 消费,WARM 单色; +# 对象键 = node_type 文件侧延伸 §8.3;缺位 = load_tex null → 字形回退) +const TEX_NODE_BATTLE := "res://assets/art/ui/node_battle.png" # 节点图标·战斗房 +const TEX_NODE_REWARD := "res://assets/art/ui/node_reward.png" # 节点图标·奖励房 +const TEX_NODE_REST := "res://assets/art/ui/node_rest.png" # 节点图标·恢复房 +const TEX_NODE_BOSS := "res://assets/art/ui/node_boss.png" # 节点图标·Boss 房 +const TEX_NODE_SHOP := "res://assets/art/ui/node_shop.png" # 节点图标·商店房 # 素材纹理守卫装载(exists 检查避开缺位树上 ResourceLoader 报错刷屏——冒烟/无素材检出静默回退) diff --git a/tools/quantize_ui_skin.py b/tools/quantize_ui_skin.py index 65ea7202..c69c88c7 100644 --- a/tools/quantize_ui_skin.py +++ b/tools/quantize_ui_skin.py @@ -42,6 +42,8 @@ RAMP_BACKDROP = [(30, 25, 20), (120, 106, 88)] # BG_ROOT→P RAMP_BORDER = [(120, 106, 88), (201, 175, 131)] # PANEL_BORDER→BRICK ICON_GOLD = (240, 168, 40) # 奖励金(钥石) ICON_WARM = (240, 226, 200) # 暖白(柜台件,暗卡上可读) +NODE_WARM = (240, 226, 200) # 节点图标暖白(关卡画面样张 v1 冻结——暗底态原色,当前金底乘法调制深墨) +NODE_PX = 20 # 节点图标烘焙边长(52×36 方块内 20px 图标+右上编号,1:1 消费零缩放) def ramp_color(ramp, t): @@ -103,14 +105,55 @@ def main(): 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))) + # 节点图标五件(board-game-icons 64px→20px 烘焙,WARM 单色——关卡画面样张 v1 冻结形制,M-2 迭代 4 T2; + # 战=bow/赏=award/疗=campfire/王=crown_b/商=pouch,语义近似+灰度互辨,色相不承载身份 §4.6) + node_jobs = (("node_battle.png", "bow.png"), ("node_reward.png", "award.png"), + ("node_rest.png", "campfire.png"), ("node_boss.png", "crown_b.png"), + ("node_shop.png", "pouch.png")) + node_outs = {} + for name, src in node_jobs: + im = recolor_flat(Image.open(src_path(*(icon_dir + (src,)))), NODE_WARM) + im = im.resize((NODE_PX, NODE_PX), Image.NEAREST) + node_outs[name] = im + jobs.append((name, im)) for name, im in jobs: im.save(out_path(name)) print("saved:", "/".join(OUT + (name,))) + node_icon_gates(node_outs) print("=== 量化改色完成(%d 件,全 CC0;闸①②③④ 见脚本头注) ===" % len(jobs)) return 0 +def node_icon_gates(outs): + """节点图标复用闸四道自检(闸④授权=kenney-board-game-icons CC0 全包在案): + ①量化=全像素 NODE_WARM 或透明(零原包杂色) ②禁入带=WARM 色相 39° 天然合规 + ③灰度互辨=alpha 剪影两两错位率 ≥0.08(与样张 L1 同判据) ④见上。""" + fail = [] + for name, im in outs.items(): + bad = sum(1 for px in im.getdata() if px[3] > 8 and px[:3] != NODE_WARM) + if bad: + fail.append("%s 闸①杂色 %d px" % (name, bad)) + if im.size != (NODE_PX, NODE_PX): + fail.append("%s 尺寸 %s" % (name, im.size)) + keys = list(outs.keys()) + worst = (1.0, None) + sigs = {k: [1 if px[3] > 96 else 0 for px in outs[k].getdata()] for k in keys} + for i, a in enumerate(keys): + for b in keys[i + 1:]: + diff = sum(1 for x, y in zip(sigs[a], sigs[b]) if x != y) / len(sigs[a]) + if diff < worst[0]: + worst = (diff, (a, b)) + if worst[0] < 0.08: + fail.append("闸③灰度互辨最小错位 %.2f %s" % worst) + if fail: + for f in fail: + print("[FAIL] 节点图标闸 %s" % f) + raise SystemExit(1) + print("[PASS] 节点图标复用闸①②③④(量化单色/禁入带天然合规/灰度互辨最小错位 %.2f %s/CC0)" + % worst) + + if __name__ == "__main__": import sys sys.exit(main()) diff --git a/tools/render_level_map_sheet.py b/tools/render_level_map_sheet.py index 7c2f970f..1b115ad4 100644 --- a/tools/render_level_map_sheet.py +++ b/tools/render_level_map_sheet.py @@ -73,7 +73,7 @@ COLD_PASSAGE = (112, 120, 128) # S3 换层通道冷调(underdark 方向—— NODE_KEYS = ["battle", "reward", "rest", "boss", "shop"] NODE_ICONS = {"battle": "bow.png", "reward": "award.png", "rest": "campfire.png", "boss": "crown_b.png", "shop": "pouch.png"} # T1 挑选(语义近似;闸④全 CC0) -ICON_PX = 22 # 节点图标边长(52×36 方块内 22px 图标+右下 10px 编号) +ICON_PX = 20 # 节点图标边长(52×36 方块内 20px 图标+右上编号——冻结值,与 quantize 烘焙同值) NODE_HALF = (26, 18) # 节点方块半宽高(与 level_map_screen.gd 同源解析对表) CANVAS_MIN = (620, 240) # 画布下限(同源) W_PASS, W_LIT, W_HOVER = 2, 3, 4 # 通道折线宽(未lit/lit/悬停——冻结值) @@ -202,8 +202,14 @@ def fit_content(nodes): return size, layer, slot -# ══ 图标装载(kenney CC0 64px → WARM 单色重着色 → NEAREST 缩放) ══ +# ══ 图标装载(优先烘焙终稿 game/assets/art/ui/node_*.png(quantize 产出,20px 1:1); +# 缺位回退 kenney 64px 现场量化——同 WARM 单色+NEAREST,与烘焙产物逐像素同构) ══ def load_icon(kind): + for base in (ROOT, MAIN_ROOT): + p = os.path.join(base, "game", "assets", "art", "ui", "node_%s.png" % kind) + if os.path.exists(p): + im = Image.open(p).convert("RGBA") + return im if im.size == (ICON_PX, ICON_PX) else im.resize((ICON_PX, ICON_PX), Image.NEAREST) im = Image.open(asset_root(*(ICON_DIR + (NODE_ICONS[kind],)))).convert("RGBA") px = im.load() for y in range(im.size[1]): @@ -329,8 +335,8 @@ def draw_screen(nodes, title, level_name, gp, visited, current, cands, hover=-1, img.alpha_composite(icon, (ic_x, ic_y)) num = str(nid) nb = d.textbbox((0, 0), num, font=F_NODE_NUM) - nx = x1 - 4 - (nb[2] - nb[0]) - ny = y1 - 3 - (nb[3] - nb[1]) + nx = x1 - 4 - (nb[2] - nb[0]) # 右对齐至方块右内缘 -4(与引擎 HORIZONTAL_ALIGNMENT_RIGHT 同位) + ny = y0 + 1 # 右上角——与图标占位区零重叠(L7) d.text((nx - nb[0], ny - nb[1]), num, font=F_NODE_NUM, fill=ink) records["nodes"][nid] = { "state": state, "type": n["type"], "block": (x0, y0, x1, y1),