iter(36卡③): 状态面板瘦身——八键投影换代+本局信息面板三段新装两段退役+两角标tooltip换代+断言面换代(theme投影族九断言手拼夹具+探针真实入口三断言对拍独立oracle);变异验证按用户P0禁令零执行(正向验收测试照跑)

This commit is contained in:
DKChan committed 2026-10-24 08:35:39 +08:00
1 parent e74656b228
commit 477a73e763
6 files changed
+152 -140

No files matched your search

+21 -5
View File
@@ -1068,18 +1068,34 @@ func _on_settings_closed() -> void:
# ---- 局内状态面板·只读(入口角标在两屏装配段;开合与数据喂入归组合根)----
# 状态面板快照投影(数据只读同源:每次开层实时读 MetaProgression 访问器现值拼装——
# 状态面板快照投影(数据只读同源:每次开层实时读访问器现值拼装——
# 快照仅服务单次开层生命周期(open_status 重建各行),跨开层零持有;零写调用。
# 状态面板零锻造段)
# 八键换代(迭代 36 卡③):跨关解锁概览退役(营地/生涯承载)——只留本局信息;
# ledger 直取下标(键缺失=致命,禁 .get 缺省留门——fail-fast 红线);禁 null 守卫软回退
# (两入口开层时 runner 恒在役——状态面板卡 F3 证据链)
func _status_data() -> Dictionary:
return {
"balance": meta.keystone_balance(),
"workshop": meta.treasure_shop_status(), # 秘宝匣 12 件解锁投影
"tree": meta.tree_camp_rows(),
"fork_groups": meta.tree_fork_groups(),
"hero_hp": int(run_scope.runner.hero_hp), # LevelRunner 属性面(_ledger 直取下标——键缺失=致命)
"max_hp": run_scope.runner.effective_max_hp(),
"atk": run_scope.runner.effective_atk(),
"ac": run_scope.runner.effective_ac(),
"gp": int(run_scope.runner.gp),
"curios": _status_curios(),
"feats": HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()),
}
# 秘宝行映射(inventory_projection 现役投影逐行取 name/used 两键——受理序沿库存投影;
# 禁第二份投影:件名换算已在 inventory_projection 收口,此处只做键裁剪)
func _status_curios() -> Array:
var out: Array = []
for row in (_treasure_ui.inventory_projection() as Array):
var r: Dictionary = row
out.append({"name": String(r.get("name", "")), "used": bool(r.get("used", false))})
return out
func _open_status_panel() -> void:
_open_modal(status_panel) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
status_panel.open_status(_status_data())
+1 -1
View File
@@ -55,7 +55,7 @@ func _init() -> void:
corner.add_theme_constant_override("separation", 8)
status_badge = UiKit.button("状态", 0.0) # 角落小件豁免宽度基准
status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT)
status_badge.tooltip_text = "查看钥石与永久强化进度(只读)"
status_badge.tooltip_text = "查看本局冒险进度(只读)"
status_badge.visible = false # 显式初始化——非对局期不误现(可见性归组合根同步)
corner.add_child(status_badge)
corner_wrap.add_child(corner)
+1 -1
View File
@@ -104,7 +104,7 @@ func _init() -> void:
# 信号连接归组合根(视图零连接))
status_badge = UiKit.button("状态", 0.0)
status_badge.add_theme_font_size_override("font_size", UiKit.FS_HINT)
status_badge.tooltip_text = "查看钥石与永久强化进度(只读)"
status_badge.tooltip_text = "查看本局冒险进度(只读)"
status_badge.size_flags_vertical = Control.SIZE_SHRINK_CENTER
top.add_child(status_badge)
_title = UiKit.label(TITLE_FORK,UiKit.FS_SUB, true, UiKit.WARM)
+37 -82
View File
@@ -1,14 +1,16 @@
## VERTICAL SLICE - NOT FOR PRODUCTION
## 局内状态面板·只读:钥石余额与进度概览(入口=两屏角标,本类不含入口)。
## 局内状态面板·只读:本局信息面板(钥石总量+本局四件:带的秘宝/已获专长/当前属性/现有 GP)。
## 跨关解锁概览(秘宝匣解锁态/天赋树已购)由营地/生涯承载——本面板不再镜像(迭代 36 卡③)。
## 账面快照由组合根 open_status 喂入;进出零动效直开直关;构成件全部经 UiKit 工厂产出——禁裸色值自建样式。
class_name StatusPanel
extends Control
signal closed
var _balance_badge: UiKit.KeystoneBadge # 钥石余额徽(badge 工厂件——Q10 甲)
var _workshop_holder: VBoxContainer
var _tree_label: Label
var _balance_badge: UiKit.KeystoneBadge # 钥石总量徽(badge 工厂件——Q10 甲)
var _attrs_label: Label
var _curios_label: Label
var _feats_label: Label
func _init() -> void:
@@ -19,11 +21,11 @@ func _init() -> void:
var title := UiKit.label("状态",UiKit.FS_TITLE, true, UiKit.INK_TITLE)
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(title)
var hint := UiKit.label("查看本局钥石与永久强化进度(只读——购买请回营地)",UiKit.FS_HINT, false, UiKit.INK_HINT)
var hint := UiKit.label("查看本局冒险进度(只读)",UiKit.FS_HINT, false, UiKit.INK_HINT)
hint.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
box.add_child(hint)
# 钥石余额行(badge 工厂件——与营地顶栏钥石徽同形制)
# 钥石总量行(badge 工厂件——与营地顶栏钥石徽同形制)
var bal_row := HBoxContainer.new()
bal_row.alignment = BoxContainer.ALIGNMENT_CENTER
box.add_child(bal_row)
@@ -31,13 +33,20 @@ func _init() -> void:
bal_row.add_child(_balance_badge)
box.add_child(HSeparator.new())
_section_head(box, "道具工坊 · 解锁态")
_workshop_holder = _section_holder(box)
_section_head(box, "本局状态")
_attrs_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL)
_attrs_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(_attrs_label)
_section_head(box, "天赋树 · 已购概览")
_tree_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL)
_tree_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(_tree_label)
_section_head(box, "带的秘宝")
_curios_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL)
_curios_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(_curios_label)
_section_head(box, "已获专长")
_feats_label = UiKit.label("",UiKit.FS_HINT, false, UiKit.INK_LABEL)
_feats_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
box.add_child(_feats_label)
box.add_child(HSeparator.new())
var close_row := HBoxContainer.new()
@@ -56,80 +65,26 @@ func _section_head(box: VBoxContainer, text: String) -> void:
box.add_child(head)
func _section_holder(box: VBoxContainer) -> VBoxContainer:
var holder := VBoxContainer.new()
holder.add_theme_constant_override("separation", 4)
box.add_child(holder)
return holder
# ---- 开层(组合根唯一调用口——快照随开随建,R4 单次开层生命周期口径)----
# 八键快照(组合根 _status_data 换代):balance/hero_hp/max_hp/atk/ac/gp/curios/feats——
# 本局四件拼装在此,禁第二份投影(秘宝=inventory_projection 行映射、专长=feat_holdings_lines 现成行)
func open_status(data: Dictionary) -> void:
_balance_badge.set_text("钥石 %d" % int(data.get("balance", 0)))
_rebuild_workshop(data)
_rebuild_tree(data)
visible = true
# 道具工坊六行(解锁态文案与营地柜台同口径——退役/封存/已解锁/未解锁;进度=解锁判定量读数)
# 秘宝匣 12 行只读概览(数据源=treasure_shop_status 投影,
# 解锁态文案与营地柜台同口径;进度列=解锁混合制三形态:钥石价/条件进度拼接/已入手)
func _rebuild_workshop(data: Dictionary) -> void:
for c in _workshop_holder.get_children():
c.queue_free()
for row in (data.get("workshop", []) as Array):
var line := HBoxContainer.new()
line.add_theme_constant_override("separation", 8)
var name_l := UiKit.label(String(row.get("display_name", "")),UiKit.FS_HINT, true, UiKit.INK)
name_l.custom_minimum_size = Vector2(120, 0)
name_l.clip_text = true
name_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(name_l)
var unlocked := bool(row.get("unlocked", false))
var prog_text := "已入手"
if not unlocked:
if String(row.get("mode", "")) == "keystone":
prog_text = "钥石 %d" % int(row.get("price", 0))
else:
var parts: Array[String] = []
for req_v in (row.get("reqs", []) as Array):
var req: Dictionary[Variant, Variant] = req_v as Dictionary
var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件"))
parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))])
prog_text = "、".join(parts)
var prog_l := UiKit.label(prog_text,UiKit.FS_HINT, false, UiKit.INK_HINT)
prog_l.custom_minimum_size = Vector2(150, 0)
prog_l.clip_text = true
prog_l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS
line.add_child(prog_l)
var st_l := UiKit.label("已解锁" if unlocked else "未解锁",UiKit.FS_HINT, true, UiKit.INK_LABEL)
st_l.size_flags_horizontal = Control.SIZE_EXPAND_FILL
line.add_child(st_l)
_workshop_holder.add_child(line)
# 天赋树已购概览(单条聚合文案——已购节点等级 + Fork 组激活侧;零已购=空态句)
func _rebuild_tree(data: Dictionary) -> void:
var owned: Array[String] = []
for row in (data.get("tree", []) as Array):
var lvl := int(row.get("level", 0))
if lvl > 0:
owned.append("%s Lv%d/%d" % [String(row.get("display_name", "")), lvl, int(row.get("cap", 1))])
for fg in (data.get("fork_groups", []) as Array):
var active := String(fg.get("active_side", ""))
if active == "":
continue
var side_name := active
for sd in (fg.get("sides", []) as Array):
if String(sd.get("side", "")) == active:
side_name = String(sd.get("display_name", active))
break
owned.append("%s·激活 %s" % [String(fg.get("display_name", "")), side_name])
if owned.is_empty():
_tree_label.text = "尚未购买任何节点(天赋树在营地主柜台)"
_attrs_label.text = "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [
int(data.get("hero_hp", 0)), int(data.get("max_hp", 0)), int(data.get("atk", 0)),
int(data.get("ac", 0)), int(data.get("gp", 0))]
var curios: Array = data.get("curios", [])
if (curios as Array).is_empty():
_curios_label.text = "未携带秘宝"
else:
_tree_label.text = "已购 %d 项:%s" % [owned.size(), " · ".join(owned)]
var parts: Array[String] = []
for row in curios:
var r: Dictionary = row
parts.append(String(r.get("name", "")) + ("(已用)" if bool(r.get("used", false)) else ""))
_curios_label.text = " · ".join(parts)
var feats: Array = data.get("feats", [])
_feats_label.text = "未获专长" if (feats as Array).is_empty() else " · ".join(feats)
visible = true
func _emit_closed() -> void:
+40
View File
@@ -762,6 +762,46 @@ func _run_modal_probes_once() -> void:
var bal_txt: String = main.status_panel._balance_badge.value_label.text
if not bal_txt.begins_with("钥石 "):
_problems.append("状态面板余额徽投影缺失(%s)" % bal_txt)
# ---- 本局信息面板换代断言(迭代36 卡③——真实入口三断言)----
# ① 面板子树零旧段文案(跨关概览退役零残留)
var sv_labels: Array = []
_collect_labels_by_text(main.status_panel, sv_labels)
for l in sv_labels:
if String(l.text).contains("道具工坊") or String(l.text).contains("天赋树"):
_problems.append("状态面板子树残留旧段文案「%s」(sv6①——零旧段失守)" % String(l.text))
# ② 属性行全值 = 契约4 格式 % runner 五现值(main 侧投影值层真链路)
var rnr = main.run_scope.runner
if rnr == null:
_problems.append("状态面板开层时 runner 空指针(契约5——状态机缺陷,禁软回退)")
else:
var attr_expect := "生命 %d/%d · 攻击 %d · 护甲 AC %d · GP %d" % [
int(rnr.hero_hp), rnr.effective_max_hp(), rnr.effective_atk(),
rnr.effective_ac(), int(rnr.gp)]
if String(main.status_panel._attrs_label.text) != attr_expect:
_problems.append("状态面板属性行投影失真(sv6②——期望 %s 实测 %s)" % [attr_expect, main.status_panel._attrs_label.text])
# ③ _status_data() 直调对拍(sv7——键集恰八键+curios/feats 独立 oracle)
var sd: Dictionary = main._status_data()
var keys := ["balance", "hero_hp", "max_hp", "atk", "ac", "gp", "curios", "feats"]
var keys_ok: bool = (sd as Dictionary).size() == 8
for k in keys:
keys_ok = keys_ok and (sd as Dictionary).has(k)
if not keys_ok:
_problems.append("_status_data() 键集≠八键(sv7——实测 %s)" % str((sd as Dictionary).keys()))
var oracle: Array = []
for rec in rnr.treasure_inventory.query_inventory(Callable()):
var rr: Dictionary = rec
oracle.append({"name": String(main.db.treasure_item_row(String(rr.get("item_id", ""))).get("display_name", "")),
"used": bool(rr.get("used", false))})
var sd_curios: Array = (sd as Dictionary).get("curios", [])
var curios_ok: bool = (sd_curios as Array).size() == (oracle as Array).size()
if curios_ok:
for i in (oracle as Array).size():
curios_ok = curios_ok and String((sd_curios[i] as Dictionary).get("name", "")) == String((oracle[i] as Dictionary).get("name", "")) and bool((sd_curios[i] as Dictionary).get("used", false)) == bool((oracle[i] as Dictionary).get("used", false))
if not curios_ok:
_problems.append("_status_data().curios 对拍独立 oracle 失守(sv7——实测 %s vs %s)" % [str(sd_curios), str(oracle)])
var feats_expect: Array = load("res://presentation/hud_presenter.gd").feat_holdings_lines(main.db, rnr.feats_held())
if str((sd as Dictionary).get("feats", [])) != str(feats_expect):
_problems.append("_status_data().feats 对拍 feat_holdings_lines 失守(sv7)")
var resume_btn: Button = ProbeUiUtil.find_button(main.status_panel, "继续", "prefix")
if resume_btn == null:
_problems.append("状态面板关闭钮缺失(「继续」)")
@@ -746,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 计数访问器/