877 lines
51 KiB
GDScript
877 lines
51 KiB
GDScript
# 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:
|
||
_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:
|
||
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, _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:
|
||
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:
|
||
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()
|
||
|
||
|
||
# ---- 全局纹理过滤冒烟(工程设置 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.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:
|
||
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:
|
||
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.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:
|
||
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:
|
||
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
|