iter55-卡1:QA覆盖余量收口批: 46→45真缺口断言落地(违约词负面套件/结算与cap与换算率钉/配额满/rap端到端/布局/天赋树/状态行/L3变异/reward行)+p_perfect读表接线(行为零漂移)+豁免登记(ontology 154/controls 19)+探针扩面十一组真输入+登记件开发面过滤(DebugOverlay/调试角标/inter三根, seen开发面键0)+枚举器scan_screens基类分类(绘制域件与调试浮窗出圈, 868实体/未豁免未补测=0)+cell_mark_overlay渲染通道(采集态六类型注入全入帧, 棋盘三图基线换代用户过目, 渲染闸绿);卡修轮2用户裁两处不豁免(渲染通道+开发面出圈);独立审计U1修复闭合+增量轮通过
This commit is contained in:
1 parent
095bb4c91b
commit
c41ce9cfc5
25 files changed
+1502
-26
No files matched your search
@@ -414,12 +414,15 @@ func grant_clear_reward(ri: int, skip_on_refresh: bool, perfect: bool) -> void:
|
||||
if not (snap is Dictionary):
|
||||
return
|
||||
var first_ok := _grant_clear_identity(snap, ri, "(清图奖励)")
|
||||
# 完美封印追加兑付倍率读表(数值单一事实源——p_perfect 经 ConfigDB settlement_core 行,
|
||||
# 载体域 {100,200}:200=×2 追加第二份、100=×1 单份不发。追加份数结构上限 1 份不变)
|
||||
var perfect_mult := int(db.combat.settlement.core.p_perfect) / 100
|
||||
var second_ok := false
|
||||
if perfect and first_ok:
|
||||
if perfect and first_ok and perfect_mult >= 2:
|
||||
second_ok = _grant_clear_identity(snap, ri, "(完美第二份)")
|
||||
if perfect and first_ok:
|
||||
emit_cb.call({"type": "highlight", "source_class": "clear_reward", "origin_cell": Vector2i(-1, -1), "highlight": "perfect_sweep"},
|
||||
ri, "完美封印!清图奖励 ×2", (2 if second_ok else 1), {})
|
||||
ri, "完美封印!清图奖励 ×%d" % perfect_mult, (1 + (1 if second_ok else 0)), {})
|
||||
|
||||
|
||||
# 清图奖励 = 整图独立对象(不属于任何一格):兑付形态按其 cell_type 的正向字典形态走,
|
||||
|
||||
@@ -23,6 +23,73 @@ var _level_readings := {} # 关 → 双帧比对读数(实现报告读数表
|
||||
var _cur_settlement = null # 当关 settlement(身份回调读取面——仿生产 run_scope.settlement 现值;
|
||||
# 不用 Callable.bind 传参:bind 的 Callable 动态派发时对类型化参数做 Object 硬校验,
|
||||
# 报「Cannot convert argument 2 from Object to Vector2i」——直接方法引用+成员现值无此坑)
|
||||
var _cur_mark_counts := {} # 当关 cell_mark_overlay 绘制域逐类型格计数(注入后读数——V14 采集态核对)
|
||||
|
||||
|
||||
func _probe_settlement_getter():
|
||||
return _cur_settlement
|
||||
|
||||
|
||||
# 绘制域缺型注入(确定性直构——每关固定选格,同种子同态可复现):
|
||||
# 真视框=首个未开未封印雷格;预读徽章=身份查询命中的未开格(循环挑到命中,全空记零);
|
||||
# 移除退色=remove_mines_in 真实写口移除一个未揭示雷格;点迹/高亮=英雄格 face 直设定格。
|
||||
func _mark_domain_inject(view: Node, settlement: Object) -> void:
|
||||
var ms = settlement.minesweeper
|
||||
var board = ms.current_board()
|
||||
var reveal_cell := Vector2i(-9, -9)
|
||||
var preview_cell := Vector2i(-9, -9)
|
||||
var remove_cell := Vector2i(-9, -9)
|
||||
for y in board.height:
|
||||
for x in board.width:
|
||||
var c := Vector2i(x, y)
|
||||
if ms.is_revealed(c) or ms.has_seal(c) or ms.is_removed(c):
|
||||
continue
|
||||
if board.is_mine(c):
|
||||
if reveal_cell.x < 0:
|
||||
reveal_cell = c
|
||||
if remove_cell.x < 0:
|
||||
remove_cell = c
|
||||
elif preview_cell.x < 0 and _probe_identity_lookup(c) != null:
|
||||
preview_cell = c
|
||||
if remove_cell.x >= 0:
|
||||
ms.remove_mines_in([remove_cell])
|
||||
view.set_treasure_overlays([reveal_cell] if reveal_cell.x >= 0 else [],
|
||||
[preview_cell] if preview_cell.x >= 0 else [])
|
||||
view.refresh()
|
||||
var hero: Vector2i = ms.hero_pos()
|
||||
var cells: Dictionary = view.cells_probe()
|
||||
if cells.has(hero):
|
||||
(cells[hero] as Node2D).face.dot_visible = true
|
||||
var nb: Vector2i = hero + Vector2i(1, 0)
|
||||
if not cells.has(nb):
|
||||
nb = hero + Vector2i(0, 1)
|
||||
if cells.has(nb):
|
||||
(cells[nb] as Node2D).face.hl_visible = true
|
||||
_cur_mark_counts = {"injected": {"reveal": str(reveal_cell), "preview": str(preview_cell),
|
||||
"remove": str(remove_cell), "dot": str(hero)}}
|
||||
|
||||
|
||||
# 逐类型格计数读数(off 帧呈现态——face 现值直读,CellUnit 在册探针读口同面)
|
||||
func _mark_domain_counts(view: Node, tag: String) -> Dictionary:
|
||||
var counts := {"mutation_mark": 0, "treasure_mark": 0, "badge_glyph": 0,
|
||||
"dot_visible": 0, "hl_visible": 0, "removed_dim": 0}
|
||||
var cells: Dictionary = view.cells_probe()
|
||||
for c in cells:
|
||||
var f = (cells[c] as Node2D).face
|
||||
if String(f.mutation_mark) != "":
|
||||
counts["mutation_mark"] = int(counts["mutation_mark"]) + 1
|
||||
if String(f.treasure_mark) != "":
|
||||
counts["treasure_mark"] = int(counts["treasure_mark"]) + 1
|
||||
if String(f.badge_glyph) != "":
|
||||
counts["badge_glyph"] = int(counts["badge_glyph"]) + 1
|
||||
if bool(f.dot_visible):
|
||||
counts["dot_visible"] = int(counts["dot_visible"]) + 1
|
||||
if bool(f.hl_visible):
|
||||
counts["hl_visible"] = int(counts["hl_visible"]) + 1
|
||||
if bool(f.removed_dim):
|
||||
counts["removed_dim"] = int(counts["removed_dim"]) + 1
|
||||
print("[board_shot] mark_domain %s: %s" % [tag, counts])
|
||||
return counts
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -120,7 +187,17 @@ func _ready() -> void:
|
||||
# 双帧比对空转、PASS 假绿)。现值经 _cur_settlement 成员读入
|
||||
# (仿生产 run_scope.settlement;bind 传参有 Callable 类型校验坑,见成员注)。
|
||||
view.set_identity_resolver(_probe_identity_lookup)
|
||||
# 变异角标 resolver(生产同源——main_ui_build:262 set_variant_mark_resolver 接线同款):
|
||||
# 裸视图不接 = mutation_mark 恒空 = L3 伤害型变异角标缺帧。rebuild 前接(paint 链读回调)。
|
||||
var resolvers = load("res://presentation/board_resolvers.gd").new(db, null, _probe_settlement_getter)
|
||||
view.set_variant_mark_resolver(resolvers.variant_mark_lookup)
|
||||
view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
|
||||
# ---- cell_mark_overlay 绘制域采集态注入(卡修轮 2 V14——确定性直构,缺型补进采集态)----
|
||||
# 五类型注入全走真链/生产同源:真视框与预读徽章=set_treasure_overlays(生产呈现链);
|
||||
# 移除退色=remove_mines_in(核心真实写口——移除集=未揭示无封印是雷,判定基线零回溯);
|
||||
# 点迹/高亮=u.face 直设(CellUnit 探针读口同面的写侧,交互瞬态的定格化)。
|
||||
_mark_domain_inject(view, settlement)
|
||||
var mark_counts := _mark_domain_counts(view, tag)
|
||||
# 进挖掘相位(生产态 = hud_presenter.set_dig_glow_enabled(not settling) 接线——裸视图默认
|
||||
# _dig_glow=false,接触带与类型微光同源门控都不渲染;golden 基线即捕获于
|
||||
# 该裸态。双帧取证要求生产默认态,故显式开——两帧均含静态接触带(motion_scale=0.5 停格),
|
||||
@@ -342,7 +419,8 @@ func _check_dual_frames(tag: String, view, glow_cells: Dictionary, off_img: Imag
|
||||
if first_outside == "":
|
||||
first_outside = "%s 非生效格" % str(c2)
|
||||
_level_readings[tag] = {"glow_cells": glow_cells.size(), "changed_total": changed_total,
|
||||
"changed_outside": changed_outside, "band_px": band_px}
|
||||
"changed_outside": changed_outside, "band_px": band_px,
|
||||
"mark_domain": _cur_mark_counts}
|
||||
print("[board_shot] %s 双帧读数 glow=%d changed=%d outside=%d band=%.0fpx"
|
||||
% [tag, glow_cells.size(), changed_total, changed_outside, band_px])
|
||||
if changed_outside != 0:
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
# 锚(具名节点用其名、带脚本节点用脚本文件名;@ 临时名段整体不入键),控件本体记 类#钮面文本,
|
||||
# 同键控件按接线遍历序追加 #序位。键在确定构建序下可复现;UI 构建序变更会平移同键序位——
|
||||
# 表现为键漂移而非静默丢失,属测量面可接受代价。
|
||||
# 开发面边界(2026-11-07 用户裁——调试面不入玩家可达对账宇宙,登记层过滤、不产豁免条目):
|
||||
# DebugOverlay 浮窗(class_name 声明,main_ui_build 装配注「玩家画面零常驻调试件」)+
|
||||
# ui.screens.debug_badge 调试角标+ui.screens.inter_overlay 调试两层模式专用层
|
||||
# (main.gd _on_battle_finished「仅调试模式接线」——正常冒险层间推进归 AdventureRunner)。
|
||||
# 依据=test-conventions「调试注入两层」调试面发布态恒隐条款;过滤为纯减法(玩家可达面零影响)。
|
||||
# 用法:探针 _ready 里 `_recorder = ProbeCoverageRecorder.new()` → `attach(main, "<探针名>")`;
|
||||
# 收尾 `dump_js()`。探针共用面不入 S 系扫描面(code-conventions §4.13 分档)。
|
||||
extends RefCounted
|
||||
@@ -29,6 +34,8 @@ var _node_keys: Dictionary = {} # 接线时分配的节点键(instanc
|
||||
var _key_seen: Dictionary = {} # 基键出现次数(同键控件按遍历序追加序位)
|
||||
var _wired: Dictionary = {} # 已连 pressed 的按钮实例 id → true(防重连)
|
||||
var _skipped_temp := 0 # 临时名命中笔数(不入 key,只计数)
|
||||
var _dev_roots: Array[Node] = [] # 开发面根(DebugOverlay 浮窗/调试角标/inter 调试专用层)——整棵子树不入账
|
||||
var _dev_skipped := 0 # 开发面接线跳过笔数(观测面——dump meta 携带)
|
||||
|
||||
|
||||
# ---- 装载与接线 ----
|
||||
@@ -37,10 +44,32 @@ func attach(main: Node, probe_name: String) -> void:
|
||||
_main = main
|
||||
_probe_name = probe_name
|
||||
ProbeUiUtil.coverage_hook = self # 共用件真输入口统一过旗标(push_click/push_click_at/push_motion)
|
||||
_resolve_dev_roots()
|
||||
_wire_buttons()
|
||||
_wire_camp_scene()
|
||||
|
||||
|
||||
# 开发面根解析(三处成员引用/脚本身份单点,见头注边界声明)——解析仅 attach 时一次,
|
||||
# 解析不到的根跳过(无独立计数;attach 后重建的开发面根会漏滤)——漏滤方向由 seen 集
|
||||
# 「开发面键 0」核对兜住(开发面键再现=过滤失守),玩家可达面零误滤风险(早退只命中三根)。
|
||||
func _resolve_dev_roots() -> void:
|
||||
var overlay := _find_by_script(_main, "debug_overlay")
|
||||
if overlay != null:
|
||||
_dev_roots.append(overlay)
|
||||
var ui: Object = _main.get("ui")
|
||||
if ui != null:
|
||||
var screens: Object = ui.get("screens")
|
||||
if screens != null:
|
||||
for prop in ["inter_overlay", "debug_badge"]:
|
||||
var n: Node = screens.get(prop)
|
||||
if n != null:
|
||||
_dev_roots.append(n)
|
||||
|
||||
|
||||
func _is_dev_root(n: Node) -> bool:
|
||||
return _dev_roots.has(n)
|
||||
|
||||
|
||||
# 懒重扫:每次真输入 push 前接线一遍新入树的按钮(开层重建的钮行在下次点击前接上);
|
||||
# 已释放按钮的连接随节点释放失效,登记表按 instance_id 清扫。
|
||||
func mark_real_input() -> void:
|
||||
@@ -63,6 +92,9 @@ func _wire_buttons() -> void:
|
||||
|
||||
|
||||
func _wire_into(node: Node) -> void:
|
||||
if _is_dev_root(node): # 开发面整棵子树不入账(接线与命中双断——pressed 沿都不连)
|
||||
_dev_skipped += 1
|
||||
return
|
||||
if node is BaseButton:
|
||||
var id := node.get_instance_id()
|
||||
if not _wired.has(id):
|
||||
@@ -184,6 +216,9 @@ func enumerate_seen() -> Array[String]:
|
||||
|
||||
|
||||
func _seen_into(node: Node, keys: Array[String]) -> void:
|
||||
if _is_dev_root(node): # seen 枚举与登记同源同滤——开发面不入「存在但零覆盖」缺口面
|
||||
_dev_skipped += 1
|
||||
return
|
||||
if node is BaseButton:
|
||||
var id := node.get_instance_id()
|
||||
var key := String(_node_keys.get(id, ""))
|
||||
@@ -228,6 +263,8 @@ func dump_js() -> void:
|
||||
"hits_total": total,
|
||||
"seen_total": seen.size(),
|
||||
"skipped_temp_names": _skipped_temp,
|
||||
"dev_face_roots": _dev_roots.size(),
|
||||
"dev_face_skipped": _dev_skipped,
|
||||
},
|
||||
"hits": _hits,
|
||||
"seen": seen,
|
||||
|
||||
@@ -198,16 +198,30 @@ static func window_panel(view: Control) -> PanelContainer:
|
||||
# 判据:钮 rect 整体落在窗板内(上缘留 8px、下缘让开底栏 64px——返回营地钮带高约 56px)。
|
||||
# 保险丝 60 次滚叶防异常内容死循环;窗板缺失或钮句柄失效即返回(调用方前置断言兜底)。
|
||||
static func scroll_button_into_view(main, view: Control, btn: Control) -> void:
|
||||
var panel := window_panel(view)
|
||||
if panel == null or btn == null:
|
||||
if btn == null:
|
||||
return
|
||||
# 可视区基准=按钮最近的 ScrollContainer 祖先(滚动裁剪的真实可视域);
|
||||
# 无滚动祖先时回退窗壳面板(旧基准——按钮不在滚动容器内的窗体形态)。
|
||||
# 修因(迭代 55):秘宝匣 _ws_scroll 336px 高于窗壳面板内部——以面板为基准时
|
||||
# 裁剪区外按钮被误判「在视」,滚轮零滚动、真点落空(实证:购买 25/装配/升级 零命中)。
|
||||
var panel: Control = null
|
||||
var anc: Node = btn.get_parent()
|
||||
while anc != null and anc is Control:
|
||||
if anc is ScrollContainer:
|
||||
panel = anc
|
||||
break
|
||||
anc = anc.get_parent()
|
||||
if panel == null:
|
||||
panel = window_panel(view)
|
||||
if panel == null:
|
||||
return
|
||||
var guard := 60
|
||||
while guard > 0:
|
||||
var r := btn.get_global_rect()
|
||||
var pr := panel.get_global_rect()
|
||||
if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 64.0:
|
||||
if r.position.y >= pr.position.y + 8.0 and r.end.y <= pr.end.y - 8.0:
|
||||
return
|
||||
var down := r.end.y > pr.end.y - 64.0
|
||||
var down := r.end.y > pr.end.y - 8.0
|
||||
push_wheel(main, pr.get_center(), 1 if down else -1)
|
||||
guard -= 1
|
||||
await main.get_tree().process_frame
|
||||
|
||||
+335
-10
@@ -42,6 +42,9 @@ var _locktest_cell := Vector2i(-9, -9)
|
||||
var _locktest_frames := 0
|
||||
var _stoptest_state := 0 # 停手钮定型化覆盖段状态机(0=待命 1=已点击待核效 2=完结)——每 run 一次
|
||||
var _stoptest_frames := 0
|
||||
var _tested_node_continue := false # 迭代 55 B5 扩面——节点继续钮真输入一次性段
|
||||
var _tested_curio_refresh := false # 迭代 55 B5 扩面——三选一刷新钮真输入一次性段
|
||||
var _tested_shop_buy := false # 迭代 55 B5 扩面——商店购买+刷新钮真输入一次性段
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
@@ -139,6 +142,47 @@ func _ready() -> void:
|
||||
iso_fk.load_from_disk()
|
||||
if String(iso_fk.meta_state.tree_state().fork_sides.get("fg_posture", "")) != "b":
|
||||
_problems.append("Fork 切态未持久(reload 后激活态 != b)〔句柄子容器批 V1〕")
|
||||
# ---- 树面高档钮扩面段(迭代 55 B5——购买0/20/40/60 档经调试注钥后真点+先手压制 A 侧回切)----
|
||||
# 判据出处: design/gdd/systems/跨局循环/天赋树.md:198 (营地购买段——购买面真输入扩面组)
|
||||
iso_c.meta_state._add_keystone(9999)
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
for target_id in ["forge_workshop", "att_socket2", "att_socket3"]:
|
||||
var hi_btn: Button = null
|
||||
for card in main.ui.modals.camp.tree._tree_buy_btns:
|
||||
if String(card.node_id) == target_id:
|
||||
hi_btn = card.btn
|
||||
if hi_btn == null:
|
||||
_problems.append("树面 %s 购买钮缺失(高档扩面驱动中断)〔QA余 V13〕" % target_id)
|
||||
elif hi_btn.disabled:
|
||||
_problems.append("树面 %s 购买钮置灰(注钥后仍不可达——前置链失守)〔QA余 V13〕" % target_id)
|
||||
else:
|
||||
var price_hi: int = int(main.db.crossrun.meta_tree.row_by_id(target_id).get("price", 0))
|
||||
var bal_hi: int = iso_c.meta_state.keystone_balance()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, hi_btn)
|
||||
ProbeUiUtil.push_click(main, hi_btn) # 真点高档购买(forge_workshop=购买 0 钥石档)
|
||||
await get_tree().process_frame
|
||||
main.ui.modals.camp.tree.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal_hi - price_hi:
|
||||
_problems.append("树面 %s 购买未出账(%d→%d 应 −%d)〔QA余 V13〕" % [target_id, bal_hi, iso_c.meta_state.keystone_balance(), price_hi])
|
||||
if int(iso_c.meta_state.tree_state().nodes.get(target_id, 0)) != 1:
|
||||
_problems.append("树面 %s 购买落点失守(节点未点亮)〔QA余 V13〕" % target_id)
|
||||
# 先手压制 A 侧回切(先手压制=fg_posture A 侧名——购买默认 A 后既有段切至 B,A 侧钮零真输入命中)
|
||||
var side_a: Button = null
|
||||
for fk in main.ui.modals.camp.tree._tree_fork_btns:
|
||||
if String(fk.group) == "fg_posture" and String(fk.side) == "a":
|
||||
side_a = fk.btn
|
||||
if side_a == null:
|
||||
_problems.append("Fork A 侧切换钮缺失(先手压制回切驱动中断)〔QA余 V13〕")
|
||||
else:
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, side_a)
|
||||
ProbeUiUtil.push_click(main, side_a) # 真点先手压制(B→A 回切)
|
||||
await get_tree().process_frame
|
||||
var iso_fk2 = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp_slot_1.json")
|
||||
iso_fk2.load_from_disk()
|
||||
if String(iso_fk2.meta_state.tree_state().fork_sides.get("fg_posture", "")) != "a":
|
||||
_problems.append("先手压制回切未持久(reload 后激活态 != a)〔QA余 V13〕")
|
||||
# ---- 营地灰字取色断言(迭代36 wc4——四子界面拆分后按视图分扫;卡④ T5 迁移):
|
||||
# 图鉴锁文→codex 视图(锚随形态订正去『柜台』);秘宝行未解锁/同调形态/档位/刷新行→treasure 视图
|
||||
# _workshop_holder 子树。font_color 逐处 == STONE_DEEP;树链段已闭树界面。
|
||||
@@ -193,6 +237,116 @@ func _ready() -> void:
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, treasure_back) # 真点返回(closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
# ---- 秘宝架扩面段(迭代 55 B5——购买10/购买25/装配/升级30 真点;钥匙门先经树购 ks_workshop 点亮)----
|
||||
# 判据出处: design/gdd/systems/跨局循环/秘宝.md:246 (秘宝匣柜台——购买/装配/升级真输入扩面组)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center()) # 木桩→树界面
|
||||
await get_tree().process_frame
|
||||
var ks_btn: Button = null
|
||||
for card in main.ui.modals.camp.tree._tree_buy_btns:
|
||||
if String(card.node_id) == "ks_workshop":
|
||||
ks_btn = card.btn
|
||||
if ks_btn == null:
|
||||
_problems.append("树面 ks_workshop 购买钮缺失(秘宝架扩面前置中断)〔QA余 V13〕")
|
||||
else:
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, ks_btn)
|
||||
ProbeUiUtil.push_click(main, ks_btn) # 真点(free 行——钥匙门点亮)
|
||||
await get_tree().process_frame
|
||||
var ws_tree_back: Button = ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")
|
||||
if ws_tree_back != null:
|
||||
ProbeUiUtil.push_click(main, ws_tree_back) # 真点返回营地
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center()) # 宝箱→秘宝匣(已点亮态)
|
||||
await get_tree().process_frame
|
||||
var tview = main.ui.modals.camp.treasure
|
||||
iso_c.meta_state._add_keystone(9999)
|
||||
tview.refresh_camp(main._camp_data())
|
||||
await get_tree().process_frame
|
||||
var buy10: Button = null
|
||||
var buy25: Button = null
|
||||
for c in tview.find_children("*", "Button", true, false):
|
||||
var cb := c as Button
|
||||
if cb.text.begins_with("购买 10") and not cb.disabled and buy10 == null:
|
||||
buy10 = cb
|
||||
if cb.text.begins_with("购买 25") and not cb.disabled and buy25 == null:
|
||||
buy25 = cb
|
||||
if buy10 == null:
|
||||
_problems.append("秘宝架「购买 10」钮缺失或置灰(真输入可达性失守)〔QA余 V13〕")
|
||||
else:
|
||||
var bal10: int = iso_c.meta_state.keystone_balance()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, tview, buy10)
|
||||
ProbeUiUtil.push_click(main, buy10) # 真点购买 10
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal10 - 10:
|
||||
_problems.append("秘宝购买 10 未出账(%d→%d 应 −10)〔QA余 V13〕" % [bal10, iso_c.meta_state.keystone_balance()])
|
||||
if buy25 == null:
|
||||
_problems.append("秘宝架「购买 25」钮缺失或置灰(真输入可达性失守)〔QA余 V13〕")
|
||||
else:
|
||||
var bal25: int = iso_c.meta_state.keystone_balance()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, tview, buy25)
|
||||
ProbeUiUtil.push_click(main, buy25) # 真点购买 25
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal25 - 25:
|
||||
_problems.append("秘宝购买 25 未出账(%d→%d 应 −25)〔QA余 V13〕" % [bal25, iso_c.meta_state.keystone_balance()])
|
||||
# 装配/升级段前重开视图(_refresh_attune_segment 的 unlocked 快照仅随 rebuild 刷新——
|
||||
# 购后不重开则新解锁件的装配钮恒禁用;重开=全量重建,快照与解锁集对齐)
|
||||
var ws_back_mid: Button = ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地")
|
||||
if ws_back_mid != null:
|
||||
ProbeUiUtil.push_click(main, ws_back_mid)
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"chest").get_center())
|
||||
await get_tree().process_frame
|
||||
tview = main.ui.modals.camp.treasure
|
||||
var attune_entry: Dictionary = {}
|
||||
for a in tview._attune_btns:
|
||||
if not (a["btn"] as Button).disabled:
|
||||
attune_entry = a
|
||||
break
|
||||
if attune_entry.is_empty():
|
||||
_problems.append("秘宝架「装配」钮缺失或全置灰(解锁后应可装)〔QA余 V13〕")
|
||||
else:
|
||||
await ProbeUiUtil.scroll_button_into_view(main, tview, attune_entry["btn"])
|
||||
ProbeUiUtil.push_click(main, attune_entry["btn"]) # 真点装配(toggle_attunement 写口)
|
||||
await get_tree().process_frame
|
||||
if not (iso_c.meta_state.attune_state().equipped as Array).has(String(attune_entry["item_id"])):
|
||||
_problems.append("秘宝装配落点失守(%s 未入装配集)〔QA余 V13〕" % String(attune_entry["item_id"]))
|
||||
if tview._refresh_btn == null or tview._refresh_btn.disabled:
|
||||
_problems.append("秘宝架「升级」钮缺失或置灰(Y 刷新升级行可达性失守)〔QA余 V13〕")
|
||||
else:
|
||||
var bal_y: int = iso_c.meta_state.keystone_balance()
|
||||
var ylvl0: int = int(iso_c.meta_state.attune_state().refresh_level)
|
||||
await ProbeUiUtil.scroll_button_into_view(main, tview, tview._refresh_btn)
|
||||
ProbeUiUtil.push_click(main, tview._refresh_btn) # 真点升级(attune_refresh 同账写口)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.attune_state().refresh_level) != ylvl0 + 1:
|
||||
_problems.append("秘宝升级落点失守(refresh_level %d→%d)〔QA余 V13〕" % [ylvl0, int(iso_c.meta_state.attune_state().refresh_level)])
|
||||
if int(iso_c.meta_state.keystone_balance()) >= bal_y:
|
||||
_problems.append("秘宝升级未出账(%d→%d)〔QA余 V13〕" % [bal_y, iso_c.meta_state.keystone_balance()])
|
||||
var ws_back2: Button = ProbeUiUtil.find_button(main.ui.modals.camp.treasure, "返回营地")
|
||||
if ws_back2 != null:
|
||||
ProbeUiUtil.push_click(main, ws_back2) # 真点返回(关闭后交还布告板段)
|
||||
await get_tree().process_frame
|
||||
# 树面购买 30 档(ks_refresh——前置 ks_workshop 已随秘宝段点亮;同账路由购 L2 扣 60)
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"dummy").get_center())
|
||||
await get_tree().process_frame
|
||||
var ks30_btn: Button = null
|
||||
for card in main.ui.modals.camp.tree._tree_buy_btns:
|
||||
if String(card.node_id) == "ks_refresh":
|
||||
ks30_btn = card.btn
|
||||
if ks30_btn == null:
|
||||
_problems.append("树面 ks_refresh 购买钮缺失(30 档扩面驱动中断)〔QA余 V13〕")
|
||||
elif ks30_btn.disabled:
|
||||
_problems.append("树面 ks_refresh 购买钮置灰(ks_workshop 点亮后仍不可达)〔QA余 V13〕")
|
||||
else:
|
||||
var bal30: int = iso_c.meta_state.keystone_balance()
|
||||
await ProbeUiUtil.scroll_button_into_view(main, main.ui.modals.camp.tree, ks30_btn)
|
||||
ProbeUiUtil.push_click(main, ks30_btn) # 真点购买 30(同账路由——L2 档扣 60)
|
||||
await get_tree().process_frame
|
||||
if int(iso_c.meta_state.keystone_balance()) != bal30 - 60:
|
||||
_problems.append("树面 ks_refresh 购买未出账(%d→%d 应 −60)〔QA余 V13〕" % [bal30, iso_c.meta_state.keystone_balance()])
|
||||
var tree_back3: Button = ProbeUiUtil.find_button(main.ui.modals.camp.tree, "返回营地")
|
||||
if tree_back3 != null:
|
||||
ProbeUiUtil.push_click(main, tree_back3)
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"board").get_center()) # 布告板→图鉴占位(锁文宿主)
|
||||
await get_tree().process_frame
|
||||
var codex_labels: Array = []
|
||||
@@ -209,6 +363,38 @@ func _ready() -> void:
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, codex_back) # 真点返回(closed 同源钮)
|
||||
await get_tree().process_frame
|
||||
# ---- 营地 Esc 浮窗设置动线(迭代 55 B5——Esc 真键唤起浮窗→设置钮真点开设置层)----
|
||||
# 判据出处: design/ux/camp.md:45 (Esc 段组——三出口真输入扩面)
|
||||
var esc_ev := InputEventKey.new()
|
||||
esc_ev.keycode = KEY_ESCAPE
|
||||
esc_ev.physical_keycode = KEY_ESCAPE
|
||||
esc_ev.pressed = true
|
||||
main.get_viewport().push_input(esc_ev, true) # 真键(营地根态——screen_flow 同款合成路径)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("营地 Esc 真键未唤起浮窗(扩面驱动中断)〔QA余 V13〕")
|
||||
else:
|
||||
var esc_settings: Button = main.ui.modals.camp.esc_overlay._settings_btn
|
||||
if esc_settings == null:
|
||||
_problems.append("Esc 浮窗「设置」钮缺失〔QA余 V13〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, esc_settings) # 真点设置(settings_requested 三出口之一)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.menu.settings_overlay.visible:
|
||||
_problems.append("Esc 浮窗「设置」真点击未开设置层(落点判定失守)〔QA余 V13〕")
|
||||
var esc_set_close: Button = ProbeUiUtil.find_button(main.ui.modals.menu.settings_overlay, "返回", "prefix")
|
||||
if esc_set_close != null:
|
||||
ProbeUiUtil.push_click(main, esc_set_close) # 真点返回收设置层
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
var esc_ev2 := InputEventKey.new()
|
||||
esc_ev2.keycode = KEY_ESCAPE
|
||||
esc_ev2.physical_keycode = KEY_ESCAPE
|
||||
esc_ev2.pressed = true
|
||||
main.get_viewport().push_input(esc_ev2, true) # 再按 Esc 关浮窗(同键单义切换)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.esc_overlay.visible:
|
||||
_problems.append("再按 Esc 未关浮窗(扩面收口失守)〔QA余 V13〕")
|
||||
main._on_camp_closed() # 关层收闭(营地与选档层同收+开始层恢复——卡④ T3)
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.camp.scene.visible or not main.ui.screens.start_overlay.visible:
|
||||
@@ -341,7 +527,7 @@ func _ready() -> void:
|
||||
if motion_btn == null:
|
||||
_problems.append("设置面减动效开关缺失")
|
||||
else:
|
||||
motion_btn.button_pressed = false # off=快速(减动效开)
|
||||
ProbeUiUtil.push_click(main, motion_btn) # 真点拨杆(迭代 55 B5 扩面——原 button_pressed 直改绕过真输入命中面;off=快速)
|
||||
await get_tree().process_frame
|
||||
if UiKit.motion_scale != 0.5:
|
||||
_problems.append("减动效开未生效(motion_scale != 0.5)")
|
||||
@@ -349,10 +535,24 @@ func _ready() -> void:
|
||||
chk.load_settings()
|
||||
if not chk.reduced_motion:
|
||||
_problems.append("减动效开未持久化(设置独立存储失守——卡④ AC⑤)")
|
||||
motion_btn.button_pressed = true # on=完整回常态
|
||||
ProbeUiUtil.push_click(main, motion_btn) # 真点回完整(on=完整)
|
||||
await get_tree().process_frame
|
||||
if UiKit.motion_scale != 1.0:
|
||||
_problems.append("减动效关未恢复(motion_scale != 1.0)")
|
||||
# 全屏拨杆真输入段(迭代 55 B5 扩面——settings 两枚 switch_toggle 其二;store 态断言为准)
|
||||
var fs_btn: Button = main.ui.modals.menu.settings_overlay._fs_btn
|
||||
if fs_btn == null:
|
||||
_problems.append("设置面全屏拨杆缺失〔QA余 V13〕")
|
||||
else:
|
||||
var fs0: bool = iso_s.fullscreen
|
||||
ProbeUiUtil.push_click(main, fs_btn) # 真点全屏拨杆
|
||||
await get_tree().process_frame
|
||||
if bool(iso_s.fullscreen) == fs0:
|
||||
_problems.append("全屏拨杆真点击未翻转 store 态(%s→%s)〔QA余 V13〕" % [str(fs0), str(iso_s.fullscreen)])
|
||||
ProbeUiUtil.push_click(main, fs_btn) # 真点复原
|
||||
await get_tree().process_frame
|
||||
if bool(iso_s.fullscreen) != fs0:
|
||||
_problems.append("全屏拨杆复原失守〔QA余 V13〕")
|
||||
# 音量实时生效(卡④ AC①):滑杆 30% → master bus db 与 store 双读
|
||||
var vol: HSlider = main.ui.modals.menu.settings_overlay._volume
|
||||
vol.value = 30
|
||||
@@ -412,7 +612,7 @@ func _ready() -> void:
|
||||
_problems.append("生涯页关闭后未恢复开始层")
|
||||
# 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注(继续上局钮已随三入口化撤出——卡② mn8)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(400, 200) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(2000, 2000) # 高加成保 L1 可完成——L2 岔路选路段只能经冒险推进到达(单关直入口退役)
|
||||
main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(种子框默认为空=非首局随机,行为走查确定性禁依赖默认分支)
|
||||
var t1 := Time.get_ticks_msec()
|
||||
main._start_adventure_from_menu()
|
||||
@@ -451,7 +651,17 @@ func _ready() -> void:
|
||||
action_budget -= 1
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.run.node_overlay.visible:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
# 节点继续一次性落点断言(迭代 55 B5 扩面——休息停点真输入继续,落点=overlay 关闭回到战斗屏)
|
||||
if not _tested_node_continue:
|
||||
_tested_node_continue = true
|
||||
if not main.ui.flow.locks_input():
|
||||
_problems.append("节点信息浮窗打开时输入未锁定〔QA余 V13〕")
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn) # 真点「继续」
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.run.node_overlay.visible:
|
||||
_problems.append("节点「继续」真点击后浮窗未关(落点判定失守)〔QA余 V13〕")
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
if main.ui.modals.run.curio_overlay.visible:
|
||||
@@ -475,6 +685,17 @@ func _ready() -> void:
|
||||
_problems.append("奖励节点战报文案未换代(卡⑤ wr2——旧话残留或新话缺失)")
|
||||
if not main.ui.flow.locks_input():
|
||||
_problems.append("三选一停点打开时输入未锁定")
|
||||
# 三选一刷新钮一次性真输入段(迭代 55 B5 扩面——重抽意图经 refresh_requested 信号走组合根
|
||||
# on_refresh;落点=停点仍开且候选重投喂,零崩零关)
|
||||
if not _tested_curio_refresh and main.ui.modals.run.curio_overlay._refresh_btn.visible \
|
||||
and not main.ui.modals.run.curio_overlay._refresh_btn.disabled:
|
||||
_tested_curio_refresh = true
|
||||
var cards_before: int = (main.ui.modals.run.curio_overlay._bcards as Array).size()
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._refresh_btn) # 真点刷新图标钮
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.run.curio_overlay.visible \
|
||||
or (main.ui.modals.run.curio_overlay._bcards as Array).size() != cards_before:
|
||||
_problems.append("三选一刷新钮真点击后停点未保持重投喂形态(落点判定失守)〔QA余 V13〕")
|
||||
if main.ui.modals.run.curio_overlay._continue_btn.visible:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
@@ -498,6 +719,39 @@ func _ready() -> void:
|
||||
if shop_leave == null:
|
||||
_problems.append("商店「离开商店」钮缺失(真输入化驱动中断)")
|
||||
else:
|
||||
# 商店购买+刷新一次性真输入段(迭代 55 B5 扩面——购买写口经 buy_requested 信号,
|
||||
# 刷新经 refresh_requested→on_shop_refresh MC-1;测试通道注 GP 保可达,行为序列零依赖)
|
||||
if not _tested_shop_buy:
|
||||
_tested_shop_buy = true
|
||||
var rnr_shop = main.run_scope.runner
|
||||
if rnr_shop != null and int(rnr_shop.gp) < 500:
|
||||
rnr_shop.gp = 500 # 测试通道:注 GP 使购买与付费刷新可达(探针行为序列不依赖本注记)
|
||||
await get_tree().process_frame
|
||||
var buy_btn: Button = null
|
||||
for c in main.ui.modals.menu.shop_overlay.find_children("*", "Button", true, false):
|
||||
var cb := c as Button
|
||||
if cb.text.begins_with("购买") and not cb.disabled:
|
||||
buy_btn = cb
|
||||
break
|
||||
if buy_btn == null:
|
||||
_problems.append("商店「购买」钮缺失或全置灰(真输入可达性失守)〔QA余 V13〕")
|
||||
else:
|
||||
var gp_before: int = int(rnr_shop.gp)
|
||||
ProbeUiUtil.push_click(main, buy_btn) # 真点购买
|
||||
await get_tree().process_frame
|
||||
if int(rnr_shop.gp) >= gp_before:
|
||||
_problems.append("商店购买真点击未出账(GP %d→%d)〔QA余 V13〕" % [gp_before, int(rnr_shop.gp)])
|
||||
var shop_refresh: Button = main.ui.modals.menu.shop_overlay._refresh_btn
|
||||
if shop_refresh != null and not shop_refresh.disabled:
|
||||
var gp_r: int = int(rnr_shop.gp)
|
||||
ProbeUiUtil.push_click(main, shop_refresh) # 真点刷新(重抽货架)
|
||||
await get_tree().process_frame
|
||||
if not main.ui.modals.menu.shop_overlay.visible:
|
||||
_problems.append("商店刷新真点击后覆盖层异常关闭(落点判定失守)〔QA余 V13〕")
|
||||
if int(rnr_shop.gp) > gp_r:
|
||||
_problems.append("商店刷新未出账反涨(GP %d→%d)" % [gp_r, int(rnr_shop.gp)])
|
||||
else:
|
||||
_problems.append("商店刷新钮缺失或置灰(MC-1 投喂可达性失守)〔QA余 V13〕")
|
||||
ProbeUiUtil.push_click(main, shop_leave) # 真点离店(leave_pressed 同源钮)放行选路
|
||||
_seals_placed.clear()
|
||||
continue
|
||||
@@ -534,13 +788,21 @@ func _ready() -> void:
|
||||
_problems.append("地图屏加成帽未换代为「属性值提升」(wa2)")
|
||||
# 选路定向(卡⑤ d4——wa4 转正的必达前提):候选含奖励节点优先选(真实
|
||||
# press_candidate 命中路径零变化,只改索引策略);无奖励候选回退交替(岔路使用率观测面保留)
|
||||
# 迭代 55 B5 扩面——节点继续段未测时休息候选最优先(node_overlay 仅休息停点开启)
|
||||
var pick := _route_clicks % 2
|
||||
if main.run_scope.runner != null:
|
||||
for nd in main.run_scope.runner.nodes():
|
||||
var nd_id: int = main.run_scope.runner.node_id_of(nd)
|
||||
if String(nd.get("type", "")) == "reward" and main.ui.modals.run.level_screen._cand_ids.has(nd_id):
|
||||
pick = main.ui.modals.run.level_screen._cand_ids.find(nd_id)
|
||||
break
|
||||
if not _tested_node_continue:
|
||||
for nd in main.run_scope.runner.nodes():
|
||||
var nd_id_rest: int = main.run_scope.runner.node_id_of(nd)
|
||||
if String(nd.get("type", "")) == "rest" and main.ui.modals.run.level_screen._cand_ids.has(nd_id_rest):
|
||||
pick = main.ui.modals.run.level_screen._cand_ids.find(nd_id_rest)
|
||||
break
|
||||
if pick == _route_clicks % 2:
|
||||
for nd in main.run_scope.runner.nodes():
|
||||
var nd_id: int = main.run_scope.runner.node_id_of(nd)
|
||||
if String(nd.get("type", "")) == "reward" and main.ui.modals.run.level_screen._cand_ids.has(nd_id):
|
||||
pick = main.ui.modals.run.level_screen._cand_ids.find(nd_id)
|
||||
break
|
||||
ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(pick).get_center())
|
||||
_route_clicks += 1
|
||||
_seals_placed.clear()
|
||||
@@ -720,6 +982,54 @@ func _ready() -> void:
|
||||
# 卡④大门 T5 接收,两头登记禁静默删除;续玩钮已退役,主菜单确认链已摘 mn6)。
|
||||
# ① 战斗收口断点写盘断言保留(上方——冒险流直调中间态 mn10)。
|
||||
|
||||
# ---- 迭代 55 B5 扩面收尾段(地图放弃动线——主循环流不定达放弃,独立新局确定性驱动)----
|
||||
# 判据出处: design/ux/level-map.md:11 (主循环选路段——放弃二击真输入组)
|
||||
ProbeUiUtil.isolate_save(main, "smoke_click_probe_expansion", true, false, true)
|
||||
main.ui.modals.menu.debug_overlay.set_sliders(2000, 2000) # 高加成上限(与主循环同口径——层速推进保放弃停点快达)
|
||||
main.ui.modals.menu.debug_overlay.seed_edit.text = "44"
|
||||
main._start_adventure_from_menu()
|
||||
await get_tree().process_frame
|
||||
await get_tree().process_frame
|
||||
var ab_guard := 4000
|
||||
var ab_done := false
|
||||
while ab_guard > 0 and not ab_done:
|
||||
ab_guard -= 1
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.run.end_overlay.is_open():
|
||||
break # 败退先至——放弃动线未达(问题行呈报)
|
||||
if main.ui.modals.run.curio_overlay.visible:
|
||||
if main.ui.modals.run.curio_overlay._continue_btn.visible:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn)
|
||||
elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty():
|
||||
ProbeUiUtil.push_click_at(main, (main.ui.modals.run.curio_overlay._bcards[0] as Control).get_global_rect().get_center())
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._confirm_btn)
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn)
|
||||
continue
|
||||
if main.ui.modals.run.node_overlay.visible:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn)
|
||||
continue
|
||||
if main.ui.modals.run.level_screen.visible:
|
||||
var ab_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.level_screen, "放弃", "prefix")
|
||||
if ab_btn != null:
|
||||
ProbeUiUtil.push_click(main, ab_btn) # 真点「放弃」一击(3s 确认窗)
|
||||
await get_tree().process_frame
|
||||
ProbeUiUtil.push_click(main, ab_btn) # 真点二击确认(窗内)
|
||||
for i in 10:
|
||||
await get_tree().process_frame
|
||||
if main.ui.modals.run.end_overlay.is_open():
|
||||
print("[click_probe] 放弃动线落点=终局面(地图放弃真输入)")
|
||||
else:
|
||||
_problems.append("放弃二击后未达终局面(落点判定失守)〔QA余 V13〕")
|
||||
ab_done = true
|
||||
if not ab_done:
|
||||
_problems.append("扩面收尾段放弃动线未达(guard 耗尽或败退先至)〔QA余 V13〕")
|
||||
# 终局面返回开始菜单(大门段菜单前提归位——句柄沿断点段同源 _btn,文本查找零命中实证)
|
||||
if main.ui.modals.run.end_overlay.is_open() and main.ui.modals.run.end_overlay._btn != null:
|
||||
ProbeUiUtil.push_click(main, main.ui.modals.run.end_overlay._btn) # 真点「返回」(_btn=继续/返回共用句柄——断点段同款)
|
||||
await get_tree().process_frame
|
||||
|
||||
# ---- 大门两分支断言段(卡④ T5 接收——卡② 移交落点;V3 断言面)----
|
||||
# 判据出处: design/ux/camp.md:16 (大门段——点大门先出确认组)
|
||||
# 驱动=营地场景根点选信号(真实路由)→组合根 _on_camp_object_pressed→确认弹窗。
|
||||
@@ -906,7 +1216,15 @@ func _run_modal_probes_once() -> void:
|
||||
", "|"))
|
||||
if not legend_has_seal:
|
||||
_problems.append("图例缺『封印』格态条目(格态图例基线)")
|
||||
main.ui.modals.run.legend_overlay.close()
|
||||
# 图例关闭走真输入关闭钮(迭代 55 B5 扩面——原 .close() 函数直调绕过关闭钮命中面;
|
||||
# 本函数为主循环内同步调用(协程化会与主循环双分派竞态)——push_click 的 release
|
||||
# 同帧触发关闭处理器,断言同帧读数,零 await)
|
||||
var legend_close_btn: Button = ProbeUiUtil.find_button(main.ui.modals.run.legend_overlay, "关闭")
|
||||
if legend_close_btn == null:
|
||||
_problems.append("图例「关闭」钮缺失(真输入关闭路径驱动中断)〔QA余 V13〕")
|
||||
main.ui.modals.run.legend_overlay.close()
|
||||
else:
|
||||
ProbeUiUtil.push_click(main, legend_close_btn) # 真点「关闭」收图例
|
||||
if main.ui.modals.run.legend_overlay.is_open() or main.ui.flow.locks_input():
|
||||
_problems.append("图例关闭后未恢复输入/可见性")
|
||||
# ②/②b 明细回看模态无探针(M9/P-C7——冒险中回看归「生涯」,06 登记)
|
||||
@@ -1001,6 +1319,13 @@ func _run_modal_probes_once() -> void:
|
||||
_problems.append("状态槽 tooltip 未随移出隐藏")
|
||||
break
|
||||
# ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案)
|
||||
# 迭代 55 B5 扩面——确定性前置:RAP 不足时经 grant_reserve_ap 注足(测试通道——探针行为序列
|
||||
# 不依赖本注记;零注记时本链只在停手转存积累 RAP≥8 的偶发轮触发=覆盖不稳定实证)
|
||||
if not _tested_refresh_chain and main.run_scope.settlement != null \
|
||||
and main.run_scope.settlement.action_economy.phase == "Active" \
|
||||
and not main.run_scope.settlement.can_refresh() \
|
||||
and int(main.run_scope.settlement.action_economy.rap) < 8:
|
||||
main.run_scope.settlement.action_economy.grant_reserve_ap(8 - int(main.run_scope.settlement.action_economy.rap))
|
||||
if not _tested_refresh_chain and main.ui.battle.board_zone.ap_bar.is_refresh_ready():
|
||||
_tested_refresh_chain = true
|
||||
var rap_before: int = int(main.run_scope.settlement.action_economy.rap)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
## 时效判定纪律
|
||||
|
||||
基线冻结于 2026-09-30;关卡视口两 Shot(lmap_entry/lmap_boss)冻结于 2026-11-05——两 Shot 面口径(只对拍入口停点与 Boss 节点)见上文现持清单行。**判定先于动作**:每代换代面(视觉/素材/布局改动)
|
||||
基线冻结于 2026-09-30;关卡视口两 Shot(lmap_entry/lmap_boss)冻结于 2026-11-05;棋盘三图 2026-11-07 换代(采集态扩型——cell_mark_overlay 六绘制域入帧,需求卡片「QA覆盖余量收口批」卡修轮 2 V14 渲染通道裁定,新帧用户过目后重制)——两 Shot 面口径(只对拍入口停点与 Boss 节点)见上文现持清单行。**判定先于动作**:每代换代面(视觉/素材/布局改动)
|
||||
跑批时先跑一次渲染闸看差异落不落在裁剪区与容差内——
|
||||
|
||||
- **判有效**(changed 远低于 2% 阈值):基线**留在位**,不得顺手重制;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 106 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 114 KiB |
@@ -169,6 +169,7 @@ func _suites() -> Array:
|
||||
# 样板件占位断言跑绿没有信息量,且占据「27 套件」计数。文件保留作模板(同类先例见下 fog_lantern 摘除行)。
|
||||
{"path": "res://tests/unit/data_config/config_db_test.gd", "expect": 0},
|
||||
{"path": "res://tests/unit/data_config/forge_table_test.gd", "expect": 0}, # 六路线 schema 正反(V1)+特效留空(V11)+几何价格梯(V2 半边)
|
||||
{"path": "res://tests/unit/schema_vocab_violation_test.gd", "expect": 0}, # schema 词表违约词拒绝路径集中面(十七词族七检查——迭代 55 卡 T1〔QA余 V1〕;validate 诊断面直调非负面套件)
|
||||
{"path": "res://tests/unit/data_config/level_gen_params_test.gd", "expect": 0}, # 参数档 schema 正反+访问器读回(节点图生成器核心批 卡① V2〔fv2〕)
|
||||
{"path": "res://tests/unit/data_config/config_db_fatal_oob_test.gd", "expect": 1},
|
||||
{"path": "res://tests/unit/data_config/treasure_target_violation_test.gd", "expect": 1}, # 真视窄域护栏
|
||||
|
||||
@@ -42,6 +42,7 @@ func _init() -> void:
|
||||
_smoke_counter_buckets()
|
||||
_smoke_extended_rows()
|
||||
_smoke_enemy_buff_targets_self()
|
||||
_check_defensive_stance_values()
|
||||
quit(Kit.finish("round_flow_skill_pool_test"))
|
||||
|
||||
|
||||
@@ -1156,3 +1157,69 @@ func _smoke_enemy_buff_targets_self() -> void:
|
||||
break
|
||||
Kit.check("蓄力施放种子可达(恰一次 cast=charge,seed=%d)" % found_seed, found_seed > 0)
|
||||
Kit.check("敌方增益技自体落点:施放者 output 池 = 蓄力幅度(%d,实测 %d)、他敌恒 0〔个体化批 V5〕" % [emp_mag, carrier_pool], found_ok)
|
||||
|
||||
|
||||
# ---- G6 状态行值断言(迭代 55「QA覆盖余量收口批」T6/V9——defensive_stance 生产行:减伤/持续/叠层)----
|
||||
# 真实入口=战斗结算编排链(技能施放):begin_battle → 压低施法者 HP 触发 desperate_guard
|
||||
#(self_hp_pct lte 35 非 latched)→ stop 推进结算 → 施放行 duration 与施法者个体状态池读数。
|
||||
# 种子狩猎沿 _smoke_enemy_buff_targets_self 同款;叠层行为=容器级再授(roll_multiplier 先例形态)。
|
||||
|
||||
func _check_defensive_stance_values() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:56 (判定上报通道——验证函数组)
|
||||
var row: Dictionary = _db.combat.status_effects.row_by_id("defensive_stance")
|
||||
Kit.check("defensive_stance 生产行值:magnitude=-25 percent/duration_settlements=2/accumulate max_stack=1〔QA余 V9〕",
|
||||
int(row.get("magnitude", 0)) == -25 and String(row.get("unit", "")) == "percent"
|
||||
and int(row.get("duration_settlements", 0)) == 2 and String(row.get("stack_policy", "")) == "accumulate"
|
||||
and int(row.get("max_stack", 0)) == 1)
|
||||
var sc_script = load("res://core/status_container.gd")
|
||||
var found_seed := -1
|
||||
var cast_dur := -1
|
||||
var eff_val := 999
|
||||
var keep_settlement = null
|
||||
var keep_key := ""
|
||||
for s in range(1, 401):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(s)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(r): rows.append(r))
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
continue
|
||||
for turn in range(4):
|
||||
if settlement.battle_result != "":
|
||||
break
|
||||
for u in settlement._enemy_system.all_enemies():
|
||||
u.hp = mini(int(u.hp), maxi(1, int(u.max_hp) * 35 / 100)) # 测试通道:压至触发阈内(弱形式登记)
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
if settlement.battle_result != "":
|
||||
break
|
||||
var cast_seen := false
|
||||
for r in rows:
|
||||
if String(r.get("type", "")) == "cast" and String(r.get("skill_id", "")) == "desperate_guard":
|
||||
cast_seen = true
|
||||
cast_dur = int(r.get("duration", -1))
|
||||
if cast_seen:
|
||||
for u in settlement._enemy_system.all_enemies():
|
||||
var okey: String = sc_script.owner_key_for(u)
|
||||
for eff in settlement._status_effects.query_active(okey):
|
||||
if String(eff.get("effect_id", "")) == "defensive_stance":
|
||||
found_seed = s
|
||||
eff_val = int(eff.get("effective_value", 999))
|
||||
keep_settlement = settlement
|
||||
keep_key = okey
|
||||
if found_seed > 0:
|
||||
break
|
||||
if found_seed > 0:
|
||||
break
|
||||
Kit.check("种子狩猎命中(真实链施放 desperate_guard——401 种子内)〔QA余 V9〕", found_seed > 0)
|
||||
Kit.check("减伤==生产行值:施法者个体池 effective_value==-25(L×m·max_stack1 单层)〔QA余 V9〕", eff_val == -25)
|
||||
Kit.check("持续==生产行值:施放行 duration==2(buff_duration 表值)〔QA余 V9〕", cast_dur == 2)
|
||||
# 叠层行为:max_stack=1 accumulate 下容器级再授(生产 dur=行值 2)→ effective 仍单层 -25 不叠加
|
||||
if keep_settlement != null:
|
||||
var src := {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": 1}
|
||||
Kit.check("叠层再授受理", bool(keep_settlement._status_effects.apply_effect("defensive_stance", keep_key, src)))
|
||||
var eff2 := 999
|
||||
for eff in keep_settlement._status_effects.query_active(keep_key):
|
||||
if String(eff.get("effect_id", "")) == "defensive_stance":
|
||||
eff2 = int(eff.get("effective_value", 999))
|
||||
Kit.check("叠层行为:max_stack=1 再授后 effective_value 仍==-25(不翻倍)〔QA余 V9〕", eff2 == -25)
|
||||
@@ -40,6 +40,8 @@ func _init() -> void:
|
||||
_smoke_seal_irreversible_quadrants()
|
||||
_smoke_negative_dictionary()
|
||||
_smoke_resource_surface()
|
||||
_check_enemy_alert_production_row()
|
||||
_check_ap_drain_production_row()
|
||||
quit(Kit.finish("seal_polarity_test"))
|
||||
|
||||
|
||||
@@ -827,3 +829,44 @@ func _smoke_resource_surface() -> void:
|
||||
|
||||
|
||||
# 五段全序断言(战斗结算 验收"五段全序"+ 行型封闭 10 类):逐轮行相位序 = 授予 → 增益/减益 → 战斗 → 衰减 → 判定。
|
||||
|
||||
|
||||
# ---- G8 reward 行读值改断(迭代 55「QA覆盖余量收口批」T8/V12——生产行驱动兑付链,替代内联合成行主证)----
|
||||
# 现役断言(上方 action 型负向段)为内联合成行——行值变更零失败;本组读生产 reward 行对象走同一兑付链,
|
||||
# 断言真值依赖生产行值(行值变更即红)。合成行旧断言保留并存(零删除)。
|
||||
|
||||
func _check_enemy_alert_production_row() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:133 (真实入口断言——生产行值驱动)
|
||||
var row: Dictionary = _db.run.reward.row_by_id("enemy_alert")
|
||||
Kit.check("enemy_alert 生产行值:blast 负向 effect=alert power0 weight0(不入池警觉行)〔QA余 V12〕",
|
||||
String(row.get("cell_type", "")) == "blast" and String(row.get("polarity", "")) == "negative"
|
||||
and String(row.get("effect_id", "")) == "alert" and int(row.get("power", -1)) == 0
|
||||
and int(row.get("weight", -1)) == 0)
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(20261108)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
Kit.check("警觉生产行驱动:战斗启动", bool(settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)})))
|
||||
settlement.reward_grant().grant_routed(row, String(row.get("polarity", "negative")),
|
||||
{"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1})
|
||||
var alert_ok := false
|
||||
for eff_a in settlement._status_effects.query_active_side("enemy"):
|
||||
if String(eff_a.effect_id) == String(row.get("effect_id", "")):
|
||||
alert_ok = true
|
||||
Kit.check("警觉兑付随生产行:effect_id=alert 状态在敌侧池(行 effect_id 变更即红)〔QA余 V12〕", alert_ok)
|
||||
|
||||
|
||||
func _check_ap_drain_production_row() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:133 (真实入口断言——生产行值驱动)
|
||||
var row: Dictionary = _db.run.reward.row_by_id("ap_drain")
|
||||
Kit.check("ap_drain 生产行值:action 负向 power=1 weight0〔QA余 V12〕",
|
||||
String(row.get("cell_type", "")) == "action" and String(row.get("polarity", "")) == "negative"
|
||||
and int(row.get("power", -1)) == 1 and int(row.get("weight", -1)) == 0)
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(20261109)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
Kit.check("迟滞生产行驱动:战斗启动", bool(settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)})))
|
||||
var d0: int = settlement.action_economy.pending_ap_delta()
|
||||
settlement.reward_grant().grant_routed(row, String(row.get("polarity", "negative")),
|
||||
{"source_class": "mine_hit", "cell": Vector2i(0, 0), "ri": 1})
|
||||
Kit.check("迟滞兑付随生产行:排队增量==-row.power==-1(行 power 变更即红)〔QA余 V12〕",
|
||||
settlement.action_economy.pending_ap_delta() == d0 - int(row.get("power", 0)))
|
||||
@@ -28,8 +28,9 @@ func _init() -> void:
|
||||
# 套执行序沿源编排(基点 _run_core_smoke 调用序——本组套的相对序)
|
||||
_smoke_variant_placement(1)
|
||||
_smoke_variant_placement(2)
|
||||
_smoke_global_variant()
|
||||
_smoke_global_variant() # 审计 U1 修复恢复(T7 编辑回退分支误删调用——33 条在役断言复归执行)
|
||||
_test_pack_min_from_table()
|
||||
_test_l3_rockfall_minedust_rows()
|
||||
quit(Kit.finish("variant_placement_test"))
|
||||
|
||||
|
||||
@@ -368,4 +369,82 @@ func _test_pack_min_from_table() -> void:
|
||||
found_diff = true
|
||||
break
|
||||
vt.pack_min = saved_min
|
||||
Kit.check("同种子 pack_min=3 vs 1 布置产物互异(下界进 randi_range——改值可观测)〔架构整改 V10〕", found_diff)
|
||||
Kit.check("同种子 pack_min=3 vs 1 布置产物互异(下界进 randi_range——改值可观测)〔架构整改 V10〕", found_diff)
|
||||
|
||||
# ---- G7 L3 变异行(迭代 55「QA覆盖余量收口批」T7/V11——rockfall/mine_dust 第三关布置与效果通道)----
|
||||
# 真实入口=变异布置段(begin_battle L3→deep_underdark 候选池→真实布置);rockfall 效果通道=
|
||||
# 授予段地形批(on_open 兑现 flat_damage);mine_dust=信息型遮蔽解析数据面(board.variant_ids×表行 effect)。
|
||||
|
||||
func _test_l3_rockfall_minedust_rows() -> void:
|
||||
# 判据出处: design/gdd/systems/排雷循环/地形变异.md:63 (L3 布置段门控与效果兑现组)
|
||||
var pool := {}
|
||||
for r in _db.combat.variation.variant_pool(3, "deep_underdark"):
|
||||
pool[String(r.variant_id)] = r
|
||||
Kit.check("L3 deep_underdark 候选池含 rockfall 行(flat_damage 3·on_open)〔QA余 V11〕",
|
||||
pool.has("rockfall") and String((pool["rockfall"] as Dictionary).get("effect", "")) == "flat_damage"
|
||||
and int((pool["rockfall"] as Dictionary).get("effect_value", -1)) == 3)
|
||||
Kit.check("L3 deep_underdark 候选池含 mine_dust 行(hide_number)〔QA余 V11〕",
|
||||
pool.has("mine_dust") and String((pool["mine_dust"] as Dictionary).get("effect", "")) == "hide_number")
|
||||
var rock_seed := -1
|
||||
var dust_seed := -1
|
||||
var rock_settlement = null
|
||||
var dust_settlement = null
|
||||
for s in range(1, 201):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(s)
|
||||
var st = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
if not st.begin_battle({"level_index": 3, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
continue
|
||||
var b = st.minesweeper.current_board()
|
||||
if b == null or String(b.board_type) != "deep_underdark":
|
||||
continue
|
||||
var ids := {}
|
||||
for c in b.variants.keys():
|
||||
for vid in (b.variants[c] as Array):
|
||||
ids[String(vid)] = true
|
||||
if ids.has("rockfall") and rock_seed < 0:
|
||||
rock_seed = s
|
||||
rock_settlement = st
|
||||
elif ids.has("mine_dust") and dust_seed < 0:
|
||||
dust_seed = s
|
||||
dust_settlement = st
|
||||
if rock_seed > 0 and dust_seed > 0:
|
||||
break
|
||||
Kit.check("rockfall 放置可达(L3 真实布置段 variants 命中——200 种子内)〔QA余 V11〕", rock_seed > 0)
|
||||
Kit.check("mine_dust 放置可达(同链)〔QA余 V11〕", dust_seed > 0)
|
||||
# rockfall 效果通道:挖开变异格 → 授予段地形批兑付 flat_damage 3(伤害行 display 落石·值 3)
|
||||
if rock_settlement != null:
|
||||
var rows: Array = []
|
||||
rock_settlement.step_row.connect(func(r): rows.append(r))
|
||||
var b2 = rock_settlement.minesweeper.current_board()
|
||||
var target := Vector2i(-1, -1)
|
||||
for c in b2.variants.keys():
|
||||
if not b2.is_mine(c):
|
||||
target = c
|
||||
break
|
||||
Kit.check("rockfall 变异格在板(可挖)", target != Vector2i(-1, -1))
|
||||
if target != Vector2i(-1, -1):
|
||||
rock_settlement.minesweeper.fx_reveal(target)
|
||||
rock_settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var hit_val := -1
|
||||
var hit_name := ""
|
||||
for r in rows:
|
||||
if String(r.get("type", "")) == "damage" and String(r.get("display_name", "")).contains("落石"):
|
||||
hit_val = int(r.get("value", -1))
|
||||
hit_name = String(r.get("display_name", ""))
|
||||
Kit.check("rockfall 效果通道命中:挖开即伤兑付行值==3(表行 effect_value 驱动)〔QA余 V11〕",
|
||||
hit_val == 3 and hit_name != "")
|
||||
# mine_dust 效果通道:变异格经遮蔽解析数据面命中 hide_number 行(信息型不进结算——呈现读取)
|
||||
if dust_settlement != null:
|
||||
var b3 = dust_settlement.minesweeper.current_board()
|
||||
var masked := false
|
||||
var unmasked_clean := true
|
||||
for c in b3.variants.keys():
|
||||
for vid in (b3.variants[c] as Array):
|
||||
if String(_db.combat.variation.row_by_id(String(vid)).get("effect", "")) == "hide_number":
|
||||
masked = true
|
||||
for c in b3.safe_cells():
|
||||
if (b3.variants.get(c, []) as Array).is_empty() and not (b3.variant_ids(c) as Array).is_empty():
|
||||
unmasked_clean = false
|
||||
Kit.check("mine_dust 效果通道:变异格 hide_number 行命中(遮蔽解析数据面真值)且无变异格零遮蔽〔QA余 V11〕",
|
||||
masked and unmasked_clean)
|
||||
@@ -577,6 +577,7 @@ func _smoke_feats() -> void:
|
||||
offered = true
|
||||
break
|
||||
Kit.check("A6 概率面存在(chance=20% 200 展示内必出现——零触发=概率面失守)", offered)
|
||||
_check_ticket_quota_exhausted(a_t) # A6-补 配额满分支(迭代 55 T3/V4——生产配额值驱动恒 false)
|
||||
a_r.stop()._pending = {"candidates": [{"kind": "relic", "tier": "rare", "feat_id": "x3"}, {"kind": "gp", "tier": "common"}], "payload": {}}
|
||||
Kit.check("relic 兑付:入持有集 + 牌库摘牌(选中踢池——池库存 %d→%d)" % [deck_relic, deck_relic - 1], a_r.stop().pick_curio("relic", "rare", "x3") and (a_r.feats_held() as Array).has("x3") and a_r.deck().pool_stock("relic") == deck_relic - 1)
|
||||
# 断点往返三条(基点 1420–1426:relics 快照/计数器键/余库逐枚还原)按主会话裁决②改判跨系统闭环宿主——
|
||||
@@ -915,3 +916,26 @@ func _fresh_rng(seed_val: int):
|
||||
func _level_active_nodes(level_index: int) -> Array:
|
||||
var gen = NodeMapGenerator.new(_fresh_rng(42))
|
||||
return (gen.generate(_db.run.level_gen_params.row_by_level_index(level_index), 42) as Dictionary).get("nodes", [])
|
||||
|
||||
|
||||
# A6-补 配额满分支(迭代 55「QA覆盖余量收口批」T3/V4):ticket_appeared 达生产配额后恒不再出现——
|
||||
# 生产 feat_ticket_quota==2 读表驱动;保底同置满使 false 只能来自配额分支(先于概率掷点)。
|
||||
# 真实入口链 = node_curio_candidates→draw_candidates("reward_node")→feat_ticket_consider。
|
||||
func _check_ticket_quota_exhausted(a_t) -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:133 (真实入口断言——配额满分支售满后恒 false)
|
||||
Kit.check("生产配额值在役:feat_ticket_quota==2〔QA余 V4〕", int(a_t.db.run.adventure.feat_ticket_quota) == 2)
|
||||
a_t.stop()._deck.ticket_appeared = int(a_t.db.run.adventure.feat_ticket_quota) # 配额满注入(A6 同款测试通道直填)
|
||||
a_t.stop()._deck.ticket_pity = int(a_t.db.run.adventure.feat_ticket_pity_step) # 保底同置满——false 唯一来源=配额分支
|
||||
var rg_q: RandomNumberGenerator = a_t.rng.open_stream("reward_pick")
|
||||
Kit.check("配额满后 consider 恒 false(保底已满仍拒——分支先于概率掷点)〔QA余 V4〕", not a_t.deck().feat_ticket_consider(rg_q))
|
||||
for i in 3:
|
||||
var cands: Array = (a_t.stop().node_curio_candidates().get("candidates", []) as Array)
|
||||
var has_ticket := false
|
||||
for cd in cands:
|
||||
if String((cd as Dictionary).get("kind", "")) == "feat_ticket":
|
||||
has_ticket = true
|
||||
Kit.check("配额满后第 %d 次停点候选零专长凭证(注入链零凭证)〔QA余 V4〕" % (i + 1), not has_ticket)
|
||||
Kit.check("配额满分支零记账(ticket_appeared 停在配额值——不随展示递增)〔QA余 V4〕",
|
||||
int(a_t.stop()._deck.ticket_appeared) == int(a_t.db.run.adventure.feat_ticket_quota))
|
||||
a_t.stop()._deck.ticket_appeared = 0 # 夹具还原(后续段零污染)
|
||||
a_t.stop()._deck.ticket_pity = 0
|
||||
@@ -42,6 +42,10 @@ func _init() -> void:
|
||||
_test_blessing_no_forge_leak()
|
||||
_test_default_axes_pre_key()
|
||||
_test_armor_switch_dual_axes()
|
||||
_check_ks_workshop_row()
|
||||
_check_ks_refresh_row()
|
||||
_check_fork_rows()
|
||||
_check_fork_side_projection()
|
||||
quit(Kit.finish("meta_progression_forge_test"))
|
||||
|
||||
|
||||
@@ -56,7 +60,9 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry
|
||||
"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",
|
||||
"user://smoke_meta_reject.json", "user://smoke_meta_blessing.json", "user://smoke_meta_axesw.json"]
|
||||
"user://smoke_meta_reject.json", "user://smoke_meta_blessing.json", "user://smoke_meta_axesw.json",
|
||||
"user://smoke_meta_ks_workshop.json", "user://smoke_meta_ks_refresh.json",
|
||||
"user://smoke_meta_fork_rows.json", "user://smoke_meta_fork_side.json"]
|
||||
|
||||
|
||||
func _meta_smoke_cleanup() -> void:
|
||||
@@ -852,3 +858,115 @@ func _test_armor_switch_dual_axes() -> void:
|
||||
return
|
||||
Kit.check("甲型双轴:切回 light 复原(AC 10+0)且 heavy 等级保留(Lv1 不丢)〔断言补齐批 V16〕",
|
||||
int(adr2.runner().effective_ac()) == 10 and sv.meta_state.forge_state().levels.get("heavy", 0) == 1)
|
||||
|
||||
|
||||
# ---- G5 天赋树行与投影断言(迭代 55「QA覆盖余量收口批」T5——ks_workshop/ks_refresh/fork 两行/侧投影+spine 读行)----
|
||||
|
||||
func _check_ks_workshop_row() -> void:
|
||||
# 判据出处: design/gdd/systems/跨局循环/天赋树.md:30 (钥匙门行——前置/零价/柜台展开)
|
||||
var row: Dictionary = _db.crossrun.meta_tree.row_by_id("ks_workshop")
|
||||
Kit.check("ks_workshop 行值:keystone 脉络 unlock 行且前置=[sp_hp](脊柱生命第 1 级)〔QA余 V7〕",
|
||||
String(row.get("vein", "")) == "keystone" and String(row.get("kind", "")) == "unlock"
|
||||
and (row.get("requires", []) as Array) == ["sp_hp"])
|
||||
Kit.check("ks_workshop 零价行值:currency=free 且 price=0(初始零购零币可点亮族)〔QA余 V7〕",
|
||||
String(row.get("currency", "")) == "free" and int(row.get("price", -1)) == 0)
|
||||
var sv = _meta_new_save("user://smoke_meta_ks_workshop.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.meta_state._add_keystone(1000)
|
||||
Kit.check("前置未满足不可购(sp_hp 未点亮——钥匙门锁定)", not mp.tree_buy("ks_workshop"))
|
||||
Kit.check("脊柱生命 L1 购买受理(前置行)", mp.tree_buy("sp_hp"))
|
||||
Kit.check("购买前投影 level=0(锁定态行在投影)", _ks_row_level(mp, "ks_workshop") == 0)
|
||||
var bal_pre: int = sv.meta_state.keystone_balance()
|
||||
Kit.check("ks_workshop 购买受理(零价 free)", mp.tree_buy("ks_workshop"))
|
||||
Kit.check("购买后投影 level=1(钥匙门开——柜台展开判据)且零币(余额差分零)",
|
||||
_ks_row_level(mp, "ks_workshop") == 1 and int(sv.meta_state.keystone_balance()) == bal_pre)
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
# 树面投影行 level 读数(_check_ks_workshop_row/_check_ks_refresh_row 共用读数面)
|
||||
func _ks_row_level(mp, node_id: String) -> int:
|
||||
for r in (mp.tree_camp_status()["rows"] as Array):
|
||||
if String((r as Dictionary).get("node_id", "")) == node_id:
|
||||
return int((r as Dictionary).get("level", 0))
|
||||
return -1
|
||||
|
||||
|
||||
func _check_ks_refresh_row() -> void:
|
||||
# 判据出处: design/gdd/systems/跨局循环/天赋树.md:30 (刷新升级行——同账路由与零刷新分支)
|
||||
var row: Dictionary = _db.crossrun.meta_tree.row_by_id("ks_refresh")
|
||||
Kit.check("ks_refresh 行值:rule 行 keystone 币 price=30 cap=2 前置=[ks_workshop]〔QA余 V7〕",
|
||||
String(row.get("kind", "")) == "rule" and String(row.get("currency", "")) == "keystone"
|
||||
and int(row.get("price", -1)) == 30 and int(row.get("cap", -1)) == 2
|
||||
and (row.get("requires", []) as Array) == ["ks_workshop"])
|
||||
var sv = _meta_new_save("user://smoke_meta_ks_refresh.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.meta_state._add_keystone(1000)
|
||||
var bal0: int = sv.meta_state.keystone_balance()
|
||||
Kit.check("ks_refresh 购买受理(tree_buy 同账路由→buy_attune_refresh)", mp.tree_buy("ks_refresh"))
|
||||
Kit.check("同账写读:refresh_level=1(树上+秘宝匣装配段双入口同一账本)", int(sv.meta_state.attune_state().refresh_level) == 1)
|
||||
Kit.check("同账币值:扣 meta 行 L1 keystone_cost=30(非树行 price 面定夺)", int(sv.meta_state.keystone_balance()) == bal0 - 30)
|
||||
Kit.check("树节点账零写入(同账语义——nodes 无 ks_refresh)", int(sv.meta_state.tree_state().nodes.get("ks_refresh", 0)) == 0)
|
||||
Kit.check("第二次购买受理(L2 keystone_cost=60)", mp.tree_buy("ks_refresh") and int(sv.meta_state.attune_state().refresh_level) == 2)
|
||||
var bal2: int = sv.meta_state.keystone_balance()
|
||||
Kit.check("零刷新分支:L2 后再购被拒(attune_refresh_row(3) 空返——升级行族耗尽)〔QA余 V7〕", not mp.tree_buy("ks_refresh"))
|
||||
Kit.check("零刷新分支零消费零升账", int(sv.meta_state.attune_state().refresh_level) == 2 and int(sv.meta_state.keystone_balance()) == bal2)
|
||||
Kit.check("同账投影:树面 ks_refresh 行 level 读 Y 账本(=2)", _ks_row_level(mp, "ks_refresh") == 2)
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
func _check_fork_rows() -> void:
|
||||
# 判据出处: design/gdd/systems/跨局循环/天赋树.md:30 (fork 一节点双态行——行值经购买面驱动)
|
||||
for spec in [{"id": "fork_sense", "group": "fg_sense", "na": "锋", "nb": "守"},
|
||||
{"id": "fork_economy", "group": "fg_economy", "na": "聚", "nb": "散"}]:
|
||||
var row: Dictionary = _db.crossrun.meta_tree.row_by_id(String(spec["id"]))
|
||||
Kit.check("%s 行值:fork 行 price=25 前置=[att_socket1] 组=%s〔QA余 V7〕" % [spec["id"], spec["group"]],
|
||||
String(row.get("kind", "")) == "fork" and int(row.get("price", -1)) == 25
|
||||
and (row.get("requires", []) as Array) == ["att_socket1"]
|
||||
and String(row.get("fork_group", "")) == String(spec["group"]))
|
||||
Kit.check("%s 侧名行值:name_a=%s / name_b=%s〔QA余 V7〕" % [spec["id"], spec["na"], spec["nb"]],
|
||||
String(row.get("name_a", "")) == String(spec["na"]) and String(row.get("name_b", "")) == String(spec["nb"]))
|
||||
var sv = _meta_new_save("user://smoke_meta_fork_rows.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.meta_state._add_keystone(1000)
|
||||
Kit.check("同调位 1 购买受理(fork 前置行——根节点无前置)", mp.tree_buy("att_socket1"))
|
||||
var bal0: int = sv.meta_state.keystone_balance()
|
||||
Kit.check("fork_sense 购买受理(tree_buy_fork 真链——解锁+默认 A 态)",
|
||||
mp.tree_buy_fork("fork_sense") and int(sv.meta_state.tree_state().nodes.get("fork_sense", 0)) == 1
|
||||
and String(sv.meta_state.tree_state().fork_sides.get("fg_sense", "")) == "a")
|
||||
Kit.check("fork_economy 购买受理(同链)",
|
||||
mp.tree_buy_fork("fork_economy") and int(sv.meta_state.tree_state().nodes.get("fork_economy", 0)) == 1
|
||||
and String(sv.meta_state.tree_state().fork_sides.get("fg_economy", "")) == "a")
|
||||
Kit.check("两 fork 行扣币共 50(表行 price 驱动)", int(sv.meta_state.keystone_balance()) == bal0 - 50)
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
func _check_fork_side_projection() -> void:
|
||||
# 判据出处: design/gdd/systems/跨局循环/天赋树.md:30 (组侧投影与脉络行板——真实表行读出)
|
||||
var sv = _meta_new_save("user://smoke_meta_fork_side.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.meta_state._add_keystone(1000)
|
||||
Kit.check("投影夹具:同调位 1+fork_economy 购买受理", mp.tree_buy("att_socket1") and mp.tree_buy_fork("fork_economy"))
|
||||
var fg_entry: Dictionary = {}
|
||||
for g in (mp.tree_camp_status()["fork_groups"] as Array):
|
||||
if String((g as Dictionary).get("group", "")) == "fg_economy":
|
||||
fg_entry = g
|
||||
Kit.check("fg_economy 组投影在册:display_name=财富之道 owned=true active_side=a〔QA余 V7〕",
|
||||
String(fg_entry.get("display_name", "")) == "财富之道" and bool(fg_entry.get("owned", false))
|
||||
and String(fg_entry.get("active_side", "")) == "a")
|
||||
var sides: Array = fg_entry.get("sides", []) as Array
|
||||
Kit.check("侧名投影:A 侧=聚(economy_hoard)/B 侧=散(economy_spend)〔QA余 V7〕",
|
||||
sides.size() == 2 and String((sides[0] as Dictionary).get("display_name", "")) == "聚"
|
||||
and ((sides[0] as Dictionary).get("effects", {}) as Dictionary).has("economy_hoard")
|
||||
and String((sides[1] as Dictionary).get("display_name", "")) == "散"
|
||||
and ((sides[1] as Dictionary).get("effects", {}) as Dictionary).has("economy_spend"))
|
||||
Kit.check("A 态规则键=economy_hoard(聚)", (mp.tree_rules() as Dictionary).has("economy_hoard"))
|
||||
Kit.check("切 b 受理(零币换侧)", mp.tree_toggle_fork("fg_economy", "b"))
|
||||
Kit.check("B 态规则键=economy_spend(散)且 A 键退场〔QA余 V7〕",
|
||||
(mp.tree_rules() as Dictionary).has("economy_spend") and not (mp.tree_rules() as Dictionary).has("economy_hoard"))
|
||||
var spine_names := {}
|
||||
for r in (mp.tree_camp_status()["rows"] as Array):
|
||||
if String((r as Dictionary).get("vein", "")) == "spine":
|
||||
spine_names[String((r as Dictionary).get("node_id", ""))] = String((r as Dictionary).get("display_name", ""))
|
||||
Kit.check("spine 脉络投影读行:sp_hp=生命淬炼/sp_hit=命中淬炼(生产表行名非夹具喂值)〔QA余 V7〕",
|
||||
String(spine_names.get("sp_hp", "")) == "生命淬炼" and String(spine_names.get("sp_hit", "")) == "命中淬炼")
|
||||
_meta_smoke_cleanup()
|
||||
@@ -144,6 +144,7 @@ func _init() -> void:
|
||||
ae6c.request_stop()
|
||||
ae6c.finish_round_stop(125)
|
||||
Kit.check_quiet(ae6c.rap == 3, "储备罐边界:3 AP→3(floor 3.75——不越参数面直改停手结算)")
|
||||
_test_rap_transfer_row_e2e(db) # ⑥-补 生产行 e2e(迭代 55 T2——装配链行值主证,替上文字面量路径作主证并存)
|
||||
|
||||
# ⑦ 巢穴预告(装板后一次报数量——明细行经 step_row 信号收集)
|
||||
var rng7 = load("res://core/rng_service.gd").new(); rng7.set_master_seed(271)
|
||||
@@ -417,3 +418,26 @@ class StubAutoInvRunner:
|
||||
extends RefCounted
|
||||
func query_inventory(_cb: Callable) -> Array:
|
||||
return []
|
||||
|
||||
|
||||
# ⑥-补 储备罐生产行 e2e(迭代 55「QA覆盖余量收口批」T2/V5):装配链行值主证——上文字面量路径(125 直调)
|
||||
# 保留并存;本函数断言全链依赖生产行值:add_item 入库→装配集直注(套件既定调试注入口径)→attune 参数和
|
||||
# ==行值 125→转存兑付按 grant_segment 生产入参形态(装配面读数为入参)兑现。行值变更即红。
|
||||
func _test_rap_transfer_row_e2e(db) -> void:
|
||||
# 判据出处: design/gdd/systems/跨局循环/秘宝.md:246 (秘宝逐件行为——储备罐转存率行值)
|
||||
var inv = load("res://core/treasure_items.gd").new()
|
||||
var got: Dictionary = inv.add_item("reserve_jar", "clear_reward", Vector2i(-1, -1), 0)
|
||||
Kit.check_quiet(not got.is_empty() and String(got.get("item_id", "")) == "reserve_jar",
|
||||
"储备罐 e2e:add_item 入库(真实入口——受理记录 item_id 命中)〔QA余 V5〕")
|
||||
var rng = load("res://core/rng_service.gd").new(); rng.set_master_seed(281)
|
||||
var ob = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
Kit.check_quiet(bool(ob.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp), "attunement": ["reserve_jar"]})),
|
||||
"储备罐 e2e:装配集直注起战〔QA余 V5〕")
|
||||
Kit.check_quiet(int(ob.attune_param_sum("rap_transfer_mult")) == 125,
|
||||
"储备罐 e2e:装配→attune 参数和==生产行值 125(行值主证)〔QA余 V5〕")
|
||||
var ae = ob.action_economy
|
||||
var ap_left: int = int(ae.ap)
|
||||
Kit.check_quiet(bool(ae.request_stop()), "储备罐 e2e:停手记账受理〔QA余 V5〕")
|
||||
ae.finish_round_stop(int(ob.attune_param_sum("rap_transfer_mult"))) # grant_segment 生产入参形态(装配面读数→转存率入参)
|
||||
Kit.check_quiet(int(ae.rap) == ap_left * 125 / 100,
|
||||
"储备罐 e2e:转存增量==行值倍率(%d AP→RAP %d=125%% 整除口径;行值或接线变更即红)〔QA余 V5〕" % [ap_left, int(ae.rap)])
|
||||
@@ -50,6 +50,10 @@ func _init() -> void:
|
||||
_smoke_nine_types_gp_stream()
|
||||
_smoke_adventure_table()
|
||||
_smoke_meta()
|
||||
_check_settlement_pin_values()
|
||||
_check_meta_per_level_pins()
|
||||
_check_curio_cap_segment_pins()
|
||||
_check_reward_row_pins()
|
||||
quit(Kit.finish("table_structure_guard_test"))
|
||||
|
||||
|
||||
@@ -660,3 +664,73 @@ func _test_feat_cap_required() -> void:
|
||||
var cap_v: int = int(some_feat["cap"])
|
||||
Kit.check("V12 cap_ok 在场行直索引读 cap(首行 %s cap=%d——零兜底路径)〔架构整改 V12〕" % [String(some_feat["feat_id"]), cap_v],
|
||||
deck.cap_ok("relic", String(some_feat["feat_id"])) == (int(deck.taken.get("relic:" + String(some_feat["feat_id"]), 0)) < cap_v))
|
||||
|
||||
|
||||
# ---- G2 结算数值钉(迭代 55「QA覆盖余量收口批」T2——equivalence_ref_damage/p_perfect 钉+p_perfect 读数活性)----
|
||||
|
||||
func _check_settlement_pin_values() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:119 (表值钉值——漂移即红)
|
||||
# 判据出处: design/gdd/systems/战斗循环/战斗结算.md:124 (完美第二份兑付读表——接线后该句由声称变属实)
|
||||
var core = _db.combat.settlement.core
|
||||
var roll = _db.combat.settlement.damage_roll
|
||||
Kit.check("equivalence_ref_damage 钉 4(离线审计镜像 tools/clear_dominance_audit.py D_REF 同源——表值漂移即审计口径静默失效)〔QA余 V2〕",
|
||||
int(roll.equivalence_ref_damage) == 4)
|
||||
Kit.check("p_perfect 钉 200(完美兑付载体域 {100,200} 在役档)〔QA余 V2〕", int(core.p_perfect) == 200)
|
||||
# 读数活性断言:注入域内另一档 p_perfect=100 → 完美兑付份数随表值变化(防「接线但弃用」死接线假绿)。
|
||||
# 构造沿 seal_polarity 零兑付段同款(真实编排器;敌人存活首份必兑付——弱形式登记:直调 grant_clear_reward
|
||||
# 构造完美谓词,真值在正常流由封印判定累加)。表值注入限本函数作用域、读后即还原(同进程后续断言免污染)。
|
||||
var old_pp: int = int(core.p_perfect)
|
||||
core.p_perfect = 100
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20261107)
|
||||
var s = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var rows: Array = []
|
||||
s.step_row.connect(func(row): rows.append(row))
|
||||
Kit.check("活性断言构造战斗启动(level_index 1 / depth 4 / boss)", bool(s.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": 80, "hero_max_hp": 80, "node_type": "boss"})))
|
||||
s._facts.perfect_hit_true = 1
|
||||
s._facts.perfect_hit_false = 0
|
||||
s.reward_grant().grant_clear_reward(1, s._facts.board_from_midround, s.perfect_sweep_earned())
|
||||
var dmg := 0
|
||||
for r in rows:
|
||||
if String(r.get("type", "")) == "damage" and String(r.get("source_class", "")) == "clear_reward":
|
||||
dmg += 1
|
||||
Kit.check("p_perfect=100 注入 → 清图奖励单份兑付(clear_reward 伤害行 ×1——第二份不随 perfect 谓词发出)〔QA余 V2〕",
|
||||
dmg == 1)
|
||||
core.p_perfect = old_pp
|
||||
Kit.check("p_perfect 表值还原 200(共享面注入窗口闭合)〔QA余 V2〕", int(_db.combat.settlement.core.p_perfect) == 200)
|
||||
|
||||
|
||||
# ---- G10 换算率钉(迭代 55「QA覆盖余量收口批」T2——per_level_shift_pct 三关档 6/9/1)----
|
||||
|
||||
func _check_meta_per_level_pins() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:119 (表值钉值——漂移即红;离线 verifier 消费端同源)
|
||||
var pins := {"max_hp": 6, "atk": 9, "armor_class": 1}
|
||||
for attr in pins.keys():
|
||||
var row: Dictionary = {}
|
||||
for r in (_db.crossrun.meta.conversion as Array):
|
||||
if String(r.get("attr", "")) == String(attr):
|
||||
row = r
|
||||
Kit.check("per_level_shift_pct 钉:%s==%d(换算率三关档——balance_verifier CONVERSION 同源)〔QA余 V16〕" % [String(attr), int(pins[attr])],
|
||||
not row.is_empty() and int(row.get("per_level_shift_pct", -1)) == int(pins[attr]))
|
||||
|
||||
|
||||
# ---- G3 cap 段七槽钉值(迭代 55「QA覆盖余量收口批」T3——与 gp/attack 同款占位形态)----
|
||||
|
||||
func _check_curio_cap_segment_pins() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:119 (表值钉值——漂移即红)
|
||||
for kind in ["ap", "armor", "heal", "max_hp", "rap", "refresh", "time_sand"]:
|
||||
Kit.check("curio_cap 七槽钉值:%s==99(案②提前退役占位——99=实质无上限,实数归调参批)〔QA余 V3〕" % kind,
|
||||
int(_db.run.adventure.curio_cap(String(kind))) == 99)
|
||||
|
||||
|
||||
# ---- G8 reward stacks 行钉(迭代 55「QA覆盖余量收口批」T8——生产行 stacks 值面,合成行键未读生产行的缺口补钉)----
|
||||
|
||||
func _check_reward_row_pins() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:119 (表值钉值——漂移即红)
|
||||
Kit.check("reward 行扫描面非空(18 行在册——防空表静默绿)", (_db.run.reward.rows as Array).size() > 0)
|
||||
var stacks_drift: Array[String] = []
|
||||
for row in _db.run.reward.rows:
|
||||
if int(row.get("stacks", 1)) != 1:
|
||||
stacks_drift.append(String(row.get("reward_id", "")))
|
||||
Kit.check("reward 生产行 stacks 全 1(schema 缺省值面——合成行 stacks 键的行值主证;行值面变更须自觉换代本钉)〔QA余 V8〕",
|
||||
stacks_drift.is_empty())
|
||||
@@ -37,6 +37,8 @@ func _run() -> void:
|
||||
await _test_background_fallback()
|
||||
await _test_ground_layer_geometry()
|
||||
_test_beams_retired()
|
||||
_test_ap_bar_height_from_table()
|
||||
_test_log_band_settle_height()
|
||||
quit(Kit.finish("stage_background_test"))
|
||||
|
||||
|
||||
@@ -170,3 +172,30 @@ func _test_beams_retired() -> void:
|
||||
var usrc := uf.get_as_text() if uf != null else ""
|
||||
Kit.check("菜单斜光带 beams_rect 仍在(不在退役面)〔舞台背景 V5〕",
|
||||
usrc.contains("beams_rect"))
|
||||
|
||||
|
||||
# ---- G4 布局字段断言(迭代 55「QA覆盖余量收口批」T4——AP 条高/结算带高经生产读点读表装配)----
|
||||
|
||||
func _test_ap_bar_height_from_table() -> void:
|
||||
# 判据出处: design/ux/battle.md:124 (D 区 AP 条高读 ap_bar_h)
|
||||
# 判据出处: design/ux/battle.md:118 (分区几何唯一事实源=battle_layout.tres——文档不复写数值)
|
||||
var layout = load("res://data/tables/battle_layout.tres")
|
||||
var bar = load("res://presentation/views/ap_bar_view.gd").new(layout)
|
||||
root.add_child(bar)
|
||||
Kit.check("AP 条高读表装配:视图几何==ap_bar_h==44(生产读点 ApBarView._init 经 layout.ap_bar_h)〔QA余 V6〕",
|
||||
int(bar.custom_minimum_size.y) == 44 and int(layout.ap_bar_h) == 44)
|
||||
bar.queue_free()
|
||||
|
||||
|
||||
func _test_log_band_settle_height() -> void:
|
||||
# 判据出处: design/ux/battle.md:122 (结算明细带挖掘期收起读 log_band_h_dig、结算期展开读 log_band_h_settle)
|
||||
# 判据出处: design/ux/battle.md:118 (分区几何唯一事实源=battle_layout.tres——文档不复写数值)
|
||||
var layout = load("res://data/tables/battle_layout.tres")
|
||||
var log_view = load("res://presentation/views/battle_log_view.gd").new(null, layout)
|
||||
root.add_child(log_view)
|
||||
Kit.check("结算带缺省 dig 态几何==log_band_h_dig(收起摘要形态)〔QA余 V6〕",
|
||||
int(log_view.custom_minimum_size.y) == int(layout.log_band_h_dig))
|
||||
log_view.set_mode("settle")
|
||||
Kit.check("结算带 settle 态几何==log_band_h_settle==84(生产读点 BattleLogView._init 经 layout.log_band_h_settle)〔QA余 V6〕",
|
||||
int(log_view.custom_minimum_size.y) == 84 and int(layout.log_band_h_settle) == 84)
|
||||
log_view.queue_free()
|
||||
@@ -0,0 +1,169 @@
|
||||
# tests/unit/schema_vocab_violation_test.gd — schema 词表违约词的拒绝路径集中断言面(迭代 55「QA覆盖余量收口批」T1/V1)
|
||||
# 覆盖:十七违约词族七检查——①empty_table 八表空行集注入(board_gen/enemy/level/meta/reward/shop/skill/status_effect)
|
||||
# ②settlement missing_segment/pperfect_oob/segorder_violation 三注入 ③feat+treasure zero_flat 白名单违约注入
|
||||
# ④meta attune_refresh_upgrade keystone_cost 越域注入 ⑤treasure use_phase 出域注入(USE_PHASES=active_dig 枚值面)
|
||||
# ⑥board_gen solver_rules="single" frozen 拒(同钉 single_subset 冻结语义) ⑦meta_tree 彩蛋 4 行(>3)load_bearing 拒
|
||||
# 注入形态沿在役先例(meta_progression_forge_test.gd:581 同款):load(schema).new() 新实例+真行深拷贝注入——
|
||||
# 禁改共享 Resource(共享 Resource 池污染 Gotcha)。违约词经 validate() 诊断面直调取 errs 断言(非致命通道,
|
||||
# 不进 manifest negative 名单——本面非运行期致命路径,不触 test-conventions §5.6)。
|
||||
# 协议照单元侧惯例:extends SceneTree+Kit.check 主通道;--script 模式无 autoload,ConfigDB 显式实例化构造注入。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/schema_vocab_violation_test.gd(expect 0)
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
|
||||
var _db
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (违约词拒绝路径——validate 诊断面直调断言宿主)
|
||||
_db = ConfigDBScript.new()
|
||||
# 表加载失败不中止(collect_only=true——诊断在此展开,红由具体断言落,沿 guard 套件同款)
|
||||
var load_ok: bool = _db.load_tables("res://data/tables", true)
|
||||
if not load_ok:
|
||||
push_error("[schema_vocab_violation_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])
|
||||
_check_empty_tables()
|
||||
_check_settlement_vocab()
|
||||
_check_zero_flat_guard()
|
||||
_check_keystone_cost_domain()
|
||||
_check_use_phase_domain()
|
||||
_check_solver_frozen()
|
||||
_check_meta_tree_load_bearing()
|
||||
quit(Kit.finish("schema_vocab_violation_test"))
|
||||
|
||||
|
||||
# errs 数组按 category(可选 message 子串)命中判定
|
||||
func _has(errs: Array, cat: String, needle: String = "") -> bool:
|
||||
for e in errs:
|
||||
if String(e["category"]) != cat:
|
||||
continue
|
||||
if needle == "" or String(e["message"]).contains(needle):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ---- ① empty_table 八表空行集注入(fresh 新实例 rows/行族全空)----
|
||||
|
||||
func _check_empty_tables() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (空表违约词拒绝路径——validate 诊断面直调取 errs)
|
||||
var schemas := {
|
||||
"board_gen": "res://data/schemas/board_gen_table.gd",
|
||||
"enemy": "res://data/schemas/enemy_table.gd",
|
||||
"level": "res://data/schemas/level_table.gd",
|
||||
"meta": "res://data/schemas/meta_table.gd",
|
||||
"reward": "res://data/schemas/reward_table.gd",
|
||||
"shop": "res://data/schemas/shop_table.gd",
|
||||
"skill": "res://data/schemas/skill_table.gd",
|
||||
"status_effect": "res://data/schemas/status_effect_table.gd",
|
||||
}
|
||||
for table_name in schemas.keys():
|
||||
var t = load(String(schemas[table_name])).new()
|
||||
Kit.check("G1-① 空表注入被拒:%s validate() errs 含 empty_table〔QA余 V1〕" % table_name,
|
||||
_has(t.validate(), "empty_table"))
|
||||
|
||||
|
||||
# ---- ② settlement 三注入(missing_segment/pperfect_oob/segorder_violation)----
|
||||
|
||||
func _check_settlement_vocab() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (settlement 三违约词拒绝路径——validate 诊断面直调取 errs)
|
||||
# missing_segment:fresh 新实例 core/damage_roll 段缺失
|
||||
var seg_missing = load("res://data/schemas/settlement_table.gd").new()
|
||||
Kit.check("G1-② settlement 段缺失注入被拒:errs 含 missing_segment〔QA余 V1〕",
|
||||
_has(seg_missing.validate(), "missing_segment"))
|
||||
# pperfect_oob:真表深拷贝改 p_perfect=150(载体域 {100,200} 外——禁改共享 Resource,deep duplicate 隔离)
|
||||
var st = (_db.combat.settlement as Resource).duplicate(true)
|
||||
st.core.p_perfect = 150
|
||||
Kit.check("G1-② p_perfect=150(域 {100,200} 外)注入被拒:errs 含 pperfect_oob〔QA余 V1〕",
|
||||
_has(st.validate(), "pperfect_oob"))
|
||||
# segorder_violation:真表深拷贝改段序(FROZEN_SEGMENT_ORDER 恒等面外)
|
||||
var st2 = (_db.combat.settlement as Resource).duplicate(true)
|
||||
st2.segment_order = PackedStringArray(["verdict", "buff_debuff", "combat", "decay", "grant"])
|
||||
Kit.check("G1-② segment_order 违例注入被拒:errs 含 segorder_violation〔QA余 V1〕",
|
||||
_has(st2.validate(), "segorder_violation"))
|
||||
|
||||
|
||||
# ---- ③ zero_flat 白名单违约注入(feat+treasure 两表)----
|
||||
|
||||
func _check_zero_flat_guard() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (零裸 flat 白名单违约词拒绝路径——validate 诊断面直调取 errs)
|
||||
# feat:真行深拷贝+白名单外键(ROW_KEYS 外)
|
||||
var feat_row: Dictionary = (_db.crossrun.feats.rows[0] as Dictionary).duplicate(true)
|
||||
feat_row["ghost_flat"] = 1
|
||||
var ft = load("res://data/schemas/feat_table.gd").new()
|
||||
ft.rows = [feat_row]
|
||||
Kit.check("G1-③ feat 行键出白名单注入被拒:errs 含 zero_flat〔QA余 V1〕", _has(ft.validate(), "zero_flat"))
|
||||
# treasure:真行深拷贝+白名单外键(KEY_WHITELIST 外)
|
||||
var tr_row: Dictionary = (_db.crossrun.treasure_items.rows[0] as Dictionary).duplicate(true)
|
||||
tr_row["ghost_flat"] = 1
|
||||
var tt = load("res://data/schemas/treasure_item_table.gd").new()
|
||||
tt.rows = [tr_row]
|
||||
Kit.check("G1-③ treasure 行键出白名单注入被拒:errs 含 zero_flat〔QA余 V1〕", _has(tt.validate(), "zero_flat"))
|
||||
|
||||
|
||||
# ---- ④ meta attune_refresh_upgrade keystone_cost 越域注入 ----
|
||||
|
||||
func _check_keystone_cost_domain() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (keystone_cost 越域拒绝路径——validate 诊断面直调取 errs)
|
||||
var bad: Dictionary = (_db.crossrun.meta.attune_refresh_upgrade[0] as Dictionary).duplicate(true)
|
||||
bad["keystone_cost"] = 1000 # 域 [1,999] 外
|
||||
var mt = load("res://data/schemas/meta_table.gd").new()
|
||||
var bad_family: Array[Dictionary] = [bad] # Array[Dictionary] 严格型——未类型化字面量赋值会 SCRIPT ERROR 中断
|
||||
mt.attune_refresh_upgrade = bad_family
|
||||
Kit.check("G1-④ meta attune_refresh_upgrade keystone_cost=1000 越域注入被拒:errs 含 domain(keystone_cost 越域)〔QA余 V1〕",
|
||||
_has(mt.validate(), "domain", "keystone_cost 越域"))
|
||||
|
||||
|
||||
# ---- ⑤ treasure use_phase 出域注入(覆盖 active_dig 枚值面)----
|
||||
|
||||
func _check_use_phase_domain() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (use_phase 出域拒绝路径——validate 诊断面直调取 errs)
|
||||
var TT = load("res://data/schemas/treasure_item_table.gd")
|
||||
# 枚值面钉:USE_PHASES 单元素 = active_dig(出域注入覆盖的正是该枚值面)
|
||||
Kit.check("G1-⑤ use_phase 枚值面钉:USE_PHASES=[active_dig]〔QA余 V1〕", TT.USE_PHASES == ["active_dig"])
|
||||
var bad: Dictionary = (_db.crossrun.treasure_items.rows[0] as Dictionary).duplicate(true)
|
||||
bad["use_phase"] = "post_clear_x" # 出域
|
||||
var tt = TT.new()
|
||||
tt.rows = [bad]
|
||||
Kit.check("G1-⑤ treasure use_phase 出域注入被拒:errs 含 enum(use_phase 出域)〔QA余 V1〕",
|
||||
_has(tt.validate(), "enum", "use_phase 出域"))
|
||||
|
||||
|
||||
# ---- ⑥ board_gen solver_rules="single" frozen 拒(同钉 single_subset 冻结语义)----
|
||||
|
||||
func _check_solver_frozen() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (solver_rules 冻结语义拒绝路径——validate 诊断面直调取 errs)
|
||||
# 冻结语义正证:真行(single_subset)零 frozen 违例——frozen 只拒 non-single_subset
|
||||
var good: Dictionary = (_db.combat.board_gen.rows[0] as Dictionary).duplicate(true)
|
||||
var bt = load("res://data/schemas/board_gen_table.gd").new()
|
||||
var good_rows: Array[Dictionary] = [good] # Array[Dictionary] 严格型——未类型化字面量赋值会 SCRIPT ERROR 中断
|
||||
bt.rows = good_rows
|
||||
Kit.check("G1-⑥ 冻结语义正证:solver_rules=single_subset 真行零 frozen 违例〔QA余 V1〕",
|
||||
not _has(bt.validate(), "frozen"))
|
||||
# 冻结拒:single 在 SOLVER_RULES 词表内(enum 合法)但被冻结面拒——注入仅命中 frozen 违约词
|
||||
var bad: Dictionary = (_db.combat.board_gen.rows[0] as Dictionary).duplicate(true)
|
||||
bad["solver_rules"] = "single"
|
||||
var bt2 = load("res://data/schemas/board_gen_table.gd").new()
|
||||
var bad_rows: Array[Dictionary] = [bad]
|
||||
bt2.rows = bad_rows
|
||||
Kit.check("G1-⑥ solver_rules=single(enum 合法、冻结面外)注入被拒:errs 含 frozen〔QA余 V1〕",
|
||||
_has(bt2.validate(), "frozen"))
|
||||
|
||||
|
||||
# ---- ⑦ meta_tree 彩蛋 4 行(>3)load_bearing 注入 ----
|
||||
|
||||
func _check_meta_tree_load_bearing() -> void:
|
||||
# 判据出处: design/engineering/test-conventions.md:67 (彩蛋上限违约词拒绝路径——validate 诊断面直调取 errs)
|
||||
var easter: Dictionary = (_db.crossrun.meta_tree.row_by_id("easter_ley") as Dictionary).duplicate(true)
|
||||
var mt = load("res://data/schemas/meta_tree_table.gd").new()
|
||||
var bad_rows: Array = []
|
||||
for i in range(4):
|
||||
var r: Dictionary = easter.duplicate(true)
|
||||
r["node_id"] = "easter_x%d" % i
|
||||
bad_rows.append(r)
|
||||
mt.rows = bad_rows
|
||||
Kit.check("G1-⑦ meta_tree 彩蛋 4 行(>3)注入被拒:errs 含 load_bearing(彩蛋节点 4 > 3)〔QA余 V1〕",
|
||||
_has(mt.validate(), "load_bearing", "彩蛋节点"))
|
||||
@@ -0,0 +1 @@
|
||||
uid://ddk4k0g6tvjh8
|
||||
Reference in new issue
Block a user