Files
gplan-2/game/tests/unit/presentation/theme_projection_test.gd
T

889 lines
52 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# tests/unit/presentation/theme_projection_test.gd — 呈现投影与主题单元测试宿主
# 断言面 = 十一套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入+开场行后缀尾接协议扩段):
# 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入——board_view 微光数据面)
# 战斗日志视图行格式化(行协议 9 类呈现层同步闸——真人试玩卡断 bug 回归)
# 开场行后缀尾接协议(level_open_line 第五参布尔直控「(含面板加成)」尾接——签名零变)
# 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:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (呈现投影与主题宿主——装载组)
_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_level_open_line()
_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:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:267 (未开格类型微光数据面组)
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:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (明细带行协议组)
print("--- 战斗日志视图行格式化(行协议 9 类呈现层同步闸)---")
var view = load("res://presentation/views/battle_log_view.gd").new(_db, _db.presentation.battle_layout)
# 敌方行动伤害行(现行形态: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)
# ---- 开场行后缀尾接协议(迭代36 wc1 静态半——level_open_line 第五参布尔直控尾接,签名零变;
# 该函数此前零既有断言,尾接行为由此钉住)----
func _smoke_level_open_line() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:272 (关卡开启行组)
var NarratorScript := load("res://presentation/battle_narrator.gd")
Kit.check("开场行 boosted=true 尾接「(含面板加成)」〔wc1〕",
String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("(含面板加成)"))
Kit.check("开场行 boosted=false 零后缀(白板局不带)〔wc1〕",
not String(NarratorScript.level_open_line("第一层", 42, 5, 30, false)).contains("(含面板加成)"))
Kit.check("开场行种子/两值带入不受后缀位影响(数值带入回归)〔wc1〕",
String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("种子 42")
and String(NarratorScript.level_open_line("第一层", 42, 5, 30, true)).contains("攻击 5 / 生命 30"))
# ---- UI 全局主题冒烟(需求卡片 §7 验收清单 V1-V6)----
# V1 挂载覆盖 / V2 滑轨两段 / V3 预盖件运行时证词 / V4 旧形态零残留 /
# V5 回归(本冒烟全绿即回归网)/ V6 中文字体防御(挂主题后 default_font 顶掉 custom_font 也不丢字形)
func _smoke_global_theme() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (全局主题组)
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:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (按钮五态组)
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()
# ---- 全局纹理过滤冒烟(工程设置 Nearest+平滑渐变件 LINEAR 白名单;V3)----
func _smoke_texture_filter() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (纹理过滤白名单组)
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:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:281 (对比度墨色锚组)
print("--- UI 残余 <4.5:1 文字位换墨(V4 对比度锚定)---")
var uikit_s = load("res://presentation/views/ui_kit.gd")
var stage: Color = uikit_s.SLOT_BACKDROP # 舞台底对比锚改读合并目标(art-bible §4.1 换色合并③——阈值与判据不变)
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(FS_HINT 14 提档原位——迭代 43 V7 换代替代,旧裸 13 清零)",
sb_src.contains("HORIZONTAL_ALIGNMENT_LEFT, -1, UiKit.FS_HINT, UiKit.GOLD_BRIGHT")
and not sb_src.contains("-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 意图蓄力档判据=分档布尔(standee_body:179 CHARGE_SKILL_IDS 判定;旧 GOLD_DEEP 恒不可达判据清零)〔wd2〕",
sb_src.contains("_intent_charge = pid in CHARGE_SKILL_IDS") and not 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:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:274 (元素细节源码锚组)
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"))
# ---- 诊断转储钮冒烟(角落钮不在玩家画面+转储入口在调试浮窗;V6)----
func _smoke_diag_button_retired() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:260 (诊断钮退役契约组)
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 信号)〔注册面 V2〕",
main_src.contains("ui.modals.menu.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("转储")'))
# ---- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具:展示值逐项 ==
# 快照钉值;投影拼装口径=组合根 _status_data 八键(数据只读同源)。只读性连接巡检与零动效
# 断言归 ui_struct_probe 呈现侧,本段只管数据面;跨关解锁概览断言族随面板换代退役——
# 替换判据=sv5 零旧段+八键投影断言(实现报告 §8.3 申报))----
func _status_panel_projection() -> void:
# oracle: design/gdd/systems/表现与支撑/HUD框架.md:274 (状态面板只读投影组)
print("--- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具,同构口径不依赖 runner)---")
_meta_smoke_cleanup() # 套件级夹具清场(生涯页 fixture 依赖此前置——旧状态段同款保留)
# 夹具(代拍板 4——任意非零可辨值,禁读 data/tables 现值、禁与属性行断言共值致误判掩盖)
var data := {"balance": 77, "hero_hp": 13, "max_hp": 29, "atk": 7, "ac": 11, "gp": 41,
"curios": [{"name": "青苔密卷", "used": false}, {"name": "锈蚀怀表", "used": true}],
"feats": ["疼痛记忆 ×2", "双管镐 ×1"]}
var po = load("res://presentation/views/status_panel.gd").new()
po.open_status(data)
Kit.check("状态面板开层可见(open_status 直开——P4 甲零动效路径)", po.visible)
Kit.check("余额徽投影 = 快照 balance 读数(sv4 回归锚)〔sv4〕",
String(po._balance_badge.value_label.text) == "钥石 77")
Kit.check("属性行全值投影 = 契约4 钉值格式逐字(生命/上限/攻击/AC/GP 五现值)〔sv1〕",
String(po._attrs_label.text) == "生命 13/29 · 攻击 7 · 护甲 AC 11 · GP 41")
Kit.check("秘宝行投影 = 受理序 join+已用件「(已用)」尾缀〔sv2〕",
String(po._curios_label.text) == "青苔密卷 · 锈蚀怀表(已用)")
Kit.check("专长行投影 = 行串 join〔sv3〕",
String(po._feats_label.text) == "疼痛记忆 ×2 · 双管镐 ×1")
# 零旧段(sv5——跨关概览退役零残留:面板子树零旧文案+旧成员退役)
var stale := ""
for ch in _all_labels(po):
if String(ch.text).contains("道具工坊") or String(ch.text).contains("天赋树"):
stale += String(ch.text) + "|"
Kit.check("面板子树零「道具工坊/天赋树」文案(跨关概览退役零残留)〔sv5〕", stale == "")
Kit.check("旧段成员退役(_workshop_holder/_tree_label 零残留——契约2 数据/成员层面清零)〔sv5〕",
po.get("_workshop_holder") == null and po.get("_tree_label") == null)
# 空态句×2(curios/feats 空夹具各跑一遍——契约4 空态呈现)
var data_empty := {"balance": 3, "hero_hp": 5, "max_hp": 6, "atk": 2, "ac": 9, "gp": 1,
"curios": [], "feats": []}
var po2 = load("res://presentation/views/status_panel.gd").new()
po2.open_status(data_empty)
Kit.check("空秘宝态句=「未携带秘宝」〔sv2〕", String(po2._curios_label.text) == "未携带秘宝")
Kit.check("空专长态句=「未获专长」〔sv3〕", String(po2._feats_label.text) == "未获专长")
# 提示行文案(sv8——新定位换代;旧「永久强化进度」半句随两段退役失效)
var hint_txt := ""
for ch in _all_labels(po):
if String(ch.text).begins_with("查看本局"):
hint_txt = String(ch.text)
Kit.check("提示行=「查看本局冒险进度(只读)」(新定位换代)〔sv8〕",
hint_txt == "查看本局冒险进度(只读)")
po.visible = false
po.free()
po2.visible = false
po2.free()
# 面板子树 Label 收集(sv5 零旧段扫描辅助——递归全子树)
func _all_labels(root: Node) -> Array:
var out: Array = []
if root is Label:
out.append(root)
for child in root.get_children():
out.append_array(_all_labels(child))
return out
# ---- 生涯页只读投影一致性(汇总段六类九项逐行 == SaveManager 计数访问器/
# MetaProgression 访问器读数;投影拼装与 HudPresenter.career_data 同构(同访问器族同序)。
# 只读性连接巡检与零动效断言归 ui_struct_probe 呈现侧,本段只管数据面+旧档载入显示
# (v5 样档升 v6——完成定义 6「计数域缺省补零、页正常显示」))----
func _career_overlay_projection() -> void:
# oracle: design/gdd/ux/camp.md:45 (生涯页只读投影组)
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.meta_state._add_keystone(45)
sv.meta_state.apply_adventure_summary({"result": "completed", "cleared": true, "levels_cleared": 3,
"stopped_at": 0, "keystone_total": 0, "master_seed": 21})
sv.meta_state.apply_adventure_summary({"result": "abandoned", "cleared": false, "levels_cleared": 1,
"stopped_at": 2, "keystone_total": 0, "master_seed": 22})
sv.write_to_disk()
# 投影拼装(HudPresenter.career_data 同构)+ 视图喂入
var data := {"total": sv.meta_state.career_state().adventure_total, "cleared": sv.meta_state.career_state().adventure_cleared,
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
"boss_kills": sv.meta_state.career_state().boss_kill_count, "keystone": mp.meta_state.keystone_balance()}
for i in _db.run.level.level_count():
var li: int = i + 1
data["clears"].append({"display_name": String(_db.run.level.row_by_level_index(li).get("display_name", "")),
"count": sv.meta_state.career_state().clear_counts.get(str(li), 0)})
if sv.meta_state.career_state().boss_first_kill.has(str(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.meta_state.keystone_balance())
# 口径换代(2026-10-26):行标签换代钉子+生涯页零「通关」残留(负断言兜全页)
var career_label_texts: Array[String] = []
for career_lbl: Label in _all_labels(co):
career_label_texts.append(String(career_lbl.text))
var career_labels_joined: String = "|".join(career_label_texts)
Kit.check("生涯页行标签换代(冒险成功局数/各关完成次数)〔口径换代 V4〕",
career_labels_joined.contains("冒险成功局数") and career_labels_joined.contains("各关完成次数"))
Kit.check("生涯页 label 全集零「通关」残留〔口径换代 V4〕", not career_labels_joined.contains("通关"))
Kit.check("秘宝解锁条件标签冒险成功口径(UiKit 单点常量)〔口径换代 V5〕",
UiKit.TREASURE_REQ_LABELS["adventure_cleared"] == "冒险成功")
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.meta_state.career_state().adventure_total == 0 and v5.meta_state.career_state().adventure_cleared == 0)
var mp_v5 = load("res://core/meta_progression.gd").new(_db, v5)
var data_v5 := {"total": v5.meta_state.career_state().adventure_total, "cleared": v5.meta_state.career_state().adventure_cleared,
"clears": [], "boss_first_kills": 0, "boss_first_total": _db.run.level.level_count(),
"boss_kills": v5.meta_state.career_state().boss_kill_count, "keystone": mp_v5.meta_state.keystone_balance()}
for i in _db.run.level.level_count():
data_v5["clears"].append({"display_name": String(_db.run.level.row_by_level_index(i + 1).get("display_name", "")),
"count": v5.meta_state.career_state().clear_counts.get(str(i + 1), 0)})
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