# VERTICAL SLICE - NOT FOR PRODUCTION # UI 文字对比度机检(probe_gate 默认必跑) # 断言面 = 文字墨对直背景 ≥4.5:1(art-bible §7.2 无区域例外;文字禁直压纹理底——垫牌后按牌色计)。 # 测点表:血数垫牌 / 意图徽标四墨(PANEL_DATA 垫牌)/ C区槽位两态墨 / RAP 标签 + 全站锚点组。 # 测点对一律取 UiKit 常量实时求值——代码侧换色即本闸红,禁只改注释不动值;α<1 墨先对背景混成再算。 # 运行:Godot_console.exe --headless --path . res://probe/ui_contrast_probe.tscn extends Node const MIN_RATIO := 4.5 # R1 过渡白名单:呈现层(ui_kit.gd 之外)允许的裸 Color8( 在册处——file→恰计数。 # 产品码裸取色已全部收进 ui_kit——本表应为空表; # 新增裸取色 = 本闸红(先立常量再消费)。 const BARE_COLOR8_WL := {} # 测点表:[名称, 墨色, 直背景, 墨α](语义见行尾;新增文字位入表,白名单豁免须注理由) func _pairs() -> Array: return [ ["站桩血数·垫牌", Color(1, 1, 1), UiKit.HP_BAR_BACK, 0.95], # N1-1/2 白 α.95 压 HP_BAR_BACK 素牌 ["意图atk·垫牌", UiKit.RED_MINE, UiKit.PANEL_DATA, 1.0], # N1-3 ⚔ 攻击预告(零换墨) ["意图buf·垫牌", UiKit.HERO_BLUE_DEEP, UiKit.PANEL_DATA, 1.0], # N1-4 ✦ 增益(同族深档) ["意图debuff·垫牌", UiKit.DEBUFF_PURPLE, UiKit.PANEL_DATA, 1.0], # ☠ 妨害(零换墨随牌达标) ["意图charge·垫牌", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0], # 蓄力重击(深金墨) ["C区槽·已用", UiKit.STONE_DEEP, UiKit.PANEL_DATA, 1.0], # N1-5 占位暗字(灰石深化) ["C区槽·不可用", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # N1-6 置灰(标签深墨) ["RAP标签", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # N1-7(T0 实测已合规,显式化) ["锚·正文墨/数据板", UiKit.INK, UiKit.PANEL_DATA, 1.0], # 全站锚点:正文墨对数据板 ["锚·暖白/屏根", UiKit.WARM, UiKit.BG_ROOT, 1.0], # 全站锚点:暗底正文 ["锚·暖白弱/舞台底", UiKit.WARM_FAINT, UiKit.STAGE_BG, 1.0], # 全站锚点:B区辅助墨 ["钥石徽·深墨/烛光黄", UiKit.INK_ON_GOLD, UiKit.CANDLE, 1.0], # badge 工厂件文字(营地徽+状态面板余额行同形制) ["生涯页·行名墨/暖亮页底", UiKit.WARM, UiKit.BG_BRIGHT, 1.0], # 生涯页行名列/提示文(WARM_SOFT 同族亮墨) ["生涯页·行值墨/暖亮页底", UiKit.CANDLE, UiKit.BG_BRIGHT, 1.0], # 生涯页数值列(烛光黄 bold) ["生涯页·组帽墨/暖亮页底", UiKit.INK_L_GOLD, UiKit.BG_BRIGHT, 1.0],# 生涯页分组小节头(金亮 bold) # ---- 旧屏文字位追溯收测;石皮纹面板 # (panel_skinned)缺素材回退牌色 PANEL_DATA,机检按回退牌色计(素材不入 git 冒烟安全惯例)。 # 存量违例对首跑红=登记呈报不修(处置权归用户)。 # 已回绿对:随代码侧换墨同步改指新常量(比率见各行尾注)。 ["结算·判定墨/结算板", UiKit.INK_TITLE, UiKit.PANEL_DATA, 1.0], # battle_end 判定行(INK_TITLE 亮底标题墨) ["结算·GP账行墨/结算板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0], # battle_end 战利品 GP 行名/值(深金墨 5.21:1) ["结算·账行墨/结算板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # battle_end 钥石/加成账行(标签深墨) ["商店·行卡名墨/暗底行卡", UiKit.WARM_SOFT, UiKit.BG_OVERLAY, 1.0], # legacy 行卡商品名(显式传暖白次级 9.53:1) ["商店·行卡现价墨/暗底行卡", UiKit.WARM_SOFT, UiKit.BG_OVERLAY, 1.0],# legacy 行卡「现价 N GP」 ["商店·行卡已购墨/暗底行卡", UiKit.WARM_FAINT, UiKit.BG_OVERLAY, 1.0],# legacy 行卡「已购 N 档」 ["商店·爬价规则墨/暖亮页底", UiKit.WARM_SOFT, UiKit.BG_BRIGHT, 1.0],# 货架面爬价规则行(page_shell 亮底) ["商店·货架卡名墨/货架板", UiKit.INK_TITLE, UiKit.PANEL_DATA, 1.0], # bcard 货架卡商品名(换 TG 带后未机检位) ["商店·货架价签墨/货架板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0], # bcard 价签「N ◈」(深金墨 5.21:1) ["营地·等级章字/头带深棕", UiKit.INK_ON_DEEP, UiKit.CAMP_HEADER_BAND, 1.0],# 锻造行 Lv 章(木底小章亮墨) ["营地·柜台头带字/头带深棕", UiKit.WARM, UiKit.CAMP_HEADER_BAND, 1.0], # 三柜台头带标题(暖白 bold) ["营地·铭牌副题墨/铭牌板", UiKit.STONE_DEEP, UiKit.PANEL_DATA, 1.0],# 营地铭牌副题「跨局成长账本…」(灰石深化 5.98:1) ["营地·行名墨/行板", UiKit.INK, UiKit.CAMP_ROW_PLATE, 1.0], # 锻造/工坊行名(墨族压暖米行板) ["营地·行价格墨/行板", UiKit.STONE_DEEP, UiKit.CAMP_ROW_PLATE, 1.0],# 锻造行「N 钥石」价签/工坊进度行(灰石深化 6.70:1) ["营地·树分组头墨/柜台板", UiKit.GOLD_INK, UiKit.PANEL_DATA, 1.0],# 天赋树脉分组头「·锻造脉·」(深金墨 5.21:1) ["营地·树行名墨/柜台板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 天赋树行「名 Lv0/1」(标签深墨) ["营地·工坊状态墨/行板", UiKit.GOLD_INK, UiKit.CAMP_ROW_PLATE, 1.0],# 工坊状态字「已解锁」(深金墨 5.84:1·最不利现役态) ["营地·最近冒险墨/暖亮底", UiKit.WARM_SOFT, UiKit.BG_BRIGHT, 1.0], # 底栏最近冒险条带(暖白次级) ["设置·警示文/设置板", UiKit.INK_HINT, UiKit.PANEL_DATA, 1.0], # 设置警示文/后果说明(暖棕提示墨) ["设置·选项标签/设置板", UiKit.INK_LABEL, UiKit.PANEL_DATA, 1.0], # 设置行标签/状态字(标签深墨) ["三选一·停点头部墨/暗页底", UiKit.WARM_SOFT, UiKit.BG_ROOT, 1.0], # 三选一停点头部/回血反馈行(page_shell 根底档) ["三选一·卡名墨/货架板", UiKit.INK_TITLE, UiKit.PANEL_DATA, 1.0], # bcard 选项卡名(与商店货架同构卡) # ---- 迷雾测点三关扩对(迭代36 wc5——甲·归一后基准:行值=三关归一后(真实渲染面) # 地板均值合成底(写卡会话 PIL 复算+两轮 grilling 独立复算吻合;T0 探针派生首跑对拍 # L2/L3 修值(PIL↔Godot 导入解码系统差约 5/255,卡内 L2 条款预授权以派生为准); # 素材更新随动——一致性由 _ready 派生校验断言钉住,行值回原图基准或素材换代未随动均红)---- ["迷雾·记忆墨/合成底L1地牢", UiKit.FOG_MEMORY_INK, Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 111.0 / 255.0 * 0.34, # L1 归一后地板均值 #6F4D33(合成底 #4C4038 5.71:1·最差关) UiKit.FOG_MEMORY_PLATE.g * 0.66 + 77.0 / 255.0 * 0.34, UiKit.FOG_MEMORY_PLATE.b * 0.66 + 51.0 / 255.0 * 0.34), 1.0], ["迷雾·记忆墨/合成底L2幽暗", UiKit.FOG_MEMORY_INK, Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 74.0 / 255.0 * 0.34, # L2 归一后地板均值 #4A4F60(合成底 #3F4147 5.83:1) UiKit.FOG_MEMORY_PLATE.g * 0.66 + 79.0 / 255.0 * 0.34, UiKit.FOG_MEMORY_PLATE.b * 0.66 + 96.0 / 255.0 * 0.34), 1.0], ["迷雾·记忆墨/合成底L3深幽", UiKit.FOG_MEMORY_INK, Color(UiKit.FOG_MEMORY_PLATE.r * 0.66 + 71.0 / 255.0 * 0.34, # L3 归一后地板均值 #474B63(合成底 #3E4048 5.93:1) UiKit.FOG_MEMORY_PLATE.g * 0.66 + 75.0 / 255.0 * 0.34, UiKit.FOG_MEMORY_PLATE.b * 0.66 + 99.0 / 255.0 * 0.34), 1.0], ] func _lum(c: Color) -> float: var f := func(x: float) -> float: return x / 12.92 if x <= 0.03928 else pow((x + 0.055) / 1.055, 2.4) return 0.2126 * f.call(c.r) + 0.7152 * f.call(c.g) + 0.0722 * f.call(c.b) func _ratio(a: Color, b: Color) -> float: var la := _lum(a) var lb := _lum(b) var hi := la if la >= lb else lb var lo := lb if la >= lb else la return (hi + 0.05) / (lo + 0.05) func _ready() -> void: var problems: Array = [] var pairs := _pairs() for p in pairs: var fg: Color = p[1] var bg: Color = p[2] var alpha: float = p[3] if alpha < 1.0: fg = Color(fg.r * alpha + bg.r * (1.0 - alpha), fg.g * alpha + bg.g * (1.0 - alpha), fg.b * alpha + bg.b * (1.0 - alpha)) var r := _ratio(fg, bg) if r < MIN_RATIO: problems.append("[contrast] %s = %.2f:1 < %.1f(测点对须垫牌或换墨)" % [p[0], r, MIN_RATIO]) problems.append_array(_scan_static()) problems.append_array(_fog_derived_check()) if problems.is_empty(): print("[ui_contrast_probe] PASS — 对比度 %d 对测点全绿(≥%.1f:1 无区域例外)+ 裸色白名单/preload 红线静态段绿 + 迷雾三关派生校验绿" % [pairs.size(), MIN_RATIO]) get_tree().quit(0) else: for msg in problems: print(msg) print("[ui_contrast_probe] FAIL — %d 项越带(含静态段)" % problems.size()) get_tree().quit(1) # ---- 迷雾测点行值一致性派生校验(迭代36 wc5——行值 vs 运行时按生产归一函数实算的 # 三关归一后地板均值,容差 ±2/255=8-bit 量化级;行值回原图基准或素材换代未随动均红)---- func _fog_derived_check() -> Array: var problems: Array = [] var rows := [ ["L1地牢", "dungeon", Color8(111, 77, 51)], ["L2幽暗", "underdark", Color8(74, 79, 96)], ["L3深幽", "deep_underdark", Color8(71, 75, 99)], ] for row in rows: var derived := _norm_floor_avg(row[1]) var expect: Color = row[2] if absi(derived.r8 - expect.r8) > 2 or absi(derived.g8 - expect.g8) > 2 or absi(derived.b8 - expect.b8) > 2: problems.append("[contrast] 迷雾测点行值失真:%s 行值 (%d,%d,%d) vs 运行时归一派生 (%d,%d,%d)(±2/255——素材更新须随动改行值)" % [row[0], expect.r8, expect.g8, expect.b8, derived.r8, derived.g8, derived.b8]) return problems # 迷雾测点派生助手(迭代36 wc5 契约 3——全仓唯一「按板型算归一后均值」判据面):纹理路径经 # TilePalette.TILE_FLOOR_FMT 常量(禁复制字面量)、归一经 TilePalette.normalize_floor_luma # 生产静态函数直呼(真渲染同源——禁探针内复制 k 公式);逐像素归一后均值再五变体平均。 static func _norm_floor_avg(board_type: String) -> Color: var sum_r := 0.0 var sum_g := 0.0 var sum_b := 0.0 var px := 0 for i in range(1, TilePalette.TILE_FLOOR_VARIANTS + 1): var tex := UiKit.load_tex(TilePalette.TILE_FLOOR_FMT % [board_type, i]) if tex == null: continue var img: Image = TilePalette.normalize_floor_luma(tex, 0.10, null).get_image() if img.is_compressed(): img.decompress() img.convert(Image.FORMAT_RGBA8) for y in range(img.get_height()): for x in range(img.get_width()): var c := img.get_pixel(x, y) sum_r += c.r sum_g += c.g sum_b += c.b px += 1 if px == 0: return Color(-1, -1, -1) # 素材缺位=派生校验必红(禁跳过——契约 3 齐盘条款) return Color(sum_r / px, sum_g / px, sum_b / px) # ---- 机检段②③(需求卡片 §7):呈现层裸 Color8( 计数 vs 白名单 + 素材 preload 计数 = 0 ---- func _scan_static() -> Array: var problems: Array = [] var root := "res://presentation" var dirs := [root] while not dirs.is_empty(): var d: String = dirs.pop_front() for f in DirAccess.get_files_at(d): if not f.ends_with(".gd"): continue var path := d + "/" + f var src := FileAccess.get_file_as_string(path) var c8 := src.count("Color8(") var pre := 0 for line in src.split("\n"): if line.contains("preload(") and (line.contains("res://assets/") or line.contains(".png")): pre += 1 if path.ends_with("/ui_kit.gd"): if pre != 0: problems.append("[static] ui_kit.gd 素材 preload=%d(红线=0)" % pre) continue # ui_kit=色板工厂本体,Color8 定义面合法 var wl: int = BARE_COLOR8_WL.get(path, 0) if c8 != wl: problems.append("[static] %s 裸 Color8=%d 白名单=%d(R1 先立常量再消费)" % [path, c8, wl]) if pre != 0: problems.append("[static] %s 素材 preload=%d(R2 红线=0)" % [path, pre]) for sub in DirAccess.get_directories_at(d): dirs.append(d + "/" + sub) return problems