# tests/unit/presentation/theme_projection_test.gd — 呈现投影与主题单元测试宿主 # 断言面 = 十套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入): # 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入——board_view 微光数据面) # 战斗日志视图行格式化(行协议 9 类呈现层同步闸——真人试玩卡断 bug 回归) # UI 全局主题(main_theme.tres 挂载/类型覆盖/滑轨两段对比/退役实现零残留/中文字体防御) # UI 按钮五态矩阵(四工厂五态样式盒逐实例 override) # UI 全局纹理过滤(工程 Nearest+平滑渐变件 LINEAR 白名单+光柱 alpha 结构) # UI 残余 <4.5:1 文字位换墨(站桩徽标两族亮档+RAP 行深石墨对比度锚定) # UI 元素细节余②③(BCard 图标槽明度差降档+商店居中+营地四栏等高源码锚) # UI 诊断转储钮退役(玩家画面清零+浮窗可达——源码契约) # 状态面板只读投影一致性(展示值逐项 == MetaProgression 访问器读数) # 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager/MetaProgression 访问器读数+v5 旧档显示) # 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload, # ConfigDB/RngService 显式实例化构造注入(统一流 master 种子 42 沿源);表加载失败不中止 # (沿被迁断言面的源语义——诊断在此展开,红由具体断言落,承 G2–G5 同款);全局 class_name # (UiKit)裸用(--script 模式全局类在册,G3 先例)。 # 分宿主注记:源 `_smoke_playtest_logger`(挂真实结算流跨系统)经主会话裁决七改判集成测试, # 不随本宿主——见迁移账本转办清单;`_smoke_global_theme` 依赖 ThemeDB 项目主题在座, # 裁决四要求实跑验证(本批零测试运行,验证归装配验证轮)。 # 运行:godot --headless --path game --script res://tests/unit/presentation/theme_projection_test.gd extends SceneTree const Kit := preload("res://tests/unit/test_kit.gd") const ConfigDBScript := preload("res://core/config_db.gd") const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现(迁移期按现路径 preload——账本 §五夹具前提) var _db var _rng func _init() -> void: _db = ConfigDBScript.new() # 表加载失败不中止(承 G2–G5 同款:表损坏的红落在具体断言上) var load_ok: bool = _db.load_tables("res://data/tables", true) if not load_ok: push_error("[theme_projection_test] 表加载返回失败——诊断如下(红由具体断言落)") for d in _db.diagnostics(): if d.severity == "error" or d.severity == "fatal": push_error(" [%s] %s: %s" % [d.severity, d.category, d.message]) _rng = load("res://core/rng_service.gd").new() _rng.set_master_seed(42) # 套执行序沿源编排序(_run_core_smoke 序 + _smoke_meta 薄壳尾序) _smoke_type_glow() _smoke_battle_log_view() _smoke_global_theme() _smoke_button_five_states() _smoke_texture_filter() _smoke_contrast_inks() _smoke_element_details() _smoke_diag_button_retired() _status_panel_projection() _career_overlay_projection() quit(Kit.finish("theme_projection_test")) # ---- 隔离夹具(smoke_all.gd helper 的宿主自持私有副本——源文件收尾整体删除,宿主自给自足)---- # WCAG 相对亮度/对比度(滑轨两段对比度断言用) 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) # 定域流桩(scope 派生种子——derive_at 纯函数,与取号计数器状态无关) func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator: # stream_name = 调用点自述标签(不参与派生),保留参数零改调用方 var rg := RandomNumberGenerator.new() rg.seed = _rng.derive_at(scope) return rg # 排雷 core 态摘要(同板同序列对照面——已开记录含 kind/digit、封印集、英雄位、AP、相位) func _ms_digest(ms, ae) -> Array: var b: Variant = ms.current_board() var rev: Array = [] for c in ms.revealed_cells(): var rec: Dictionary = ms.reveal_record(c) rev.append([int(c.x), int(c.y), String(rec.get("kind", "")), int(rec.get("digit", -1))]) rev.sort() var seals: Array = [] for y in b.height: for x in b.width: var c := Vector2i(x, y) if ms.has_seal(c): seals.append([int(c.x), int(c.y)]) return [[int(ms.hero_pos().x), int(ms.hero_pos().y)], rev, seals, ae.ap, ae.phase, ms.seal_count()] # 微光夹具身份解析桩:位点覆盖表 _glow_type_overrides(Vector2i → cell_type)+封锁集 # _glow_blocked(Vector2i → true,返回 null = 不在摇号清单的封锁格)。 # 哈希兜底按坐标确定性散列——全缸可复现。 const _GLOW_FIXTURE_TYPES := ["attack", "defense", "blast", "heal", "weaken", "pierce", "magic", "action", "gp"] var _glow_type_overrides := {} var _glow_blocked := {} func _glow_fixture_identity(c: Vector2i) -> Variant: if _glow_blocked.has(c): return null var ctype := String(_glow_type_overrides.get(c, "")) if ctype == "": ctype = _GLOW_FIXTURE_TYPES[int(abs(c.x * 31 + c.y * 17)) % _GLOW_FIXTURE_TYPES.size()] return {"reward_id": "fix_%d_%d" % [c.x, c.y], "cell_type": ctype, "tier": 1} # 英雄 8 邻中未开且未移除的格数(夹具起板前提——初始展开常把 8 邻全揭示) func _glow_unrevealed_neighbors(ms) -> int: var n := 0 var hero: Vector2i = ms.hero_pos() for c in ms.current_board().neighbors8(hero): if not ms.is_revealed(c) and not ms.is_removed(c): n += 1 return n # 独立重算的期望作用集(不读 BoardView 私有域——恒等断言的两侧各自算) func _glow_expected_set(b, ms) -> Dictionary: var out := {} var hero: Vector2i = ms.hero_pos() for n in b.neighbors8(hero): if not ms.is_revealed(n) and not ms.is_removed(n): out[n] = true return out # 键集相等(期望集是纯键集——V1 恒等面对照) func _glow_key_set_equal(a: Dictionary, b: Dictionary) -> bool: if a.size() != b.size(): return false for k in a: if not b.has(k): return false return true # 键值逐位相等(off→on 复现——两侧都是作用集快照,组号亦须逐位一致) func _glow_map_equal(a: Dictionary, b: Dictionary) -> bool: if a.size() != b.size(): return false for k in a: if not b.has(k) or int(a[k]) != int(b[k]): return false return true # 元进度隔离档夹具(_status_panel_projection/_career_overlay_projection 消费——G9 同名 helper # 的宿主侧私有副本,双份过渡,收尾批去重) const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry.json", "user://smoke_meta_settle.json", "user://smoke_meta_workshop.json", "user://smoke_meta_cap.json", "user://smoke_meta_white.json", "user://smoke_meta_attune.json", "user://smoke_meta_attune2.json", "user://smoke_meta_tree.json", "user://smoke_meta_career.json", "user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json", "user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json", "user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"] func _meta_smoke_cleanup() -> void: for p in _META_SMOKE_PATHS: for suffix in ["", ".tmp", ".corrupt.bak"]: if FileAccess.file_exists(p + suffix): DirAccess.remove_absolute(p + suffix) func _meta_new_save(path: String): var sv = load("res://core/save_manager.gd").new(path) sv.load_from_disk() return sv # ---- 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入)---- func _smoke_type_glow() -> void: print("--- 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入)---") var board_generator = load("res://core/board_generator.gd").new() var b = board_generator.generate(1, 0, _mk_stream("board_gen", 4242), _db) var ae = load("res://core/action_economy.gd").new(_db) var ms = load("res://core/minesweeper_rules.gd").new(_db, ae) ms.install_board(b) ae.notify_round_start() # 新手策略驱动到「英雄邻域有未开格」态(初始展开常把 8 邻全揭示——取证探针同款起手法; # 恒定种子确定性,步数上限 40 防冻态空转) var steps := 0 while _glow_unrevealed_neighbors(ms) < 5 and steps < 40: var act: Dictionary = AutoPlayer.novice_action(ms) if String(act.get("kind", "stop")) == "stop": break var op := String(act.get("kind", "")) if op == "seal": op = "seal_place" ms.settle_batch([{"action": op, "cell": act.get("cell")}]) steps += 1 var view = load("res://presentation/views/board_view.gd").new() view.set_identity_resolver(_glow_fixture_identity) view.rebuild(b, ms) view.set_dig_glow_enabled(true) var hero: Vector2i = ms.hero_pos() # 快照 trio:棋盘态(零玩法写入的对照面)+作用集 var revealed_before: Array = ms.revealed_cells() revealed_before.sort() var hero_before: Vector2i = ms.hero_pos() var seals_before: int = ms.seal_count() # ---- 相 0:夹具全集合法九型 ⇒ 作用集 == 独立重算期望集(V1)---- var snap: Dictionary = view.type_glow_probe() Kit.check("V1 微光默认开(Q4 丙案——呈现态布尔缺省 true,生产写入面零显式开)", bool(snap.get("enabled", false))) Kit.check("V1 夹具全集合法九型 ⇒ 清单外登记为空(禁静默空光通道零误报)", (snap.get("errors", []) as Array).is_empty()) var expect: Dictionary = _glow_expected_set(b, ms) Kit.check("V1 作用集恒等:微光格集合 == _diggable ∩ 身份可查(期望 %d,实测 %d)" % [expect.size(), (snap.get("cells", {}) as Dictionary).size()], _glow_key_set_equal(snap.get("cells", {}), expect)) Kit.check("V1 夹具前提:英雄未开邻格 ≥5(实测 %d——novice %d 步)" % [expect.size(), steps], expect.size() >= 5) # 已开格/r0 出生展开区零微光(S1:已开格不显示) var opened_leak := "" for c in ms.revealed_cells(): if (snap.get("cells", {}) as Dictionary).has(c): opened_leak += str(c) + " " for c in b.compute_r0(): if (snap.get("cells", {}) as Dictionary).has(c): opened_leak += "r0:" + str(c) + " " Kit.check("V1 已开格/r0 出生展开区零微光(S1)%s" % ("" if opened_leak == "" else "违例:" + opened_leak), opened_leak == "") # ---- 相 1:八邻派定八型 ⇒ 五组逐格映射(V2 活板面)---- var nbs: Array = b.neighbors8(hero) var assign := ["attack", "heal", "pierce", "blast", "magic", "defense", "action", "gp"] var expect_groups := {"attack": UiKit.GLOW_BUFF, "defense": UiKit.GLOW_BUFF, "weaken": UiKit.GLOW_BUFF, "action": UiKit.GLOW_BUFF, "gp": UiKit.GLOW_BUFF, "heal": UiKit.GLOW_HEAL, "pierce": UiKit.GLOW_PIERCE, "blast": UiKit.GLOW_AOE, "magic": UiKit.GLOW_MAGIC} var mapped := 0 for i in mini(assign.size(), nbs.size()): var nb: Vector2i = nbs[i] if ms.is_revealed(nb) or ms.is_removed(nb): continue # 已开邻格不派定——只测未开格 _glow_type_overrides[nb] = assign[i] mapped += 1 view.refresh() snap = view.type_glow_probe() Kit.check("V2 夹具前提:未开邻格派定 ≥5 型(实测 %d)" % mapped, mapped >= 5) var map_bad := "" for i in mini(assign.size(), nbs.size()): var nb2: Vector2i = nbs[i] if not _glow_type_overrides.has(nb2): continue var got: int = int((snap.get("cells", {}) as Dictionary).get(nb2, 0)) if got != int(expect_groups[assign[i]]): map_bad += "%s:%s→组%d(期望组%d) " % [str(nb2), assign[i], got, int(expect_groups[assign[i]])] Kit.check("V2 九型→五组逐格映射(attack/defense/action/gp→增益/heal→治疗/pierce→穿刺/blast→群伤/magic→魔法)%s" % ("" if map_bad == "" else "违例:" + map_bad), map_bad == "") # ---- 相 2:清单外 cell_type ⇒ 显式登记 + 该格无微光(V2 禁静默空光)---- var bogus_cell: Vector2i = hero for i in mini(assign.size(), nbs.size()): var nb3: Vector2i = nbs[i] if _glow_type_overrides.has(nb3): bogus_cell = nb3 break _glow_type_overrides[bogus_cell] = "keystone" # 大宝箱未来第十型之外的清单外值 view.refresh() snap = view.type_glow_probe() var errs: Array = snap.get("errors", []) Kit.check("V2 清单外 cell_type 显式登记(errors=%s)" % str(errs), errs.has("keystone")) Kit.check("V2 清单外格零微光(无静默空光——%s 不在作用集)" % str(bogus_cell), not (snap.get("cells", {}) as Dictionary).has(bogus_cell)) Kit.check("V2 清单外格剔除后作用集 = 派定数 −1(%d→%d)" % [mapped, (snap.get("cells", {}) as Dictionary).size()], (snap.get("cells", {}) as Dictionary).size() == mapped - 1) # ---- 相 3:封锁格(query 返回 null)⇒ 无微光且零登记 ---- _glow_type_overrides.erase(bogus_cell) # 复位清单外(errors 保留=整图登记语义) var blocked_cell: Vector2i = hero for i in mini(assign.size(), nbs.size()): var nb4: Vector2i = nbs[i] if _glow_type_overrides.has(nb4): blocked_cell = nb4 break _glow_blocked[blocked_cell] = true view.refresh() snap = view.type_glow_probe() Kit.check("V1 封锁格零微光(query null ⇒ 天然惰性——%s)" % str(blocked_cell), not (snap.get("cells", {}) as Dictionary).has(blocked_cell)) Kit.check("V1 封锁格零登记(errors 仍仅清单外一条——%s)" % str(snap.get("errors", [])), (snap.get("errors", []) as Array).size() == 1) Kit.check("V1 封锁格剔除后作用集 = 派定数 −1(实测 %d)" % (snap.get("cells", {}) as Dictionary).size(), (snap.get("cells", {}) as Dictionary).size() == mapped - 1) # ---- 相 4:封锁解除 ⇒ 作用集复位(进出圈复亮走回)---- _glow_blocked.erase(blocked_cell) view.refresh() snap = view.type_glow_probe() Kit.check("V1 封锁解除后作用集复位恒等(期望 %d,实测 %d)" % [expect.size(), (snap.get("cells", {}) as Dictionary).size()], _glow_key_set_equal(snap.get("cells", {}), expect)) # ---- 相 5:开关 off/on(V7——off 态空+on 态逐位复现+棋盘态零扰动)---- var snap_before_off: Dictionary = view.type_glow_probe() view.set_type_glow_enabled(false) snap = view.type_glow_probe() Kit.check("V7 off 态开关读数=false", not bool(snap.get("enabled", true))) Kit.check("V7 off 态作用集为空(基线保护面)", (snap.get("cells", {}) as Dictionary).is_empty()) view.set_type_glow_enabled(true) var snap_on: Dictionary = view.type_glow_probe() Kit.check("V7 on 态开关读数=true(复现)", bool(snap_on.get("enabled", false))) Kit.check("V7 off→on 作用集逐位复现(关前 %d 格/开后 %d 格,组号逐位一致)" % [ (snap_before_off.get("cells", {}) as Dictionary).size(), (snap_on.get("cells", {}) as Dictionary).size()], _glow_map_equal(snap_on.get("cells", {}), snap_before_off.get("cells", {}))) Kit.check("V7 off→on 作用集与独立重算期望集恒等(%d 格)" % (snap_on.get("cells", {}) as Dictionary).size(), _glow_key_set_equal(snap_on.get("cells", {}), expect)) var revealed_after: Array = ms.revealed_cells() revealed_after.sort() Kit.check("V7 微光开关零玩法写入(已开集/英雄位/封印数三面对照逐位一致)", revealed_before == revealed_after and hero_before == ms.hero_pos() and seals_before == ms.seal_count()) # ---- 相 6:换图随 rebuild 重算(V3——旧板坐标零残留)---- _glow_type_overrides.clear() _glow_blocked.clear() var b2 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), _db) var ae2 = load("res://core/action_economy.gd").new(_db) var ms2 = load("res://core/minesweeper_rules.gd").new(_db, ae2) ms2.install_board(b2) ae2.notify_round_start() view.rebuild(b2, ms2) var snap2: Dictionary = view.type_glow_probe() var expect2: Dictionary = _glow_expected_set(b2, ms2) Kit.check("V3 换图 rebuild 后作用集按新板重算(期望 %d,实测 %d——零旧板残留)" % [expect2.size(), (snap2.get("cells", {}) as Dictionary).size()], _glow_key_set_equal(snap2.get("cells", {}), expect2)) Kit.check("V3 换图清清单外登记(rebuild 清零)", (snap2.get("errors", []) as Array).is_empty()) view.free() # ---- 相 7:同板同序列 on/off 双实例结算逐位一致(契约 1 端到端面——不新起跑批)---- # 两个平行 core(同 db/同一 BoardData 产物/同初始展开),一侧挂生产默认开态 view、一侧挂 # 显式 off 态 view(差异唯一定位在类型微光开关——与渲染取证双帧同款隔离思想);同一动作 # 序列逐步对照摘要,on 侧若在微光派生中意外写板态,两侧即分叉。 var aeA = load("res://core/action_economy.gd").new(_db) var msA = load("res://core/minesweeper_rules.gd").new(_db, aeA) var aeB = load("res://core/action_economy.gd").new(_db) var msB = load("res://core/minesweeper_rules.gd").new(_db, aeB) msA.install_board(b) msB.install_board(b) aeA.notify_round_start() aeB.notify_round_start() var viewA = load("res://presentation/views/board_view.gd").new() viewA.set_identity_resolver(_glow_fixture_identity) viewA.rebuild(b, msA) viewA.set_dig_glow_enabled(true) var viewB = load("res://presentation/views/board_view.gd").new() viewB.set_identity_resolver(_glow_fixture_identity) viewB.rebuild(b, msB) viewB.set_dig_glow_enabled(true) viewB.set_type_glow_enabled(false) # off 态 = 基线保护面 var diverge := "" var seq_steps := 0 while seq_steps < 12: var act7: Dictionary = AutoPlayer.novice_action(msA) if String(act7.get("kind", "stop")) == "stop": break var op7 := String(act7.get("kind", "")) if op7 == "seal": op7 = "seal_place" var batch7 := [{"action": op7, "cell": act7.get("cell")}] msA.settle_batch(batch7) msB.settle_batch(batch7) viewA.refresh() viewB.refresh() if _ms_digest(msA, aeA) != _ms_digest(msB, aeB): diverge = "第 %d 步(%s)后摘要分叉" % [seq_steps, op7] break seq_steps += 1 Kit.check("契约 1 同板同序列 on/off 两态结算逐位一致(%d 步对照——A 开态/B 关态)%s" % [seq_steps, diverge], diverge == "") Kit.check("契约 1 对照前提:同序列 ≥5 步(实测 %d)" % seq_steps, seq_steps >= 5) viewA.free() viewB.free() # ---- 战斗日志视图·行格式化呈现层同步闸(真人试玩卡断 bug 回归: # 伤害行 attach 字段变更后本视图拆行分支漏网未同步——敌方首笔攻击行带 baseline 无 attach, # _format_row Invalid access 使结算段中途夭折、界面卡死。行协议变更必须同步呈现层〔战斗结算 §行协议〕, # 本段把现行行形态逐型喂进真实视图:拆行残留与行字段错位在此转红)---- func _smoke_battle_log_view() -> void: print("--- 战斗日志视图行格式化(行协议 9 类呈现层同步闸)---") var view = load("res://presentation/views/battle_log_view.gd").new(_db) # 敌方行动伤害行(现行形态:baseline 在场、attach 已出协议——修复前此行即 Invalid access) view.append_row({"type": "damage", "display_name": "地精 撕咬", "value": 2, "target": "主角", "baseline": 3, "roll": 15, "ac": 12, "tier": 100, "source_class": "enemy_action", "round_index": 2}) var enemy_txt: String = view.parsed_text() Kit.check("敌方伤害行格式化(baseline 行不读退役 attach)", enemy_txt.contains("撕咬") and enemy_txt.contains("〔掷 15 vs 护甲 12")) Kit.check("退役伏击/基线拆行文案不再出现", not enemy_txt.contains("伏击") and not enemy_txt.contains("基线")) # 主角攻击行·单分量模型(payload 只带实际攻击类型一键——pierce/magic 多键即崩) view.append_row({"type": "damage", "display_name": "主角攻击", "value": 4, "pierce": 4, "roll": 20, "ac": 13, "tier": 100, "crit": true, "killed": true, "source_class": "hero_attack", "round_index": 2}) var hero_txt: String = view.parsed_text() Kit.check("主角穿刺型暴击行金框(单分量 payload 不触发三分量拆行)", hero_txt.contains("暴击×") and hero_txt.contains("⚔击杀") and hero_txt.contains("■")) Kit.check("退役三分量拆行文案不再出现", not hero_txt.contains(" + 穿刺 ")) # 未生效行聚合(pattern 3 聚合纪律——同键两行呈现 ×2 不逐行刷屏) view.append_row({"type": "reward_ineffective", "display_name": "增益未生效(满层)", "batch_key": "attack_up", "source_class": "safe_cell", "round_index": 1}) view.append_row({"type": "reward_ineffective", "display_name": "增益未生效(满层)", "batch_key": "attack_up", "source_class": "safe_cell", "round_index": 1}) view.append_row({"type": "heal", "display_name": "急救包", "value": 3, "target": "主角", "source_class": "safe_cell", "round_index": 1}) var agg_txt: String = view.parsed_text() Kit.check("未生效行聚合计数 ×2 呈现", agg_txt.contains("⚠") and agg_txt.contains("×2")) # 其余行型逐型喂入:格式化中途夭折 = 解析文本零增长(脚本错误静默吞断言的盲区由此转为可见 FAIL) var rest := { "effect": {"type": "effect", "display_name": "攻击强化", "value": 1, "pool_value": 2, "effect_id": "attack_up", "source_class": "seal_hit", "round_index": 1}, "seal_miss": {"type": "seal_miss", "display_name": "封印未中", "source_class": "seal_miss", "round_index": 1}, "cast": {"type": "cast", "display_name": "卓尔精灵 施放 毒雾", "source_class": "enemy_action", "round_index": 3}, "decay": {"type": "decay", "value": 1, "round_index": 3}, "verdict": {"type": "verdict", "display_name": "下一轮次", "value": 0, "round_index": 3}, "highlight": {"type": "highlight", "display_name": "完美封印!清图奖励 ×2", "value": 2, "source_class": "clear_reward", "round_index": 4}, } for row_type in rest: var before: int = view.parsed_text().length() view.append_row(rest[row_type]) Kit.check("行型 %s 格式化落地(解析文本增长)" % String(row_type), view.parsed_text().length() > before) # ---- UI 全局主题冒烟(需求卡片 §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() Kit.check("V1 项目主题挂载(project.godot gui/theme/custom → main_theme.tres)", th != null) 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: Kit.check("V1 主题类型覆盖 %s" % t, type_set.has(t)) # V1 红线:Button 类型词条零接触(五态矩阵归 06 第一层第二条) Kit.check("V1 红线 Button 类型词条零接触", not type_set.has("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] Kit.check("V1 红线 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"), focus_hits == "") # V1:关键词条抽查在位(实例走主题链解析) var vsb := VScrollBar.new() Kit.check("V1 滚动条槽/柄三态词条在位", 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")) Kit.check("V1 滚动条 scroll_focus 未定义(红线)", not th.has_stylebox("scroll_focus", "VScrollBar") and not th.has_stylebox("scroll_focus", "HScrollBar")) var hsl := HSlider.new() Kit.check("V1 滑轨槽/填充/柄词条在位", 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")) var le := LineEdit.new() Kit.check("V1 LineEdit 词条在位(含占位色 font_placeholder_color)", le.has_theme_stylebox("normal") and le.has_theme_color("font_color") and le.has_theme_color("font_placeholder_color")) var sp := SpinBox.new() Kit.check("V1 SpinBox 箭头词条在位(updown 合并槽)", sp.has_theme_icon("updown")) var hsep := HSeparator.new() var vsep := VSeparator.new() Kit.check("V1 分隔线词条在位", hsep.has_theme_stylebox("separator") and vsep.has_theme_stylebox("separator")) var tpanel := PanelContainer.new() var tlabel := Label.new() Kit.check("V1 TooltipPanel 词条在位", tpanel.has_theme_stylebox("panel")) Kit.check("V1 TooltipLabel 词条在位", tlabel.has_theme_color("font_color") and tlabel.has_theme_font_size("font_size")) # 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 Kit.check("V2 冻结值锚定(已填充段=BG_ROOT #3A2E22,槽=暖石 #B8A582)", fill_c.is_equal_approx(Color8(58, 46, 34)) and track_c.is_equal_approx(Color8(184, 165, 130))) var ratio := _theme_contrast(fill_c, track_c) Kit.check("V2 滑轨两段对比 %.2f:1 ≥4.5:1(拖到中点一眼可辨)" % ratio, ratio >= 4.5) else: Kit.check("V2 滑轨 stylebox 类型均为 StyleBoxFlat", false) # V3:预盖件运行时证词(临时实例化读词条——无实役控件的在役证据) var cbx := CheckBox.new() Kit.check("V3 CheckBox 四态图标非空", 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) var cbt := CheckButton.new() Kit.check("V3 CheckButton 四态图标非空(与 CheckBox 同套 checked/unchecked 名,4.7 源码实证)", 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) var pm := PopupMenu.new() Kit.check("V3 PopupMenu 词条存在(弹层预盖)", pm.has_theme_stylebox("panel") and pm.has_theme_stylebox("hover") and pm.has_theme_font_size("font_size")) var ob := OptionButton.new() Kit.check("V3 OptionButton 词条存在(预盖)", ob.has_theme_stylebox("normal") and ob.has_theme_stylebox("hover") and ob.has_theme_stylebox("pressed") and ob.has_theme_color("font_color")) # 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 + " " Kit.check("V4 退役实现零残留(%s)%s" % [path, "" if bad == "" else "违例:" + bad], bad == "") # V6:中文字体防御——default_font 非空且 Label 实取字体非空 Kit.check("V6 主题 default_font 非空(挂主题顶掉 custom_font 也不丢中文字形)", th.default_font != null) var lbl := Label.new() lbl.text = "测试" Kit.check("V6 Label 实取字体非空(中文字形防御实证)", lbl.get_theme_font("font") != null) 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() # ---- 按钮五态矩阵冒烟(四工厂产物逐实例 override 断言;V1/V2)---- func _smoke_button_five_states() -> void: print("--- UI 按钮五态矩阵(四工厂五态样式盒)---") var uikit_s = load("res://presentation/views/ui_kit.gd") var states := ["normal", "hover", "pressed", "disabled", "focus"] # 普通钮(verb_button 同走 button() 打底——同断言覆盖) var b = uikit_s.button("测试") for st in states: Kit.check("V1 普通钮 %s 样式盒非空" % st, b.has_theme_stylebox_override(st)) var n_sb: StyleBox = b.get_theme_stylebox("normal") var h_sb: StyleBox = b.get_theme_stylebox("hover") var p_sb: StyleBox = b.get_theme_stylebox("pressed") var d_sb: StyleBox = b.get_theme_stylebox("disabled") var f_sb: StyleBox = b.get_theme_stylebox("focus") Kit.check("V1 普通钮 normal/hover/disabled 均为石纹九宫格贴图样式盒", n_sb is StyleBoxTexture and h_sb is StyleBoxTexture and d_sb is StyleBoxTexture) # 悬停=乙案:modulate 提亮(≠常态 WHITE——乙案判据) var h_mod: Color = (h_sb as StyleBoxTexture).modulate_color Kit.check("V1 普通钮 hover=modulate 提亮(乙案,异于常态 WHITE)", h_mod != Color.WHITE and h_mod.r > 1.0) # 禁用=甲案:本族底+α0.45 幽灵化 var d_mod: Color = (d_sb as StyleBoxTexture).modulate_color Kit.check("V1 普通钮 disabled=本族底 α0.45 幽灵化(甲案)", is_equal_approx(d_mod.a, 0.45) and d_mod.r == 1.0 and d_mod.g == 1.0) # 错配修正判据:disabled≠pressed(旧「禁用=按压图」) Kit.check("V1 普通钮 disabled≠pressed(旧错配已修正)", _sb_content_differs(d_sb, p_sb)) # 焦点=甲案:INK 2px 内缩 1px 环、无底(叠加态不遮钮底) Kit.check("V1 普通钮 focus 样式盒为扁平环(StyleBoxFlat)", f_sb is StyleBoxFlat) var fsb := f_sb as StyleBoxFlat Kit.check("V1 焦点环色=INK(与 font_focus_color 同源)", fsb.border_color == uikit_s.INK) Kit.check("V1 焦点环宽=2px", is_equal_approx(fsb.border_width_left, 2.0) and is_equal_approx(fsb.border_width_right, 2.0) and is_equal_approx(fsb.border_width_top, 2.0) and is_equal_approx(fsb.border_width_bottom, 2.0)) Kit.check("V1 焦点环内缩=1px", is_equal_approx(fsb.expand_margin_left, -1.0) and is_equal_approx(fsb.expand_margin_top, -1.0) and is_equal_approx(fsb.expand_margin_right, -1.0) and is_equal_approx(fsb.expand_margin_bottom, -1.0)) Kit.check("V1 焦点环无底(叠加态不遮钮底)", not fsb.draw_center and fsb.bg_color.a < 0.001) b.free() # 主钮:五态非空+本族平涂(pressed/disabled 非石纹)+disabled≠pressed+hover 提亮档承现行 var pb = uikit_s.primary_button("测试") for st in states: Kit.check("V1 主钮 %s 样式盒非空" % st, pb.has_theme_stylebox_override(st)) var ppn: StyleBox = pb.get_theme_stylebox("pressed") var pdd: StyleBox = pb.get_theme_stylebox("disabled") var phv: StyleBox = pb.get_theme_stylebox("hover") Kit.check("V1 主钮 pressed/disabled=本族平涂(非石纹)", ppn is StyleBoxFlat and pdd is StyleBoxFlat) Kit.check("V1 主钮 pressed=烛光黄深档(现行值不动)", (ppn as StyleBoxFlat).bg_color == Color8(214, 172, 88)) Kit.check("V1 主钮 disabled=本族底 α0.45 幽灵化", is_equal_approx((pdd as StyleBoxFlat).bg_color.a, 0.45)) Kit.check("V1 主钮 disabled≠pressed", _sb_content_differs(ppn, pdd)) Kit.check("V1 主钮 hover=提亮档(承现行不动)", (phv as StyleBoxFlat).bg_color == uikit_s.CANDLE.lightened(0.06)) pb.free() # 危险钮:补本族 pressed(深档)+disabled 幽灵化+focus;pressed/disabled 非石纹 var db = uikit_s.danger_button("测试") for st in states: Kit.check("V1 危险钮 %s 样式盒非空" % st, db.has_theme_stylebox_override(st)) var dpr: StyleBox = db.get_theme_stylebox("pressed") var dds: StyleBox = db.get_theme_stylebox("disabled") Kit.check("V1 危险钮 pressed/disabled=本族平涂(非石纹,旧错配已修正)", dpr is StyleBoxFlat and dds is StyleBoxFlat) Kit.check("V1 危险钮 pressed=深红深档(暗于常态)", (dpr as StyleBoxFlat).bg_color.r < 180.0 / 255.0) Kit.check("V1 危险钮 disabled=本族底 α0.45 幽灵化", is_equal_approx((dds as StyleBoxFlat).bg_color.a, 0.45)) Kit.check("V1 危险钮 disabled≠pressed", _sb_content_differs(dpr, dds)) db.free() # V2 主题红线维持(Button 类型词条零接触+focus 族词条零定义)——与 _smoke_global_theme 同判据复述 var th := ThemeDB.get_project_theme() if th != null: Kit.check("V2 红线维持 Button 类型词条零接触", not th.get_type_list().has("Button")) var focus_hits := "" for t in th.get_type_list(): for nm 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(nm): focus_hits += "%s/%s " % [t, nm] Kit.check("V2 红线维持 focus 族词条零定义%s" % ("" if focus_hits == "" else "(违例:" + focus_hits + ")"), focus_hits == "") # ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)---- func _smoke_texture_filter() -> void: print("--- UI 全局纹理过滤(Nearest+白名单三件)---") var v = ProjectSettings.get_setting("rendering/textures/canvas_textures/default_texture_filter") Kit.check("V3 工程设置 default_texture_filter=0(Nearest,翻转面=立绘/瓦片/九宫格 crisp)", v != null and int(v) == 0) var uikit_s = load("res://presentation/views/ui_kit.gd") var vg = uikit_s.vignette_rect() Kit.check("V3 白名单在位:暗角 vignette 显式 LINEAR", vg.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) vg.free() var bm = uikit_s.beams_rect() Kit.check("V3 白名单在位:光柱 beams 显式 LINEAR", bm.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) # T4 光柱连续 alpha 结构断言(观感归目检):带心峰值 0.05/带缘连续过渡/纵向衰减上亮下暗 var bimg: Image = bm.texture.get_image() var max_a := 0.0 var mid_a := 0 var top_sum := 0.0 var bot_sum := 0.0 for y in 560: for x in 560: var pa := bimg.get_pixel(x, y).a max_a = maxf(max_a, pa) if pa > 0.001 and pa < 0.049: mid_a += 1 if y < 280: top_sum += pa else: bot_sum += pa Kit.check("T4 光柱带心峰值 α≈0.05(8bit 截断量化下限 12/255,实测 %.5f)" % max_a, max_a >= 12.0 / 255.0 and max_a <= 0.05) Kit.check("T4 光柱带内连续 alpha 过渡像素 %d 个(旧零过渡硬边已修)" % mid_a, mid_a > 100) Kit.check("T4 光柱纵向衰减生效(上半亮度 %.0f > 下半 %.0f 的 2 倍——上亮下暗)" % [top_sum, bot_sum], top_sum > bot_sum * 2.0) bm.free() var gb = uikit_s.gradient_base(Color.BLACK, Color.WHITE) Kit.check("V3 白名单在位:地图渐变 gradient_base 显式 LINEAR", gb.texture_filter == CanvasItem.TEXTURE_FILTER_LINEAR) gb.free() # 既有显式 LINEAR 两处不丢(源码锚定——脚影/状态图标) var bw_src: String = FileAccess.get_file_as_string("res://presentation/views/board_world.gd") Kit.check("V3 脚影显式 LINEAR 不丢(board_world.gd)", bw_src.contains("shadow.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR")) var ss_src: String = FileAccess.get_file_as_string("res://presentation/views/status_slot_row.gd") Kit.check("V3 状态图标显式 LINEAR 不丢(status_slot_row.gd)", ss_src.contains("icon.texture_filter = CanvasItem.TEXTURE_FILTER_LINEAR")) # ---- <4.5:1 文字位换墨冒烟(站桩徽标两族亮档+RAP 行深石墨;V4)---- func _smoke_contrast_inks() -> void: print("--- UI 残余 <4.5:1 文字位换墨(V4 对比度锚定)---") var uikit_s = load("res://presentation/views/ui_kit.gd") var stage: Color = uikit_s.STAGE_BG var r_order := _theme_contrast(uikit_s.GOLD_BRIGHT, stage) Kit.check("V4 顺位徽标/重甲徽标 GOLD_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" % r_order, r_order >= 4.5) var r_ward := _theme_contrast(uikit_s.HERO_BLUE_BRIGHT, stage) Kit.check("V4 魔法护甲徽标 HERO_BLUE_BRIGHT 对舞台底 %.2f:1 ≥4.5:1" % r_ward, r_ward >= 4.5) var r_rap := _theme_contrast(uikit_s.STONE_DEEP, uikit_s.PANEL_DATA) Kit.check("V4 RAP 反馈行 STONE_DEEP 对数据板 %.2f:1 ≥4.5:1" % r_rap, r_rap >= 4.5) # 消费点源码锚定:新墨在位、旧裸用点清零、语义岗常量不动 var sb_src: String = FileAccess.get_file_as_string("res://presentation/views/standee_body.gd") Kit.check("V4 顺位徽标①②③ 墨=GOLD_BRIGHT(13px 行原位)", sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, 13, UiKit.GOLD_BRIGHT")) Kit.check("V4 魔法护甲徽标墨=HERO_BLUE_BRIGHT", sb_src.contains("col = UiKit.HERO_BLUE_BRIGHT")) Kit.check("V4 重甲徽标墨=GOLD_BRIGHT", sb_src.contains("col = UiKit.GOLD_BRIGHT")) Kit.check("V4 GOLD_DEEP 语义岗不动(standee:199 意图动画判据)", sb_src.contains("if _intent_col == UiKit.GOLD_DEEP:")) var ap_src: String = FileAccess.get_file_as_string("res://presentation/views/ap_bar_view.gd") Kit.check("V4 RAP 反馈行墨=STONE_DEEP(原位行锚定)", ap_src.contains('UiKit.label("",UiKit.FS_CAPTION, false, UiKit.STONE_DEEP)')) Kit.check("V4 RAP 反馈行旧墨 STONE_FAINT 清零(含注记面)", not ap_src.contains("STONE_FAINT")) # ---- 元素细节余②③冒烟(BCard 图标槽明度差降档+商店居中+营地四栏等高)---- func _smoke_element_details() -> void: print("--- UI 元素细节余②③(图标槽降档+布局修)---") var uikit_s = load("res://presentation/views/ui_kit.gd") # ②白图标 ≥3:1 图形对比硬约束+降档方向锚定 var r_white := _theme_contrast(Color.WHITE, uikit_s.SLOT_BACKDROP_SOFT) Kit.check("T5② BCard 图标槽白图标对比 %.2f:1 ≥3:1(硬约束)" % r_white, r_white >= 3.0) var r_new := _theme_contrast(uikit_s.SLOT_BACKDROP_SOFT, uikit_s.PANEL_DATA) var r_old := _theme_contrast(uikit_s.SLOT_BACKDROP, uikit_s.PANEL_DATA) Kit.check("T5② 明度差降档生效(槽对卡面 %.2f:1 < 旧 %.2f:1)" % [r_new, r_old], r_new < r_old) var uk_src: String = FileAccess.get_file_as_string("res://presentation/views/ui_kit.gd") Kit.check("T5② BCard 图标槽落柔和档(SLOT_BACKDROP_SOFT)", uk_src.contains("UiKit.panel_flat(SLOT_BACKDROP_SOFT, PANEL_BORDER, 0, 1)")) var ss_src: String = FileAccess.get_file_as_string("res://presentation/views/status_slot_row.gd") Kit.check("T5② B 区状态图标槽仍用深档(SLOT_BACKDROP,未误伤)", ss_src.contains("UiKit.SLOT_BACKDROP")) # ③布局修源码锚定 var shop_src: String = FileAccess.get_file_as_string("res://presentation/views/shop_overlay_view.gd") Kit.check("T5③ 商店卡片区垂直居中(ALIGNMENT_CENTER)", shop_src.contains("_cards_box.alignment = BoxContainer.ALIGNMENT_CENTER")) var camp_src: String = FileAccess.get_file_as_string("res://presentation/views/camp_overlay_view.gd") var camp_min := camp_src.count("custom_minimum_size = Vector2(0, 336)") Kit.check("T5③ 营地三 holder 统一 min 高 336(实测 %d 处)" % camp_min, camp_min == 3) var camp_fill := camp_src.count("size_flags_vertical = Control.SIZE_FILL") Kit.check("T5③ 营地四栏面板纵向 FILL 等高(实测 %d 处)" % camp_fill, camp_fill == 4) Kit.check("T5③ 三处锁文垂直居中在位", camp_src.count("SIZE_SHRINK_CENTER") >= 3) # ---- 诊断转储钮冒烟(角落钮不在玩家画面+转储入口在调试浮窗;V6)---- func _smoke_diag_button_retired() -> void: print("--- UI 诊断转储钮退役(V6:玩家画面清零+浮窗可达)---") var main_src: String = FileAccess.get_file_as_string("res://presentation/main.gd") Kit.check("V6 主区无 build_diag_button 调用残留(旧实现同批清理)", not main_src.contains("build_diag_button")) Kit.check("V6 转储入口连接随迁(组合根接浮窗 diagnose_requested 信号)", main_src.contains("debug_overlay.diagnose_requested.connect(_on_diagnose_pressed)")) Kit.check("V6 转储编排承接函数在位(dump_diagnosis 卡死取证保留)", main_src.contains("func _on_diagnose_pressed()")) var asm_src: String = FileAccess.get_file_as_string("res://presentation/ui_assembler.gd") Kit.check("V6 build_diag_button 工厂已退役(ui_assembler 零残留)", not asm_src.contains("build_diag_button")) var dbg_src: String = FileAccess.get_file_as_string("res://presentation/views/debug_overlay.gd") Kit.check("V6 调试浮窗 diagnose_requested 信号在位", dbg_src.contains("signal diagnose_requested")) Kit.check("V6 浮窗转储钮外发方法在位(纯视图边界)", dbg_src.contains("_emit_diagnose_requested")) Kit.check("V6 浮窗诊断转储钮在位(玩家画面角落钮清零后唯一入口)", dbg_src.contains('UiKit.button("转储")')) # ---- 状态面板只读投影一致性(R4 数据只读同源:状态面板展示值逐项 == MetaProgression # 访问器读数;投影拼装与 main._status_data 同构(同访问器族同序)。只读性连接巡检与零动效 # 断言归 ui_struct_probe 呈现侧,本段只管数据面)---- func _status_panel_projection() -> void: print("--- 状态面板·只读投影一致性(隔离档 smoke_meta_buy 复用)---") _meta_smoke_cleanup() var sv = _meta_new_save(_META_SMOKE_PATHS[0]) var mp = load("res://core/meta_progression.gd").new(_db, sv) # 已购态夹具:余额非零 + 秘宝匣首件解锁——投影可辨非全零白板 sv.add_keystone(37) sv.unlock_item("morning_order") sv.write_to_disk() # 投影拼装(main._status_data 同构)+ 视图喂入 var data := {"balance": mp.keystone_balance(), "workshop": mp.treasure_shop_status(), "tree": mp.tree_camp_rows(), "fork_groups": mp.tree_fork_groups()} var po = load("res://presentation/views/status_panel.gd").new() po.open_status(data) Kit.check("状态面板开层可见(open_status 直开——P4 甲零动效路径)", po.visible) Kit.check("余额徽投影 = keystone_balance() 读数(%s)" % po._balance_badge.value_label.text, String(po._balance_badge.value_label.text) == "钥石 %d" % mp.keystone_balance()) var ws: Array = mp.treasure_shop_status() var ws_ok: bool = po._workshop_holder.get_child_count() == ws.size() var uikit_s = load("res://presentation/views/ui_kit.gd") for i in mini(ws.size(), po._workshop_holder.get_child_count()): var row: Dictionary = ws[i] var lbls2: Array = [] for ch in po._workshop_holder.get_child(i).get_children(): if ch is Label: lbls2.append(ch) var unlocked: bool = bool(row.get("unlocked", false)) var expect_prog := "已入手" if not unlocked: if String(row.get("mode", "")) == "keystone": expect_prog = "钥石 %d" % int(row.get("price", 0)) elif String(row.get("mode", "")) == "condition": var parts: Array[String] = [] for req_v2 in (row.get("reqs", []) as Array): var req2: Dictionary = req_v2 as Dictionary var lbl2 := String(uikit_s.TREASURE_REQ_LABELS.get(String(req2.get("key", "")), "条件")) parts.append("%s %d/%d" % [lbl2, int(req2.get("progress", 0)), int(req2.get("threshold", 0))]) expect_prog = "、".join(parts) var expect_st2 := "已解锁" if unlocked else "未解锁" ws_ok = ws_ok and lbls2.size() == 3 \ and String(lbls2[0].text) == String(row.get("display_name", "")) \ and String(lbls2[1].text) == expect_prog \ and String(lbls2[2].text) == expect_st2 Kit.check("秘宝匣 12 行投影 = treasure_shop_status() 逐项(名称/钥石价与条件进度/解锁态——首件解锁夹具在列)〔秘宝批 V19 呈现数据源〕", ws_ok) var owned := 0 for trow in (mp.tree_camp_rows() as Array): if int(trow.get("level", 0)) > 0: owned += 1 var tree_txt := String(po._tree_label.text) Kit.check("天赋树概览聚合文案与已购节点数一致(零已购=空态句)", tree_txt == "尚未购买任何节点(天赋树在营地主柜台)" or tree_txt.begins_with("已购 %d 项:" % owned)) po.visible = false po.free() # ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/ # MetaProgression 访问器读数;投影拼装与 main._career_data 同构(同访问器族同序)。 # 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示 # (v5 样档升 v6——完成定义 6「计数域缺省补零、页正常显示」))---- func _career_overlay_projection() -> void: print("--- 生涯页·只读投影一致性(隔离档 smoke_meta_career/career_v5)---") var sv = _meta_new_save(_META_SMOKE_PATHS[9]) var mp = load("res://core/meta_progression.gd").new(_db, sv) # 已玩态夹具:两局收束(一通一止)+ 钥石非零——投影可辨非全零白板 # (生涯页不统计天赋树与账号成长态,只统计终身计数) sv.add_keystone(45) sv.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3, "stopped_at": 0, "keystone_total": 0, "master_seed": 21}) sv.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1, "stopped_at": 2, "keystone_total": 0, "master_seed": 22}) sv.write_to_disk() # 投影拼装(main._career_data 同构)+ 视图喂入 var data := {"total": sv.adventure_total(), "cleared": sv.adventure_cleared(), "clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(), "boss_kills": sv.boss_kill_count(), "keystone": mp.keystone_balance()} for i in _db.level_count(): var li: int = i + 1 data["clears"].append({"display_name": String(_db.level_row(li).get("display_name", "")), "count": sv.clear_count(li)}) if sv.boss_first_kill_marked(li): data["boss_first_kills"] += 1 var co = load("res://presentation/views/career_overlay.gd").new() co.open_career(data) Kit.check("生涯页开层可见(open_career 直开——零动效路径)", co.visible) Kit.check("总局数投影 = adventure_total() 读数(%s)" % co._total_label.text, String(co._total_label.text) == "2 局") Kit.check("通关局数投影 = adventure_cleared() 读数(%s)" % co._cleared_label.text, String(co._cleared_label.text) == "1 局(三关全通)") var expect_clears: Array[String] = [] for row in (data["clears"] as Array): expect_clears.append("%s ×%d" % [String(row.get("display_name", "")), int(row.get("count", 0))]) Kit.check("各关通关次数投影 = clear_count() 逐关读数(%s)" % co._clears_label.text, String(co._clears_label.text) == " · ".join(expect_clears)) Kit.check("Boss 首杀/总杀投影 = 首杀标记计数 + boss_kill_count()(%s / %s)" % [co._boss_first_label.text, co._boss_kills_label.text], String(co._boss_first_label.text) == "3/3 关" and String(co._boss_kills_label.text) == "4 次") Kit.check("钥石余额徽投影 = keystone_balance() 读数(%s)" % co._keystone_badge.value_label.text, String(co._keystone_badge.value_label.text) == "钥石 %d" % mp.keystone_balance()) co.visible = false co.free() # 旧档载入显示(完成定义 6):v5 现役档形态(计数域两键不存在)升 v6 → 生涯页正常呈现缺省值 var v5 = load("res://core/save_manager.gd").new(_META_SMOKE_PATHS[10]) var fv5c := FileAccess.open(_META_SMOKE_PATHS[10], FileAccess.WRITE) fv5c.store_string(JSON.stringify({"version": 5, "meta": {"keystone_balance": 3}})) fv5c.close() Kit.check("v5 现役档升 v6 载入成功(计数域缺省补零)", v5.load_from_disk() and v5.adventure_total() == 0 and v5.adventure_cleared() == 0) var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5) var data_v5 := {"total": v5.adventure_total(), "cleared": v5.adventure_cleared(), "clears": [], "boss_first_kills": 0, "boss_first_total": _db.level_count(), "boss_kills": v5.boss_kill_count(), "keystone": mp_v5.keystone_balance()} for i in _db.level_count(): data_v5["clears"].append({"display_name": String(_db.level_row(i + 1).get("display_name", "")), "count": v5.clear_count(i + 1)}) var co_v5 = load("res://presentation/views/career_overlay.gd").new() co_v5.open_career(data_v5) Kit.check("旧档载入后生涯页正常显示(v5 升 v6:总局数/通关局数显示 0,既有钥石余额原样呈现)", co_v5.visible and String(co_v5._total_label.text) == "0 局" \ and String(co_v5._cleared_label.text) == "0 局(三关全通)" \ and String(co_v5._keystone_badge.value_label.text) == "钥石 3") co_v5.visible = false co_v5.free() # 内容级比较(核代码增补——对象引用比较恒真不设防):同型比内容(Texture 比纹理、Flat 比底色),异型即不同 func _sb_content_differs(a: StyleBox, b: StyleBox) -> bool: if a is StyleBoxTexture and b is StyleBoxTexture: return (a as StyleBoxTexture).texture != (b as StyleBoxTexture).texture if a is StyleBoxFlat and b is StyleBoxFlat: return (a as StyleBoxFlat).bg_color != (b as StyleBoxFlat).bg_color return a != b