merge(迭代36): 三卡总收口合回——卡C 开场后缀认加算区+营地六处灰字深化+迷雾测点三关归一后基准(甲)+派生校验;卡A 玩家面文案/禁字清零 13 单元+player_copy_sweep 新宿主;卡③ 状态面板瘦身(八键投影+本局信息五件+跨关概览退役)。批末代码域闸全绿(回执 8b223392 对齐分支尖,11 对拍 NEED-APPROVAL 用户裁不跑;golden 加严句触发判存量漂移用户裁不重制)。变异测试经用户 P0 禁令停止(既有记录随实现报告申报)。独立验收审计两轮通过(卡C/A 19行+卡③ sv1–sv10)。

This commit is contained in:
DKChan committed 2026-10-24 10:15:43 +08:00
commit 9eaafbf4e0
26 files changed
+1022 -171

No files matched your search

+1
View File
@@ -102,6 +102,7 @@ func _suites() -> Array:
{"path": "res://tests/unit/presentation/battle_narrator_test.gd", "expect": 0}, # 战斗叙述(数值带入/胜负互异/收束态恒定)
{"path": "res://tests/unit/presentation/type_glow_test.gd", "expect": 0}, # 未开格类型微光数据面(九型归组封闭/清单外显式失败/冻结色相只准动明度/α170·带宽cell÷8锚定/合成空间阶梯单调+模型CR≥1.20/probe API 形状)
{"path": "res://tests/unit/presentation/theme_projection_test.gd", "expect": 0}, # 呈现投影与主题宿主(微光数据面/战斗日志行协议/全局主题与按钮五态/纹理过滤白名单/对比度墨色锚/元素细节源码锚/诊断钮退役源码契约/状态面板与生涯页只读投影——批 B 自 smoke_all 迁入;试玩记录仪整套按裁决七转办集成宿主)
{"path": "res://tests/unit/presentation/player_copy_sweep_test.gd", "expect": 0}, # 玩家面文案清扫宿主(迭代36 卡A——钉值表换代断言①–⑧⑩+源 token 扫描两处;直建断言族,禁字「雷」族与旧术语清零钉值)
]
@@ -662,6 +662,30 @@ func _smoke_tree_rules() -> void:
ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久)
Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕",
(fz2.dispatch(ctx_fz2, "mine_hit", {}, Callable()) as Array).is_empty())
# 锋·惊巢反制效果行换代断言(迭代36 u13/wa12——_grant_routed 减半分支效果行=战斗日志
# step_row 真实生产面;display_name 玩家可见,禁字「挖雷」由此行清零钉住)
var orch_nc = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777003))
orch_nc.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40,
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {},
"feats": [], "tree_rules": [], "battle_index": 1,
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) # 真实战斗上下文(状态容器开域——dispatch 链受理)
var nc_rows: Array = [] # step_row 采集盒(lambda 捕引用容器——捕获变量赋值不出闭包)
orch_nc.step_row.connect(func(r): nc_rows.append(r))
# heal 负向行表值 power=6(极性字典行值——负向 power 走表值不走快照)→ halve 50% ceil=3
orch_nc._grant_routed({"cell_type": "heal", "power": 10}, "negative",
"mine_hit", Vector2i.ZERO, 0, "", "", 50)
# 受理判据=惊巢反制效果行落 step_row(halve 分支先于 dispatch 发行——flat 行 _dispatch_row
# 恒返 false,返回值不承载受理语义;行发行即分支受理)
var nc_hit := ""
for r in nc_rows:
if String((r as Dictionary).get("display_name", "")).contains("惊巢反制"):
nc_hit = String((r as Dictionary).get("display_name", ""))
break
Kit.check("锋:惊巢反制 mine_hit 减半分支受理(wa12 前提——halve_pct=50 效果行落 step_row)〔wa12〕",
nc_hit != "")
Kit.check("惊巢反制效果行==「惊巢反制——危险格挖掘惩罚减半(6→3)」零「挖雷」(u13 换代)〔wa12〕",
nc_hit == "惊巢反制——危险格挖掘惩罚减半(6→3)" and not nc_hit.contains("挖雷"))
# 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_seal 直注
var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001))
Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)",
@@ -0,0 +1,174 @@
# tests/unit/presentation/player_copy_sweep_test.gd — 玩家面文案清扫直建断言宿主(迭代36 卡A)
# 断言面 = 玩家面字符串字面量换代钉值(u1–u13 钉值表——分类词/渠道旧话/「雷」族禁字清零):
# ① hero 面板两段头(专长/属性值提升——同屏分类撞车消除)
# ② 奖励节点三选一头部(冒险牌库/渠道分流三机制事实,零「同权重/同上限/提升池」旧话)
# ③ 专长卡兜底(feat_card_text 空行兜底「专长\n???」)
# ④ 图例封印行(标空/受挖掘惩罚,零「插错/挖雷」)
# ⑤ 状态槽来源名(mine_hit→危险格——tooltip 消费半)
# ⑥ 结算来源名(settlement 表值经 ConfigDB 真装载+battle_log 行消费半)
# ⑦ 已开危险格字形(glyph「危」+plate+RED_MINE——BoardView rebuild 真实刷新路径)
# ⑧ 源 token 扫描一:board_view.gd 零 "雷" 字面量(哨兵族统一钉住)
# ⑩ 源 token 扫描二:battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(注释剥离=行首 # 判)
# 扫域口径:玩家面字符串字面量(契约 2 保留面——注释/内部常量名与注记/测试消息串不入域)。
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,
# ConfigDB 显式实例化构造注入(统一流 master 种子 42 沿源)。
# 运行:godot --headless --path game --script res://tests/unit/presentation/player_copy_sweep_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") # 自动玩家策略单一实现(夹具开域驱动)
var _db
func _init() -> void:
_db = ConfigDBScript.new()
var load_ok: bool = _db.load_tables("res://data/tables", true)
if not load_ok:
push_error("[player_copy_sweep_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])
_sweep_hero_panel()
_sweep_narrator()
_sweep_legend()
_sweep_source_names()
_sweep_board_glyph()
_sweep_token_scans()
quit(Kit.finish("player_copy_sweep_test"))
# ① C 区两段头(u1/u2——直建 hero 面板,adventure_feats_test ⑦c 族同构)
func _sweep_hero_panel() -> void:
var panel = load("res://presentation/views/hero_info_panel_view.gd").new()
var feat_head := _first_label(panel._feat_section)
var curio_head := _first_label(panel._curio_section)
Kit.check("C 区专长段头==「专长」(u1 换代——段头文案钉值)〔wa1〕",
feat_head != null and feat_head.text == "专长")
Kit.check("C 区属性值提升段头==「属性值提升」(u2 换代——同屏两段同标撞车消除)〔wa1〕",
curio_head != null and curio_head.text == "属性值提升")
# ②③ 奖励节点头部+专长卡兜底(u3/u11——静态直调)
func _sweep_narrator() -> void:
var narrator = load("res://presentation/battle_narrator.gd")
var head := String(narrator.node_curio_header())
Kit.check("奖励点头部含「冒险牌库」(u3 机制事实①同库)〔wa4〕", head.contains("冒险牌库"))
Kit.check("奖励点头部含「属性值提升与补给」(u3 机制事实②渠道分流)〔wa4〕", head.contains("属性值提升与补给"))
Kit.check("奖励点头部含「同库分流」(u3 机制事实②措辞)〔wa4〕", head.contains("同库分流"))
Kit.check("奖励点头部零「同权重」旧话(u3 清零)〔wa4〕", not head.contains("同权重"))
Kit.check("奖励点头部零「同上限」旧话(u3 清零)〔wa4〕", not head.contains("同上限"))
Kit.check("奖励点头部零「提升池」旧话(u3 清零)〔wa4〕", not head.contains("提升池"))
Kit.check("专长卡兜底==「专长\n???」(u11 换代——数据缺行真实兜底路径)〔wa3〕",
String(narrator.feat_card_text({})) == "专长\n???")
# ④ 图例封印行(u4——直建 LegendOverlay 读 ENTRIES 常量)
func _sweep_legend() -> void:
load("res://presentation/views/legend_overlay.gd").new() # 直建(_init(rap_cost) 现签名)
var seal_row: Array = []
for e in load("res://presentation/views/legend_overlay.gd").ENTRIES:
if String(e[1]) == "封印":
seal_row = e
break
Kit.check("图例封印行存在(wa5 夹具前提)〔wa5〕", not seal_row.is_empty())
Kit.check("图例封印行含「标空」(u4——与「标中」成对)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("标空"))
Kit.check("图例封印行含「受挖掘惩罚」(u4 雷字清零换代)〔wa5〕", String(seal_row[2] if seal_row.size() > 2 else "").contains("受挖掘惩罚"))
Kit.check("图例封印行零「插错」(u4 旗族遗留词干清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("插错"))
Kit.check("图例封印行零「挖雷」(u4 雷字清零)〔wa5〕", not String(seal_row[2] if seal_row.size() > 2 else "").contains("挖雷"))
# ⑤⑥ 来源名双源(u5/u6——状态槽消费半+表值 ConfigDB 真装载+battle_log 行消费半)
func _sweep_source_names() -> void:
var row_view = load("res://presentation/views/status_slot_row.gd").new()
row_view.set_effects([{"effect_id": "alert", "source_class": "mine_hit",
"display_name": "危险格受击", "remaining_settlements": 2, "round_index": 1}])
var slot: PanelContainer = null
for child in row_view.get_children():
if child is PanelContainer and child.has_meta("eff"):
slot = child
break
Kit.check("状态槽 mine_hit 效果行在座(wa6 夹具前提)〔wa6〕", slot != null)
if slot != null:
row_view._on_slot_hover(slot)
Kit.check("状态槽来源注记==「危险格」(u5 换代——meta tooltip 消费半)〔wa6〕",
row_view._tip_label.text.contains("来源:危险格"))
row_view._hide_tip()
Kit.check("结算表 source_label_map[mine_hit]==「危险格」(u6 换代——ConfigDB 真装载)〔wa7〕",
String(_db.settlement.source_label_map.get("mine_hit", "")) == "危险格")
var log_view = load("res://presentation/views/battle_log_view.gd").new(_db)
var row_txt := String(log_view._format_row({"type": "effect", "source_class": "mine_hit",
"display_name": "挖掘反噬", "value": -5, "pool_value": -5, "round_index": 1}))
Kit.check("战斗日志 mine_hit 来源行含「(危险格)」(u6 消费半——_format_row 生产路径)〔wa7〕",
row_txt.contains("(危险格)"))
# ⑦⑧ 已开危险格字形族(u7/u8/u9——BoardView rebuild 真实刷新路径+源 token 扫描)
func _sweep_board_glyph() -> void:
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()
# 邻接雷定位(挖掘面规则=须邻接英雄——novice 驱动开域至危险格入挖掘射程;上限 60 步防冻态)
var mine_cell := Vector2i(-1, -1)
var glow_steps := 0
while glow_steps < 60:
for c in b.neighbors8(ms.hero_pos()):
if b.is_mine(c) and not ms.is_revealed(c):
mine_cell = c
break
if mine_cell.x >= 0:
break
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")}])
glow_steps += 1
Kit.check("棋盘夹具含挖掘射程内未开危险格(wa8 前提——实测 %d 步)〔wa8〕" % glow_steps, mine_cell.x >= 0)
if mine_cell.x >= 0:
ms.settle_batch([{"action": "dig", "cell": mine_cell}]) # 挖开危险格(真实规则路径——受击开格)
var view = load("res://presentation/views/board_view.gd").new()
view.rebuild(b, ms) # theme_projection_test.gd:200 先例式直建(缺素材程序化回退态——fresh checkout 冒烟同纪律)
var u: Node2D = view._cells[mine_cell]
Kit.check("已开危险格 glyph==「危」(u7 换代)〔wa8〕", u.glyph == "危")
Kit.check("已开危险格 plate==true(u9 牌载哨兵包含项随动)〔wa8〕", u.plate == true)
Kit.check("已开危险格 glyph_color==RED_MINE(u7 墨色不随字形换代漂移)〔wa8〕",
u.glyph_color == UiKit.RED_MINE)
# ⑧⑩ 源 token 扫描(注释剥离=行首 # 判——契约 2 保留面不入域)
func _sweep_token_scans() -> void:
var board_src := FileAccess.get_file_as_string("res://presentation/views/board_view.gd")
Kit.check("board_view.gd 源零 \"雷\" 字面量(u7/u8/u9 哨兵族统一钉住)〔wa8〕",
board_src.count("\"雷\"") == 0)
var orch_src := FileAccess.get_file_as_string("res://core/battle_orchestrator.gd")
var orch_hits := 0
for line in orch_src.split("\n"):
if line.strip_edges().begins_with("#"):
continue
if line.contains("挖雷"):
orch_hits += 1
Kit.check("battle_orchestrator.gd 玩家面字符串字面量零「挖雷」(u13——注释保留面不入域)〔wa12〕",
orch_hits == 0)
func _first_label(node: Node) -> Label:
for child in node.get_children():
if child is Label:
return child
return null
func _mk_stream(stream_name: String, scope: int) -> RandomNumberGenerator:
# stream_name = 调用点自述标签(不参与派生),保留参数零改调用方(theme_projection_test 同构)
var rg := RandomNumberGenerator.new()
var svc = load("res://core/rng_service.gd").new()
svc.set_master_seed(42)
rg.seed = svc.derive_at(scope)
return rg
@@ -1,7 +1,8 @@
# tests/unit/presentation/theme_projection_test.gd — 呈现投影与主题单元测试宿主
# 断言面 = 十套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入):
# 断言面 = 十一套(批 B 断言迁移自 smoke_all.gd,G11 施工组;十套按源编排序迁入+开场行后缀尾接协议扩段):
# 未开格类型微光(作用集恒等/九型归组封闭/零玩法写入——board_view 微光数据面)
# 战斗日志视图行格式化(行协议 9 类呈现层同步闸——真人试玩卡断 bug 回归)
# 开场行后缀尾接协议(level_open_line 第五参布尔直控「(含面板加成)」尾接——签名零变)
# UI 全局主题(main_theme.tres 挂载/类型覆盖/滑轨两段对比/退役实现零残留/中文字体防御)
# UI 按钮五态矩阵(四工厂五态样式盒逐实例 override)
# UI 全局纹理过滤(工程 Nearest+平滑渐变件 LINEAR 白名单+光柱 alpha 结构)
@@ -42,6 +43,7 @@ func _init() -> void:
# 套执行序沿源编排序(_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()
@@ -420,6 +422,19 @@ func _smoke_battle_log_view() -> void:
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 也不丢字形)
@@ -731,63 +746,64 @@ func _smoke_diag_button_retired() -> void:
dbg_src.contains('UiKit.button("转储")'))
# ---- 状态面板只读投影一致性(R4 数据只读同源:状态面板展示值逐项 == MetaProgression
# 访问器读数;投影拼装与 main._status_data 同构(同访问器族同序)。只读性连接巡检与零动效
# 断言归 ui_struct_probe 呈现侧,本段只管数据面)----
# ---- 状态面板·本局信息面板投影一致性(迭代36 卡③——八键快照手拼夹具:展示值逐项 ==
# 快照钉值;投影拼装口径=组合根 _status_data 八键(数据只读同源)。只读性连接巡检与零动效
# 断言归 ui_struct_probe 呈现侧,本段只管数据面;跨关解锁概览断言族随面板换代退役——
# 替换判据=sv5 零旧段+八键投影断言(实现报告 §8.3 申报))----
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()}
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("余额徽投影 = 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))
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 计数访问器/
@@ -17,6 +17,7 @@ func _init() -> void:
if f != null:
Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \
and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0)))
Kit.check("中文字形在集(危——已开危险格字形换代前置,迭代36 u12/wa9)", f.has_char("危".unicode_at(0)))
Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0)))
var sz := 18
var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x