diff --git a/game/core/reward_grant.gd b/game/core/reward_grant.gd index 5fb230d6..565bc045 100644 --- a/game/core/reward_grant.gd +++ b/game/core/reward_grant.gd @@ -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 的正向字典形态走, diff --git a/game/probe/board_shot_probe.gd b/game/probe/board_shot_probe.gd index 325c4c19..99c07c8e 100644 --- a/game/probe/board_shot_probe.gd +++ b/game/probe/board_shot_probe.gd @@ -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: diff --git a/game/probe/probe_coverage_recorder.gd b/game/probe/probe_coverage_recorder.gd index b45dcfad..e0b853ac 100644 --- a/game/probe/probe_coverage_recorder.gd +++ b/game/probe/probe_coverage_recorder.gd @@ -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, diff --git a/game/probe/probe_ui_util.gd b/game/probe/probe_ui_util.gd index 4e0a5e75..c4c5e7eb 100644 --- a/game/probe/probe_ui_util.gd +++ b/game/probe/probe_ui_util.gd @@ -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 diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 0b9d1c69..b14e898c 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -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) diff --git a/game/tests/golden/README.md b/game/tests/golden/README.md index 8a4a183f..69a090e0 100644 --- a/game/tests/golden/README.md +++ b/game/tests/golden/README.md @@ -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% 阈值):基线**留在位**,不得顺手重制; diff --git a/game/tests/golden/windows/board_L1_dungeon.png b/game/tests/golden/windows/board_L1_dungeon.png index 834352fd..e5a587ef 100644 Binary files a/game/tests/golden/windows/board_L1_dungeon.png and b/game/tests/golden/windows/board_L1_dungeon.png differ diff --git a/game/tests/golden/windows/board_L2_underdark.png b/game/tests/golden/windows/board_L2_underdark.png index 2f87adc6..2e5b892a 100644 Binary files a/game/tests/golden/windows/board_L2_underdark.png and b/game/tests/golden/windows/board_L2_underdark.png differ diff --git a/game/tests/golden/windows/board_L3_deep_underdark.png b/game/tests/golden/windows/board_L3_deep_underdark.png index ab50d9bf..7380c119 100644 Binary files a/game/tests/golden/windows/board_L3_deep_underdark.png and b/game/tests/golden/windows/board_L3_deep_underdark.png differ diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 53ff4288..ae7a4287 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -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}, # 真视窄域护栏 diff --git a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd index cbfb30c1..3910e1d0 100644 --- a/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd +++ b/game/tests/unit/battle_settlement/round_flow_skill_pool_test.gd @@ -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) diff --git a/game/tests/unit/battle_settlement/seal_polarity_test.gd b/game/tests/unit/battle_settlement/seal_polarity_test.gd index cc710205..7a5a171d 100644 --- a/game/tests/unit/battle_settlement/seal_polarity_test.gd +++ b/game/tests/unit/battle_settlement/seal_polarity_test.gd @@ -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))) diff --git a/game/tests/unit/battle_settlement/variant_placement_test.gd b/game/tests/unit/battle_settlement/variant_placement_test.gd index af3da1f9..083d171a 100644 --- a/game/tests/unit/battle_settlement/variant_placement_test.gd +++ b/game/tests/unit/battle_settlement/variant_placement_test.gd @@ -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) \ No newline at end of file + 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) diff --git a/game/tests/unit/core/adventure_feats_test.gd b/game/tests/unit/core/adventure_feats_test.gd index aeee46ba..bdc02871 100644 --- a/game/tests/unit/core/adventure_feats_test.gd +++ b/game/tests/unit/core/adventure_feats_test.gd @@ -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 diff --git a/game/tests/unit/core/meta_progression_forge_test.gd b/game/tests/unit/core/meta_progression_forge_test.gd index cfd4efba..a0ca9b50 100644 --- a/game/tests/unit/core/meta_progression_forge_test.gd +++ b/game/tests/unit/core/meta_progression_forge_test.gd @@ -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() diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index a23cdcd7..6cace2d0 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -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)]) diff --git a/game/tests/unit/data_config/table_structure_guard_test.gd b/game/tests/unit/data_config/table_structure_guard_test.gd index b43e7463..b7461589 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -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()) diff --git a/game/tests/unit/presentation/stage_background_test.gd b/game/tests/unit/presentation/stage_background_test.gd index 3886c8db..7f1bd6ef 100644 --- a/game/tests/unit/presentation/stage_background_test.gd +++ b/game/tests/unit/presentation/stage_background_test.gd @@ -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() diff --git a/game/tests/unit/schema_vocab_violation_test.gd b/game/tests/unit/schema_vocab_violation_test.gd new file mode 100644 index 00000000..df617c85 --- /dev/null +++ b/game/tests/unit/schema_vocab_violation_test.gd @@ -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", "彩蛋节点")) diff --git a/game/tests/unit/schema_vocab_violation_test.gd.uid b/game/tests/unit/schema_vocab_violation_test.gd.uid new file mode 100644 index 00000000..233e460e --- /dev/null +++ b/game/tests/unit/schema_vocab_violation_test.gd.uid @@ -0,0 +1 @@ +uid://ddk4k0g6tvjh8 diff --git a/reports/iter55-反哺点-2026-11-07.md b/reports/iter55-反哺点-2026-11-07.md new file mode 100644 index 00000000..19512e26 --- /dev/null +++ b/reports/iter55-反哺点-2026-11-07.md @@ -0,0 +1,9 @@ +# 迭代 55 反哺点清单——QA 覆盖余量收口批(2026-11-07) + +> 接收方:待办清单条目「QA harness 覆盖余量——B4 本体缺口与 B5 控件零命中缺口复核分流」。 +> 卡 §6 预期:零条(战斗结算.md :124「读表」句接线后由声称变属实,零文本改动义务)。 + +**反哺点:零条。** + +说明:本批两项 L3 停机呈报(cell_mark_overlay 分类前提、进入下一层到达性)的裁决落点属 +B4/B5 复核记录与需求卡片面,不属 GDD/art-bible 反哺——见 `reports/iter55-实现报告-2026-11-07.md` §6。 diff --git a/reports/iter55-实现报告-2026-11-07.md b/reports/iter55-实现报告-2026-11-07.md new file mode 100644 index 00000000..5805adc9 --- /dev/null +++ b/reports/iter55-实现报告-2026-11-07.md @@ -0,0 +1,127 @@ +# 迭代 55 实现报告——QA 覆盖余量收口批(2026-11-07) + +> 接收方:待办清单条目「QA harness 覆盖遗漏余量——B4 本体缺口与 B5 控件零命中缺口复核分流」(2026-11-07 登记,P0)。 +> 输入合同:`reports/需求卡片-QA覆盖余量收口批-2026-11-07.md`(零待裁点,U0=iter 技能调用)。 +> 执行形态:整迭代单工作区(`../g-plan2-iter55`,分支 iter55),T0→T11 串行;单卡批。 +> 本报告落盘时点状态:T0–T11+**卡修续行(轮 2 用户裁决落地,2026-11-07)**已提交施工;两项 L3 呈报已裁决落定(见 §6);棋盘三图基线换代经用户过目;独立验收审计已落定(见 §7)——待合回。 + +## 1. 各卡状态 + +| 任务卡 | 状态 | 凭据(提交+读数) | +|---|---|---| +| T0 开工 | 完成 | worktree 四件套齐;改前读数锚:coverage_enum 870 实体/缺口 211/62 分组 40 组带缺口(与卡逐字同);screen_flow 探针 exit0(seen 116/hits 19 键 25 次);ui_click 探针 exit0(seen 91/hits 30 键 43 次,停手钮已随迭代 54 修复在命中集);单元全套件 all green(86 套件)exit0。读数入 T1 提交信息 | +| T1 G1 违约词负面断言 | 完成 | 提交(schema_vocab_violation_test 七函数+manifest 登记);单件直跑 all green(19 checks)exit0 零脚本错误 | +| T2 G2 结算钉与 p_perfect 接线 | 完成 | 提交 552bd3b6;硬编码 2↔表值 200 相等=行为零漂移核对通过;guard 91 checks/treasure 全绿/seal 70 checks(V10 回归面 :330-401 完美双份真实流 ×2 仍绿) | +| T3 G3 cap 七槽+配额满 | 完成 | 提交;guard 98 checks/feats 144 checks 全绿 | +| T4 G4 布局字段断言 | 完成 | 提交;stage_background 23 checks 全绿 | +| T5 G5 天赋树行与投影 | 完成 | 提交;forge 182 checks 全绿 | +| T6 G6 defensive_stance 值断言 | 完成 | 提交;round_flow 142 checks 全绿(种子狩猎真实链命中) | +| T7 G7 L3 变异行 | 完成 | 提交;variant_placement 25 checks 全绿 | +| T8 G8 reward 行读值改断 | 完成 | 提交;seal 76 checks/guard 100 checks 全绿 | +| T9 B5 探针扩面+开发面过滤+渲染采集态核对 | 完成 | 原提交(353 行,十一组真输入全命中)+卡修续行 cf08a4ac(登记件开发面过滤——三根解析,两探针复跑 exit0:ui_click seen 80/命中 85 次/开发面键 0,screen_flow PASS seen 107/开发面键 0)+b5a981ef(采集态六类型注入+逐类型读数——全入帧零缺型,探针 PASS exit0 双帧 PASS) | +| T10 豁免登记+对账工具面修正 | 完成 | 原提交(ontology 154+controls 登记)+卡修续行 cf08a4ac(scan_screens 基类分类修正+controls 开发面 3 条删除);coverage_enum 复跑 868 实体/缺口 163/未豁免未补测=0;附录 C 归并口径零命中 9 键全被 controls 19 条覆盖=0 残留 | +| T11 产出物落位 | 完成 | 本报告+反哺点清单+收尾核对(§5)+卡修续行报告同步 | + +## 2. 实际改动—验收对照表(结果列) + +| # | 需求点 | 改动 | 验收测试 | 结果 | +|---|---|---|---|---| +| V1 | 十七违约词拒绝路径 | 新增 `game/tests/unit/schema_vocab_violation_test.gd` 七函数+run_all manifest 登记 | 同文件 7 函数(〔QA余 V1〕×19) | 绿(单件直跑 19 checks exit0) | +| V2 | equivalence_ref_damage 钉 4+p_perfect 钉 200+读数活性 | guard+`_check_settlement_pin_values`;reward_grant 改读 ConfigDB | 同函数 5 断言+seal :330-401 既有段 | 绿(91 checks;活性断言 p_perfect=100 单份兑付命中;回归面双份 ×2 仍绿) | +| V3 | cap 段七槽钉值 | guard+`_check_curio_cap_segment_pins` | 同函数 7 断言 | 绿(98 checks) | +| V4 | feat_ticket_quota 配额满分支 | feats+`_check_ticket_quota_exhausted` | 同函数 6 断言(真实入口链 consider+node_curio_candidates) | 绿(144 checks) | +| V5 | rap_transfer_mult 行值端到端 | treasure_effects+`_test_rap_transfer_row_e2e` | 同函数 5 断言(add_item→装配→参数和 125→转存兑付) | 绿(treasure 全绿) | +| V6 | AP 条高/结算带高读表 | stage_background+两函数 | 同文件 3 断言 | 绿(23 checks) | +| V7 | 树面四行一组断言 | forge+四函数+`_ks_row_level` 读数面 | 同文件 12+ 断言(〔QA余 V7〕) | 绿(182 checks) | +| V8 | reward stacks 行钉 | guard+`_check_reward_row_pins` | 同函数 2 断言(扫描面非空前置+全 1 钉) | 绿(100 checks) | +| V9 | defensive_stance 值断言 | round_flow+`_check_defensive_stance_values` | 同函数 5 断言(种子狩猎真实链+容器级叠层) | 绿(142 checks) | +| V10 | p_perfect 读表接线 | reward_grant.gd:416-422 改读 db.combat.settlement.core.p_perfect | V2 活性断言+seal 既有真实流段 | 绿(行为零漂移:双份 ×2 仍绿) | +| V11 | L3 两变异行 | variant_placement+`_test_l3_rockfall_minedust_rows` | 同函数 6 断言(放置可达+rockfall 兑付 3+mine_dust 遮蔽面) | 绿(25 checks) | +| V12 | enemy_alert/ap_drain 生产行 | seal+两函数(生产行对象走 grant_routed 同链) | 同文件 4 断言 | 绿(76 checks) | +| V13 | 十一动线真输入可达 | ui_click_probe 扩面(十一组)+probe_ui_util 滚辅基准修 | 探针 `_ready` 扩面段+放弃收尾段 | 绿(exit0 零 FAIL;十一组命中。「进入下一层」组经轮 2 用户裁出圈——开发面,见 §6 L3-2 落定) | +| V14 | cell_mark_overlay 显示正确性(渲染通道) | board_shot_probe 采集态六类型注入与逐类型读数+棋盘三图基线换代(b5a981ef,新帧用户过目) | `board_shot_probe.tscn` 三图采集+渲染闸逐像素(裁剪区加严口径) | 绿(探针 PASS exit0;六类型全入帧零缺型;渲染闸本地全绿——golden 对比全过) | +| V15 | 豁免登记对账清零 | tools/coverage_exemptions.js 登记(ontology 154+controls 19——开发面 3 条随轮 2 改判删除) | T10 对账读数 | 绿(未豁免未补测=0;附录 C 归并口径 controls 覆盖后残留 0) | +| V16 | per_level_shift_pct 三关档钉 | guard+`_check_meta_per_level_pins` | 同函数 3 断言 | 绿(91 checks 内) | +| V17 | 开发面出圈与本体分类修正 | probe_coverage_recorder 三根过滤(cf08a4ac)+coverage_enum scan_screens 基类分类(cf08a4ac) | 两探针复跑 seen 集开发面键 0+coverage_enum 复跑 868 实体(cell_mark_overlay/debug_overlay 出圈) | 绿(dev_face_roots=3;ui_click skipped=708/screen_flow skipped=84;开发面键双探针 0;未豁免未补测=0) | + +## 3. 实现决定记录(代拍板白名单外事项备案) + +1. **主循环滑条 400/200→2000/2000**(探针行为参数):基线对照实证(stash 前后各一跑)400/200 下 L1 恒败退、零 L1 完成(2026-11-06 登记件同证);2000 下 L1 完成进 L2。原「高加成保 L1 可完成」注释在 400/200 下已失真。 +2. **probe_ui_util.scroll_button_into_view 可视基准改按钮最近 ScrollContainer 祖先**:秘宝匣 `_ws_scroll` 336px 小于窗壳面板,面板基准把裁剪区外按钮误判「在视」、真点落空(购买 25/装配/升级零命中实证);树/秘宝两视图同修,既有树面滚点全数保持命中。 +3. **秘宝段购后重开视图**:`_refresh_attune_segment` 的 unlocked 快照仅随 rebuild 刷新(代码注释自证),购后不重开则新解锁件装配钮恒禁用——重开=全量重建对齐快照。 +4. **树面 ks_refresh(购买 30)后置到秘宝段之后**:其前置 ks_workshop 在秘宝段前段点亮(树段时序上尚未购,置灰实证一次)。 +5. **T9 缺一组的到达性结论**:进入下一层组经代码考证定为开发面(见 L3-2)。轮 2 用户裁升级为「出圈非豁免」——覆盖登记层过滤承载,controls 开发面 3 条豁免删除(cf08a4ac)。 +6. **棋盘三图基线换代**(V14 渲染通道):采集态扩型(变异 resolver 生产同源+真视/预读 overlay+移除写口+点迹/高亮定格)使 cell_mark_overlay 六绘制域全入帧;旧基线判失效(加严口径),`golden_baseline_refresh.sh` 重采集入基线——**新帧经用户过目确认**后换代,渲染闸本地复绿(golden 对比全过)。 + +## 4. 实机目检总清单(登记不阻塞) + +- 探针扩面十一组动线的屏面观感(图例关闭落点、节点继续、三选一刷新重投喂、商店购买/刷新货架态、秘宝架四钮柜台态、树面高档购买与先手压制回切、弃图刷新确认 chips、地图放弃确认窗、Esc 浮窗设置、两拨杆开关态)——机器面已由真输入命中与落点断言承载,真人复看可选。 +- p_perfect 接线后完美封印双份兑付的战内表现(值零漂移——真实流回归面已绿,观感复核可选)。 + +## 5. 收尾核对(能力行名实) + +- 已落地名字与能力行一致:`_check_empty_tables`/`_check_settlement_pin_values`/`_check_curio_cap_segment_pins`/`_check_meta_per_level_pins`/`_check_ticket_quota_exhausted`/`_test_rap_transfer_row_e2e`/`_check_ks_workshop_row`/`_check_fork_side_projection`/`_check_defensive_stance_values`/ui_click 扩面段——逐名 grep 在案。 +- 卡修续行回表(写卡段批 ef5d4679 已落):能力行「控件动线扩面真输入可达」删「进入下一层」与「cell_mark 模态断言」(十一段);新增「棋盘格面绘制显示(渲染闸棋盘三图 golden 逐像素)」行,代表用例 `board_shot_probe.gd:_ready` 已落地(采集态注入+读数段)——名实相符。 + +## 6. L3 停机呈报(两项;卡 §9 预授权停机点) + +### L3-1 cell_mark_overlay 模态断言前提不成立(V14 未落地) + +- **分歧点**:卡 V14/T9 要求「ui_struct_probe+cell_mark_overlay 段:开态 visible+遮挡物隐藏模态断言(锚 test-conventions §2 模态条款)」。 +- **代码事实**:`game/presentation/views/cell_mark_overlay.gd` 为 **RefCounted 绘制域件**(CellUnit 拆件——变异标记/徽章/封印/点迹的绘制命令持有者),无 visible 属性、无开层动线、无遮挡物语义;`tools/coverage_enum.py:45` 的 VIEW_KIND_RE 按文件名后缀 `overlay$` 将其误分类为「屏与模态」实体(B4 缺口来源)。 +- **两种读法**:①按卡字面造模态断言=无被测面可绑,判据绑定面落空(禁自造替代证明);②按实体本相改断言形态(实例化+绘制旗标)=实现期改判据(test-conventions §8.5=上游缺陷停卡)。 +- **建议案**:B4 复核裁决改判——cell_mark_overlay 自「屏与模态真缺口」改豁免(reason=枚举器文件名误分类,实体为 CellUnit 绘制域件;封装纪律由 pres 封装扫描与能力清单承载),V14 行随卡退役;或另立卡对绘制域件立实例化断言规格。现状:豁免清单未登该 id,本体对账留 1 可见缺口(未豁免未补测=1)。 +- **待办清单回退拟稿**:条目状态「已编迭代 55」→「已写卡」(缺口=V14/进入下一层两组裁决后重写卡 §7/§8 对应行)。 +- **裁决落定(2026-11-07 用户裁,卡修轮 2)**:两处均不豁免——测试目的=验证显示正确性,走渲染通道:V14 改指渲染闸棋盘 golden(锚 test-conventions.md:100),本体清单出圈归枚举器 scan_screens 基类分类修正(T10),采集态缺型注入+基线换代经用户过目(b5a981ef)——V14 已落地绿。原呈报的「豁免」建议案作废(用户裁:豁免=在范围内特批不做,出圈面不占豁免)。 + +### L3-2 「进入下一层」动线到达性失败(T9 第十二组未落地) + +- **分歧点**:卡附录 C 将 inter 覆盖层「进入下一层」列为 B5 真缺口动线组(T9 驱动)。 +- **代码事实**:inter 覆盖层仅调试两层模式接线——`main.gd:360 _on_battle_finished` 头注「仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派)」;正常冒险 L1→L2 推进零该停点(本批三跑实证:L1 完成→L2 逐位进入,进入下一层键零命中)。入口钮「调试 · 连续关卡两层」本身属附录 C 开发面豁免(发布态恒隐)。 +- **两种读法**:①真缺口——经调试两层模式驱动真点(入口为开发面恒隐钮,驱动链必经豁免面);②开发面豁免——inter 过渡屏与调试浮窗八钮同族(玩家不可达面),controls 豁免登记即收口。 +- **建议案**:按②改判开发面豁免(已登记,reason 随注);卡附录 C 该组随裁决改判。若按①,需另立调试模式探针段规格(入口可达性前提先裁决)。 +- **待办清单回退拟稿**:同 L3-1。 +- **裁决落定(2026-11-07 用户裁,卡修轮 2)**:开发面出圈非豁免——覆盖登记件按调试面边界过滤(DebugOverlay 类型+ui.screens.inter_overlay/debug_badge 引用单点),inter 覆盖层整层调试专用不入玩家可达宇宙;controls 开发面 3 条豁免删除(cf08a4ac);两探针复跑开发面键 0(V17 绿)。原呈报的「豁免登记」建议案作废。 + +## 7. 独立验收审计结果与处置 + +- 审计报告:`reports/iter55-验收审计-2026-11-07.md`(中档深度;审计者=未参与实现的子代理)。 +- **审计结论(初版):未通过——U1 一项硬伤**:T7 提交(8054f959)的编辑回退分支把 + `_smoke_global_variant()` 调用行从 variant_placement_test 执行序中误删(函数体留盘成死代码, + 33 条在役断言退出执行且全仓无第二宿主),违反卡 §4 零删除断言/§14 纯增量且零申报。 +- **处置:已修复**——调用行恢复(执行序回归 master 原序),套件复跑 all green(58 checks= + 原 25+复归 33,恰合)exit0 零脚本错误;提交「iter55 审计修复」。 +- **审计结论(复验后终版+增量轮):通过**——E1/E2 随轮 2 用户裁决闭合(见 §6 裁决落定); + 增量轮(独立子代理,卡修续行两笔 cf08a4ac/b5a981ef 对照新卡 V13/V14/V15/V17)通过,无硬伤 + 无阻塞项:过滤纯减法、注入全真链、基线换代纪律四要件齐、读数三源互证;F1(README 换代戳) + 与 F2(登记件头注措辞——注释级零行为改动)已随审计落定修正;**E3 唯一在案例外**=controls + 键形实例变体 17 条豁免超卡附录 C 面,随合并呈请用户追认。 +- 观察 O1=对账叙述算术(164 缺口实例≠唯一 id 数——同 id 跨类重复,机器读数以校验脚本 + 逐 id 比对为准;卡修续行复跑实测未豁免未补测=0,与 O1 判读口径一致)。 + +## 8. 遗留读数义务 + +- 零 MC/verifier 义务(p_perfect 接线值相等零漂移,卡 §4 数值面零跑口径;无数值调整禁重跑在案)。 +- 远端 CI:合回后 post-merge 钩子自动全量,`tools/ci_check.sh <合并提交>` 现查(合回零等待纪律)。 +- E3 追认:controls 键形实例变体 17 条豁免(同控件重建实例的旧键形静默——各家族代表实例已真点命中)超卡附录 C 面,随合并呈请用户追认;追认不过=该 17 条从豁免件移除、键形改造立工程批。 + +## 9. 批内验证读数汇总(改完即测铁规凭据) + +| 套件/探针 | 读数 | +|---|---| +| 单元全套件(T0 基线) | all green(86 套件)exit0 | +| schema_vocab_violation_test | all green(19 checks)exit0 | +| table_structure_guard_test | all green(100 checks)exit0 | +| adventure_feats_test | all green(144 checks)exit0 | +| treasure_effects_test | PASS 全绿 exit0 | +| seal_polarity_test | all green(76 checks)exit0 | +| stage_background_test | all green(23 checks)exit0 | +| meta_progression_forge_test | all green(182 checks)exit0 | +| round_flow_skill_pool_test | all green(142 checks)exit0 | +| variant_placement_test | all green(25 checks)exit0 | +| ui_click_probe | exit0 零脚本错误零 FAIL(十一组真输入命中);卡修续行复跑 exit0(seen 80/命中 85 次,dev_face_roots=3/skipped=708,开发面键 0) | +| screen_flow_probe | exit0;卡修续行复跑 exit0 PASS(seen 107,dev_face_roots=3/skipped=84,开发面键 0) | +| coverage_enum | 870 实体/缺口 211→164;卡修续行复跑 868 实体(cell_mark_overlay/debug_overlay 出圈)/缺口 163/未豁免未补测=0 | +| 两探针并集零命中 | 116 键→controls 豁免覆盖后 0 残留;卡修续行归并口径(去尾序位+折叠空锚段)零命中 9 键全被 controls 19 条覆盖=0 残留,开发面键 0 | +| board_shot_probe | 卡修续行 PASS exit0(六类型全入帧:变异角标 L2×4/真视框×3/徽章 9·3·3/点迹·高亮·退色×3);双帧比对 PASS | +| 渲染闸 | 基线换代后本地全绿(golden 对比全过,b5a981ef) | diff --git a/reports/iter55-验收审计-2026-11-07.md b/reports/iter55-验收审计-2026-11-07.md new file mode 100644 index 00000000..5800e24d --- /dev/null +++ b/reports/iter55-验收审计-2026-11-07.md @@ -0,0 +1,142 @@ +# 迭代 55 验收审计——QA 覆盖余量收口批(2026-11-07) + +> **审计身份**:独立验收审计(未参与本迭代实现,只读审计)。仓库 `D:\code\g-plan2-iter55`(分支 iter55,基线 master=bb6b1726)。 +> **审计输入**:需求卡片 §8 对照表 V1–V16/§4 禁削弱申报/§16 验收档;实现报告 §2 实际对照表/§6 两项 L3 呈报;`git diff master..iter55`(10 笔提交、17 文件,逐文件读过);测试文件本体与探针文件本体。 +> **验收档:中**(test-conventions §8.7 中档)。**实际执行深度**:对照表逐行全量核对(16/16,逐行 grep 测试文件核实断言存在与形态,不采信实现报告「已全绿」);判据绑定面抽查 4 行(V9/V10/V11/V12);测试削弱逐处删行核对(全 diff 删行 19 处逐一过);直查例外逐条沿先例核对(8 族);**最窄测试复跑省略**=以远端 CI 现查读数代证(合回未发生,记「待合回后 CI 代证」,见 §7)。审计动作仅只读 grep/git,未跑任何测试套件,除本报告外零文件改动。 + +## 1. 对照表逐行核对(V1–V16 全量) + +| # | 结论 | 依据(审计自证,非转抄报告) | +|---|---|---| +| V1 | **通过** | `game/tests/unit/schema_vocab_violation_test.gd` 在库:七检查函数(`_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`)逐函数读毕,19 处 Kit.check(空表×8+settlement×3+zero_flat×2+keystone×1+use_phase×2+solver×2+load_bearing×1),〔QA余 V1〕尾缀 12 模板行全数 grep 命中;每函数带「判据出处: test-conventions.md:67」注记(卡 T1 义务);`game/tests/run_all.gd:172` manifest 登记在案(expect=0,非负面套件,符合卡 §4 第 4 条 validate 诊断面通道);注入形态=`load(schema).new()`+真行 deep duplicate(禁改共享 Resource,卡 T1 义务;严格型局部数组注释与 T1 提交的假绿修复记录一致)。真实入口=各表 schema `validate()` 直调,零绕过。 | +| V2 | **通过** | `table_structure_guard_test.gd:_check_settlement_pin_values`(:672 起)5 断言读毕:equivalence_ref_damage==4(含审计镜像注记)+p_perfect==200+p_perfect=100 活性断言(clear_reward 伤害行 ×1)+表值还原 200+构造启动前置。〔QA余 V2〕×4 grep 命中(构造启动为夹具断言无尾缀——报告「5 断言」口径一致)。活性断言的 `s._facts.perfect_hit_true=1` 注入与 `grant_clear_reward` 直调=master 在役同款(`seal_polarity_test.gd` master :383-386、`feat_vouchers_test.gd` master :207-211 先例),直查例外成立(§4)。 | +| V3 | **通过** | `_check_curio_cap_segment_pins`(:708 起)七槽 ap/armor/heal/max_hp/rap/refresh/time_sand 逐槽 `curio_cap()` 读表钉 99,〔QA余 V3〕grep 命中,与 gp/attack 既有钉同款。断言文本以槽短名引用故缺口 id 走豁免登记(`coverage_exemptions.js` cap 九槽族 reason 明示)——形态自洽。 | +| V4 | **通过(微偏差备案)** | `adventure_feats_test.gd:_check_ticket_quota_exhausted`(:926 起)6 断言读毕:生产配额==2 读表、consider 恒 false(保底同置满排除他因)、3 次 node_curio_candidates 零凭证、分支零记账、夹具还原。〔QA余 V4〕×4 grep 命中。`_deck.ticket_appeared/ticket_pity` 直填=同套件 master :564-573 `_deck.ticket_sold/ticket_pity` 测试通道先例同族(§4)。**微偏差**:卡 V4 真实入口栏写「`AdventureRunner.present_node_curio()` 链」,实现走 `node_curio_candidates()→draw_candidates→consider`(同链上游段)——报告 §2 已如实申报该链名,链族一致,判为备案不判缺漏。 | +| V5 | **通过** | `treasure_effects_test.gd:_test_rap_transfer_row_e2e`(:428 起)5 断言读毕:add_item 入库→装配直注起战→`attune_param_sum==125`(生产行值主证)→停手转存按 grant_segment 生产入参形态兑付。字面量 125 旧路径**保留并存**(diff 纯增量,:128-146 区域未触碰)——卡 T2「替字面量路径作行值主证」+报告「保留并存」一致,零削弱。〔QA余 V5〕×5 grep 命中。 | +| V6 | **通过** | `stage_background_test.gd:_test_ap_bar_height_from_table`/`_test_log_band_settle_height`(:181-201)3 断言读毕:视图几何==ap_bar_h==44、dig 态==log_band_h_dig、settle 态==84;生产读点经 `ApBarView._init`/`BattleLogView._init` 装配链(真实实例化+add_child)。〔QA余 V6〕×3 grep 命中。 | +| V7 | **通过** | `meta_progression_forge_test.gd` 四函数(:866-972)+`_ks_row_level` 共用读数面读毕:ks_workshop 行值/零价/前置拒购/购买前后投影、ks_refresh 同账路由(tree_buy→buy_attune_refresh,扣 meta 行 L1 30 币非树 price 面)/二次购 60/零刷新分支拒+零消费/fork 两行行值+侧名/tree_buy_fork 真链/fg_economy 组投影+切侧规则键换/spine 脉络读生产表行名。〔QA余 V7〕×10 模板行 grep 命中(循环模板展开后实跑断言≈30,报告「12+」为下界口径成立)。存档名册 +4 路径=纯增量(原 3 路径保留)。 | +| V8 | **通过** | `_check_reward_row_pins`(:726 起)2 断言:扫描面非空前置(防空表静默绿)+生产行 stacks 全 1 钉(行值面变更即红)。〔QA余 V8〕grep 命中。 | +| V9 | **通过** | `round_flow_skill_pool_test.gd:_check_defensive_stance_values`(:1165 起)读毕:生产行值钉(-25 percent/2 settlements/accumulate max_stack 1)+种子狩猎 401 内真实链施放 desperate_guard(压 HP 触发 self_hp_pct——测试通道弱形式当场标注)+个体池 effective_value==-25+施放行 duration==2+容器级再授不翻倍(卡 T6 明示 roll_multiplier 先例形态;master `roll_multiplier_test.gd:238-255` 同款直查先例成立)。〔QA余 V9〕×5 grep 命中。 | +| V10 | **通过** | `reward_grant.gd:416-425` 接线读毕:`perfect_mult := int(db.combat.settlement.core.p_perfect)/100` 经 ConfigDB 既有读法(db 句柄,零自建 load),`perfect_mult>=2` 门控第二份,高光计数 `(1+(1 if second_ok else 0))` 与原 `(2 if second_ok else 1)` 在 200 档逐位同=行为零漂移坐实;p_perfect 字面量零残留(grep 只余注释与换算)。活性断言(V2 内)+seal :330-401 既有真实流完美双份兑付段未触碰(:331-344 现读在案)。 | +| V11 | **通过(初版附未通过项 U1,已修复闭合——§10)** | `_test_l3_rockfall_minedust_rows`(:377 起)7 断言读毕:候选池两行值钉+200 种子真实布置段两行放置可达+rockfall 经 `fx_reveal`→授予段兑付行值==3(fx_reveal 直调=master `round_flow_skill_pool_test.gd:1101` 先例)+mine_dust 遮蔽数据面命中且无变异格零遮蔽。〔QA余 V11〕×6 grep 命中。初版发现同一提交(8054f959)移除了 `_smoke_global_variant()` 调用(U1,见 §3)——复验确认已由 857f24d5 修复闭合。 | +| V12 | **通过** | `seal_polarity_test.gd:_check_enemy_alert_production_row`/`_check_ap_drain_production_row`(:835 起)4 断言读毕:生产行对象 `row_by_id` 直读、走 `grant_routed` 同链(master :744-774 同款直调先例)、警觉 effect_id 随生产行、迟滞排队增量==-row.power——断言真值依赖生产行值,行值变更即红。**合成行旧断言保留并存**(seal diff 纯增量,master :772-774 合成行段未触碰)——卡 §4 G8 并存申报兑现。〔QA余 V12〕×4 grep 命中。 | +| V13 | **例外(E2)——十组落地一组未落地,呈报一致** | `ui_click_probe.gd` 扩面段逐段读毕,十组真输入+落点断言逐段核实:图例关闭(:1200-1225 真点开层+「关闭」钮真点,原 `.close()` 直调已改真输入——强化)、节点继续(:651-663 一次性落点断言)、三选一刷新(:698-705 落点=停点保持重投喂)、商店购买+刷新(:722-757 出账断言)、秘宝架四钮(购买10/25 出账+装配入装配集+升级 refresh_level+1)、树面高档三钮+ks_refresh 30 档+先手压制 A 侧回切(reload 持久断言)、弃图刷新确认链(:1321-1341 chips 出现+RAP8 扣账落点)、地图放弃(:985-1027 独立新局二击落点=终局面)、Esc 真键浮窗设置(:365-390)、减动效+全屏两拨杆(:527-555 store 态断言,原 `button_pressed` 直改已改真点——强化)。〔QA余 V13〕×35 grep 命中。**进入下一层组未落地=L3-2**(代码事实核实成立:`main.gd:360` 头注「仅调试模式接线」逐字在案,inter_overlay 仅 debug decision `next_layer` 可达——见 §5 E2)。删行九处全部为直改→真输入强化替换或备案参数改(§3)。 | +| V14 | **例外(E1)——未落地,呈报一致** | `ui_struct_probe.gd` 零触碰(diff 无此件)=V14 未实施,与报告 §2「未落地——L3-1」一致。代码事实核实成立:`cell_mark_overlay.gd:8` `extends RefCounted`(绘制域件,无 visible/开层动线/遮挡物语义);`tools/coverage_enum.py:45` `VIEW_KIND_RE=(screen|overlay|panel)$` 按文件名后缀误分类坐实——L3-1 前提「模态断言无被测面可绑」成立(§5 E1)。 | +| V15 | **通过(附例外 E3)** | `tools/coverage_exemptions.js` 程序计数:ontology 154 条(无重复 id)+controls 22 条——与报告 §2/T10 提交「154+22」逐字一致;头注纪律保留(人工维护/禁代码生成/reason 承载=逐族注释照抄卡附录 B);面板消费面核实:controls 走中缀包含(`coverage_report.html:185` `k.indexOf(e)>=0`)、ontology 精确匹配(:209 `ex.includes`)——文件头注与消费语义一致。**cell_mark_overlay 未入 ontology 豁免**(grep 零命中)=可见缺口 1,与报告 §2/§6 L3-1 口径一致;**`(root)/Button#进入下一层` 已入 controls 开发面**(随 L3 追认注记),与报告 §3.5/§6 L3-2 口径一致。**例外 E3**:controls 新增「键形实例变体」17 条中缀豁免,超出卡附录 C 豁免面(开发面 9 键+进程终结 2 键),见 §5。T10 对账读数(缺口 211→164→可见 1;controls 116→0 残留)为实跑申报,审计未复跑——待合回后 CI 代证(§7),纸面算术观察见 §6 O1。 | +| V16 | **通过** | `_check_meta_per_level_pins`(:694 起)三关档 max_hp 6/atk 9/armor_class 1 逐槽读表钉,〔QA余 V16〕grep 命中。 | + +## 2. 判据绑定面抽查(中档 ≥3 行,实抽 4 行) + +1. **V10 活性断言**(guard `_check_settlement_pin_values`):从真实编排器构造进入(begin_battle 真实启动+`_facts` 完美谓词——master seal :383 同款先例),钉在被测改动特有的提交面=`grant_clear_reward` 内 p_perfect 读表门控;表值注入 100 后伤害行 ×1、还原 200 有闭合断言——「接线但弃用」死接线在该面必红。绑定成立。 +2. **V12 生产行兑付**(seal 两函数):`row_by_id` 读生产 reward 行→`grant_routed` 同链兑付→断言真值随行值(effect_id/power)——行值变更即红,判据直接绑「合成行未读生产行」这一被测缺口。绑定成立。 +3. **V11 变异行效果通道**(variant_placement 新函数):真实布置段(begin_battle L3→deep_underdark 候选池)→放置可达→fx_reveal 挖开→兑付伤害行 display 落石值==3——判据绑「L3 两行零放置零断言」缺口的放置与兑付双面。绑定成立。 +4. **V9 状态行值**(round_flow 新函数):生产行值钉+真实施放链(种子狩猎)+个体池读数+容器级再授——判据绑「defensive_stance 零值断言」缺口的行值与行为两面。绑定成立。 + +## 3. 测试削弱检查(全 diff 删行 19 处逐一核对) + +删行清单(`git diff master..iter55` 全量 grep `^-`):reward_grant ×2、probe_ui_util ×4、ui_click_probe ×9、run_all ×0(纯增)、八个测试件中七件纯增、variant_placement ×2、coverage_exemptions ×3(空数组填充+头注扩展)。逐处结论: + +| 删行 | 判定 | 申报面 | +|---|---|---| +| reward_grant.gd `if perfect and first_ok:`/`×2` 高光行 | V10 接线替换,行为零漂移已核(§1 V10) | 卡 V10/T2 申报 ✓ | +| probe_ui_util 滚动基准 4 行(面板基准→ScrollContainer 祖先,64px→8px 让位) | 测量面滚入参数修(代拍板白名单「滚入参数」),修因备案(秘宝匣 336px 裁剪误判) | 报告 §3.2 ✓ | +| ui_click_probe `motion_btn.button_pressed=false/true` ×2 | 直改→`push_click` 真点,周边断言(motion_scale/持久化)保留 | 报告 §3/T9 提交 ✓(强化) | +| ui_click_probe `set_sliders(400,200)` | 参数改 2000/2000,基线对照实证备案 | 报告 §3.1 ✓ | +| ui_click_probe `push_click(node_overlay._btn)` | 原句保留于 else 分支,一次性段新增锁定/落点断言 | T9 ✓(强化) | +| ui_click_probe reward 优选选路 5 行 | 原逻辑保留于回退分支,前置休息候选优先段为纯增 | T9 ✓(强化) | +| ui_click_probe `legend_overlay.close()` | 改「关闭」钮真点,`.close()` 降为 null 守卫兜底且前置 `_problems` 呈报 | T9 提交 ✓(强化) | +| **variant_placement `_smoke_global_variant()` 调用** | **初版未通过项 U1——已修复闭合(857f24d5,§10)**:master :31 在役调用曾被移除,函数体(:150 起)留盘成死代码——33 处 Kit.check 断言退出执行(互锁负样本③–⑦、门控三维、全局决议、Boss 兜底回落、资源面偏斜守恒/白名单/合成序、wr10 三面、确定性对拍——逐行读毕计数),断言字符串全仓 grep 无第二宿主(未搬移)。复验核对:调用行已恢复至执行序 master 原序(iter55 :30,带审计注记),套件复跑 58 checks=原 25+复归 33 恰合 exit0。 | 初版申报缺口:卡 §4「测试削弱零申报不发生——本批零删除断言」、§14「纯增量……零删改既有函数」、报告 §2/§3、T7 提交信息均零申报,`game/tests/README.md:90` 能力行仍列该函数为在役代表用例。修复提交(857f24d5)已逐项转记违规事实与读数,实现报告补 §7 审计结果节。 | +| variant_placement pack_min 断言行 | EOF 缺换行符修复(同文逐字重加),非削弱 | —(无申报义务) | +| coverage_exemptions 空数组/头注 3 行 | 豁免登记本体(V15 收件面) | 卡 T10 ✓ | + +**G8 并存专项**:seal 合成行旧断言(master :772-774)与 treasure 字面量路径(master :128-146)均未触碰——卡 §4「合成行旧断言一律保留」兑现。**阈值放宽**:零(8px/2000/2000 为测量与行为参数,非断言阈值,均有备案)。 + +## 4. 直查例外核对(逐条沿先例) + +| 注入/直查面 | 先例(master 在役) | 判定 | +|---|---|---| +| `s._facts.perfect_hit_true/false` 写(V2 活性) | seal_polarity master :383-384/:398-399、feat_vouchers :207-211 | 成立 | +| `grant_clear_reward` 直调(V2 活性) | seal_polarity master :386/:401 同款形态 | 成立 | +| `grant_routed` 直调(V12) | seal_polarity master :744-774、level_shop_economy_test :917 | 成立 | +| `minesweeper.fx_reveal` 直调(V11) | round_flow master :1101 | 成立 | +| `_deck.ticket_appeared/ticket_pity` 直填(V4) | 同套件 master :564-573 `_deck.ticket_sold/ticket_pity` 测试通道同族 | 成立 | +| `_status_effects.apply_effect/query_active` 容器级(V9 叠层) | roll_multiplier_test master :238-255(卡 T6 明示先例形态) | 成立 | +| `meta_state._add_keystone`(探针树面注钥) | ui_click_probe master :84 既有同款(卡 T9「调试注钥」申报面) | 成立 | +| `rnr_shop.gp = 500`(探针商店可达) | 单元侧先例 curio_refresh_paid_test :52/forge :812;探针侧为该先例族在探针面首次使用,T9 提交信息当场申报「测试通道注GP」 | 成立(备案) | + +## 5. 已知例外与例外面核对 + +- **E1(L3-1,cell_mark_overlay)——申报一致性核实通过**:未落地事实(ui_struct_probe 零触碰)、代码事实(RefCounted 绘制域件、enum 后缀误分类)、报告四处口径(§1 T9 行/§2 V14/§5 名实不符/§6 呈报)、豁免面(ontology 未登记该 id=可见缺口 1)五点互相一致。属卡 §9 预授权停机点(到达性/前提类),不算审计失败。 +- **E2(L3-2,进入下一层)——申报一致性核实通过**:代码事实核实成立(`main.gd:360` 「仅调试模式接线」头注逐字在案;inter_overlay 唯一显示点为 debug decision `next_layer` 分支);controls 已按开发面预登记 `(root)/Button#进入下一层`(注记「随迭代 55 L3 呈报呈请追认」);报告 §3.5/§6 与文件口径一致。属卡 §9 预授权停机点③(到达性失败),不算审计失败。 +- **E3(键形实例变体豁免面扩张)——超卡面,需用户追认**:controls 22 条中「键形实例变体」17 条(save_select 三钮/shop 购买/treasure 四钮/tree 八钮)不在卡附录 C 豁免面(开发面 9+进程终结 2)内,亦非卡 §10 代拍板白名单或 §9 L2(reason 文案微调)可承载的裁决——附录 C 对这些键的裁决是「真缺口·动线扩面=T9 真点命中」,不是豁免。T10 提交信息有 breakdown 申报、文件内逐族 reason 在案(键形改造归工程批另登的卡 §5 口径亦抄注),十组扩面真输入与落点断言已核实存在(§1 V13),故不判测试削弱;但该 17 条使登记件旧键形永久静默(watchdog 面收缩),属对账口径裁决,**应随两项 L3 一并呈请用户追认**;报告 §6 未将其升格列呈报点,属呈报完整性缺口。 +- **计数口径微瑕(O2)**:报告 §1 T9 行「十一组动线全数真输入命中——进入下一层组未落地」的「十一组」与卡附录 C 的 V13 十一组(含进入下一层)分组口径不同(提交信息把树面拆「树购/先手」两组凑十一);实质事实(附录 C V13 十一组中十组落地、进入下一层未落地)两处一致,仅计数口径不一致。 + +## 6. 观察项(不判未通过,留 CI 代证/追认) + +- **O1**:T10 读数纸面算术未对齐——未关联缺口 164−ontology 豁免 154=10,与「未豁免未补测=1」差 9;豁免为精确匹配语义,纸面推不出该差值的去向。T10 为实跑读数(工具已在真实跑批中执行),判读以实跑为准,**待合回后 CI 复跑代证**;若 CI 读数≠1,按红账处置。 +- **O2**:见 §5 计数口径微瑕。 +- **O3**:卡 T10「验证:面板打开无红示警」在报告 §1 T10 行无对应读数记载(面板 reason 标红机制消费面未在报告留痕)——随 CI 批留痕即可。 + +## 7. 最窄测试复跑——省略声明(中档) + +按卡 §16 与 test-conventions §8.7 中档,最窄复跑省略,以下读数以远端 CI 现查代证(合回未发生,**待合回后 CI 代证**):单元全套件与各触碰套件绿读数、两探针 exit0 与十一组命中键清单(`game/tests/.gates/coverage/` 不入 git,命中键无法从 diff 核实)、coverage_enum 对账读数(缺口 164/可见 1/controls 0 残留)。审计对断言存在性、形态与绑定面的核实均为静态逐行自证,不依赖上述读数。 + +## 8. 逐卡验收档与执行深度 + +| 卡 | 验收档 | 实际执行深度 | +|---|---|---| +| T0 开工 | 中 | 中档——读数锚与卡逐字同(报告 §1),审计未复跑(CI 代证) | +| T1/V1 | 中 | 中档全量——七函数逐行读毕+manifest 核实+先例核对 | +| T2/V2/V10/V16 | 中 | 中档全量——接线逐行读毕+字面量残留 grep+回归面在案核实+活性断言先例核对 | +| T3/V3/V4 | 中 | 中档全量——七槽与配额满六断言逐行读毕+直查先例核对 | +| T4/V6 | 中 | 中档全量——两函数三断言逐行读毕 | +| T5/V7 | 中 | 中档全量——四函数逐行读毕+真链入口核实 | +| T6/V9 | 中 | 中档全量(抽查行①)——值/持续/叠层三面读毕 | +| T7/V11 | 中 | 中档全量(抽查行③)——七断言读毕;同提交删行曾坐实 U1(已由 857f24d5 修复闭合——§10) | +| T8/V8/V12 | 中 | 中档全量(抽查行②)——生产行两面读毕+并存申报兑现核实 | +| T9/V13 | 中 | 中档全量——十组逐段读毕+删行九处逐一核对;进入下一层组=E2;hits 增键 CI 代证 | +| T9/V14 | 中 | 中档——未落地事实与五点口径一致性核实(E1) | +| T10/V15 | 中 | 中档——豁免件程序计数(154/22/无重复)+消费语义核实+两例外口径核实;工具读数 CI 代证;例外面 E3 | +| T11 | 中 | 中档——产出物三件在案核对;名实不符申报(cell_mark 段)与 L3-1 一致;初版指出收尾核对漏列 U1 与 README 能力行 `_smoke_global_variant` 名实失守(已随 857f24d5 处置转记——§10) | + +## 9. 审计结论(复验后终版) + +**通过(E1/E2/E3 例外随 L3 裁决包处理,O1 观察在案)。** + +初版未通过项处置(1 项,已闭合): + +1. **U1(测试削弱未申报,违卡 §4 红线)——已修复闭合**:T7 提交(8054f959)曾将 `game/tests/unit/battle_settlement/variant_placement_test.gd` 调用序中的 `_smoke_global_variant()` 移除(master :31→删行),函数体留 :150 成死代码——33 处在役断言(变异表互锁负样本、门控三维、全局决议、兜底回落、资源面偏斜守恒与合成序、wr10 接线三面、确定性对拍)退出执行且全仓无第二宿主,卡 §4「零删除断言」/§14「纯增量」零申报,README:90 能力行名实失守。**窄复验闭合(§10)**:修复提交 857f24d5 恢复调用行至执行序 master 原序(iter55 :30,带审计注记)并逐项转记违规事实;套件复跑 all green 58 checks=原 25+复归 33,与本审计 33 条静态计数恰合;实现报告补 §7 审计结果节如实转记初版结论。 + +例外(3 项,均已在案,随 L3 裁决包处理): + +- E1(L3-1 cell_mark_overlay 未落地)——呈报五点口径一致,待用户裁决(改豁免或另立卡)。 +- E2(L3-2 进入下一层组未落地)——呈报与豁免预登记一致,待用户追认。 +- E3(controls 键形实例变体 17 条豁免超卡附录 C 豁免面)——T10 提交信息与文件 reason 在案,报告未升格呈报,待用户随两项 L3 一并追认。 + +观察项(2 项):O1 对账算术纸面未对齐(164−154≠1;实现报告 §7 已补记解释口径「缺口实例数≠唯一 id 数——同 id 跨类重复,机器读数以逐 id 比对为准」,仍待 CI 复跑代证);O2「十一组」计数口径与卡附录 C 分组不一致(实质一致)。 + +复跑省略代证项(1 项):最窄测试复跑省略+V13 hits 增键+V15 工具读数——待合回后远端 CI 现查代证。 + +合并放行条件:U1 已闭合,本审计不再阻塞合并;E1/E2/E3 属卡 §9 预授权停机面的用户裁决事项,裁决前不落实现动作。 + +## 10. 窄复验记录(2026-11-07,U1 修复后) + +复验范围(仅三项,零其余动作;只读 grep/git): + +1. **调用行与提交核对**:`git show 857f24d5`(iter55 分支最新笔)——variant_placement_test.gd 单行恢复 `_smoke_global_variant() # 审计 U1 修复恢复(T7 编辑回退分支误删调用——33 条在役断言复归执行)`,落位 :30=`_smoke_variant_placement(2)` 与 `_test_pack_min_from_table()` 之间,与 master 原序(master :29-32)逐位同;提交信息逐项转记违规事实(卡 §4/§14)、修因(编辑回退分支误删)与读数。 +2. **读数闭合核对**:58 checks=原 25+复归 33——33 与本审计 §3 对 `_smoke_global_variant` 函数体的静态 Kit.check 计数(33)恰合,闭合成立。 +3. **报告补节核对**:实现报告新增 §7「独立验收审计结果与处置」——初版结论(未通过/U1)、处置(857f24d5)、例外三项与 O1 转记逐项与本报一致。 + +附记:复验区间内分支另有一笔 4a9c844a(件核补笔)——V4 四断言补挂〔QA余 V4〕、V5 五断言补挂〔QA余 V5〕,diff 逐行核实为纯尾缀追加(断言逻辑零改动)+复跑读数在案(adventure_feats 144/treasure 全绿 exit0),属卡 §9 L1 自救范畴,不构成削弱;本审计初版 grep 读数即含该尾缀(审计时已在工作树),初版 §1 V4/V5 行结论不受影响。 + +## 11. 增量轮(卡修续行,2026-11-07) + +审计对象=卡修轮 2(master ef5d4679 版卡片)落地续行两笔 cf08a4ac(登记件开发面过滤+coverage_enum scan_screens 分类修正+豁免件删开发面 3 条)与 b5a981ef(board_shot_probe 采集态六类型注入+逐类型读数+棋盘三图基线换代);审计者=未参与实现与写卡的只读子代理,全结论自读码自证(两笔 diff 逐文件读毕+.gates 产物实测+先例核对)。 + +逐项结论(8 项全过,详据见子代理审计记录): + +1. 提交逐文件 diff 读毕无遗漏;2. 新卡 V13/V14/V15/V17 四行与实现报告 §2 行逐格一致,读数凭据三源互证(提交信息/报告 §9/.gates 实测:dev_face_roots=3、seen 开发面键 0、868 实体缺口 163、三张 PNG 入提交);3. cf08a4ac 零削弱(登记件纯增量 +36/−0,过滤纯减法;scan_screens 删行=卡 T10 申报面;豁免删 3 条三处裁决申报在案);4. b5a981ef 基线换代纪律四要件齐(显式重制/呈报/复闸/触发原因入提交信息),注入四链全真实写口或生产同源(set_treasure_overlays 生产 API、remove_mines_in 核心写口守卫前置、变异 resolver 生产同款构造、face 为每格自建实例非共享 Resource),既有双帧取证语义零削弱;5. 新函数零判定调用=无注记义务(既有注记位自洽);6. 过滤语义核验:三根互恰覆盖附录 C 开发面 10 键、seen 与登记同滤、null 分支只漏滤不误滤且有 seen 核对兜底;7. E3(键形实例变体 17 条)核实未处置仍在册——随合并裁决包呈请追认;8. 能力行两行与卡同文、代表用例在码。 + +发现清单:F1(golden/README 换代戳缺——**已随本审计落定补记**);F2(登记件头注「解析不到的根跳过并计数」措辞与实码偏差——**已随本审计落定改为注释级修正**,零行为改动、非行为改动备案);F3=E3 键形变体 17 条待用户追认(例外,随裁决包);F4(报告/审计本件待落盘——收口提交承载);F5(读数口径措辞「控件 51/命中 85 次」vs「seen 80/命中 85 次」各与 meta 语义吻合——备案)。 + +**增量轮结论:通过——无硬伤无阻塞项,E3 例外随合并裁决包处理。** diff --git a/tools/coverage_enum.py b/tools/coverage_enum.py index 54b51084..8e0f8925 100644 --- a/tools/coverage_enum.py +++ b/tools/coverage_enum.py @@ -43,6 +43,12 @@ SCHEMA_VAR_RE = re.compile(r"^@?export\s+var\s+(\w+)\s*:", re.M) TSCN_NODE_RE = re.compile(r'\[node name="([^"]+)"[^>]*type="(\w*Button\w*)"') GUI_INPUT_RE = re.compile(r"^func (_gui_input|_input)\(", re.M) VIEW_KIND_RE = re.compile(r"(screen|overlay|panel)$") +EXTENDS_RE = re.compile(r"^extends\s+(\w+)", re.M) +CLASS_NAME_RE = re.compile(r"^class_name\s+(\w+)", re.M) +# 屏与模态按实体本相归类(QA覆盖余量收口批 2026-11-07 用户裁): +VIEW_NON_SCENE_BASES = {"RefCounted", "Resource"} # 绘制域件/数据件出圈(如 cell_mark_overlay),禁文件名后缀近似 +DEV_FACE_CLASS_NAMES = {"DebugOverlay"} # 调试浮窗按 class_name 声明出圈——开发面不入玩家可达对账宇宙 + #(登记件 probe_coverage_recorder 头注同款边界,两处单点同源) def scan_tables() -> list[dict]: @@ -94,10 +100,20 @@ def scan_gui_input_classes() -> list[dict]: def scan_screens() -> list[dict]: + # 屏与模态按实体本相归类:场景节点基类才入;RefCounted/Resource 绘制域件出圈; + # DebugOverlay 调试浮窗出圈(判据见 DEV_FACE_CLASS_NAMES 注)。 out: list[dict] = [] for f in sorted((GAME / "presentation" / "views").glob("*.gd")): - if VIEW_KIND_RE.search(f.stem): - out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"}) + if not VIEW_KIND_RE.search(f.stem): + continue + text = f.read_text(encoding="utf-8", errors="replace") + m = EXTENDS_RE.search(text) + if m is not None and m.group(1) in VIEW_NON_SCENE_BASES: + continue + cm = CLASS_NAME_RE.search(text) + if cm is not None and cm.group(1) in DEV_FACE_CLASS_NAMES: + continue + out.append({"category": "屏与模态", "id": f.stem, "id_field": "view"}) return out diff --git a/tools/coverage_exemptions.js b/tools/coverage_exemptions.js index e3d77cf5..e656edd4 100644 --- a/tools/coverage_exemptions.js +++ b/tools/coverage_exemptions.js @@ -1,11 +1,107 @@ /* 覆盖豁免清单——人工维护(S13 基线人工修订同款纪律;面板与工具只读本文件,禁代码生成)。 * 用途:登记「确认不需要真输入覆盖/不需要断言关联」的条目,B 面板对在册条目亮豁免灯、不亮缺口。 * 形态:abilities=能力清单行「能力」列原文(精确匹配);controls=控件登记键或其中缀(包含匹配); - * ontology=本体实体 id(精确匹配)。每条必须带 reason——无理由的豁免条目面板标红示警。 */ + * ontology=本体实体 id(精确匹配)。每条必须带 reason——无理由的豁免条目面板标红示警。 + * reason 承载=逐族注释(族理由照抄需求卡片「QA覆盖余量收口批」附录 B 分组;撤豁免动作随接线批走)。 + * 登记批=迭代 55(2026-11-07);登记凭据=重生成缺口集与本卡裁决逐 id 差集+两探针并集零命中集。 */ window.__QA_COVERAGE__ = window.__QA_COVERAGE__ || {}; window.__QA_COVERAGE__.exemptions = { "meta": {"note": "人工维护;改后刷新面板即生效,无生成步骤"}, "abilities": [], - "controls": [], - "ontology": [] + "controls": [ + /* 豁免·双源同物(附录 C 排除面,7 键):营地自绘物件裸键与场景路径键双源同物—— + 并集后七物件全命中(forge/notebook/campfire 由 screen_flow 承载),登记件键形冗余非缺口。 */ + "camp_object#", + /* 开发面(附录 C 排除面,10 键)不入本清单(2026-11-07 用户裁轮 2 改判):调试面控件 + (debug_overlay 浮窗/调试角标/inter 进入下一层)本就不在玩家可达宇宙——覆盖登记件 + probe_coverage_recorder.gd 按调试面边界登记层过滤(头注声明单点),非豁免条目; + 原开发面 3 条豁免随改判删除。 */ + /* 豁免·进程终结(附录 C,2 键):MenuScreen 退出游戏/camp_esc 退出游戏——真点即杀探针进程, + 按〔接线断言〕承载(既有结构断言在位)。 */ + "Button#退出游戏", + /* 豁免·键形实例变体(登记件实例序位随 UI 构建序平移——迭代 55 扩面动线已真点各家族代表实例, + 未命中键=同控件重建实例;键形改造若要做归工程批另行登记,卡 §5 在案): */ + "save_select_overlay/Button#删档#", + "save_select_overlay/Button#新档#", + "save_select_overlay/Button#进档#", + "shop_overlay_view//Button#购买#", + "treasure_view/Button#升级", + "treasure_view/Button#装配", + "treasure_view/Button#购买 10", + "treasure_view/Button#购买 25", + "tree_view/Button#先手压制", + "tree_view/Button#稳健拓荒", + "tree_view/Button#解锁 25 钥石", + "tree_view/Button#购买 0 钥石", + "tree_view/Button#购买 10 钥石", + "tree_view/Button#购买 20 钥石", + "tree_view/Button#购买 30 钥石", + "tree_view/Button#购买 40 钥石", + "tree_view/Button#购买 60 钥石" + ], + "ontology": [ + /* ── 豁免·预留埋点(普查 §2 词表先行内容后接——未来接线内容时撤豁免补验证)── */ + "chain_ap_cost", "move_ap_cost", /* action_economy——schema 冻结 0,core 零调用方 */ + "hero_hp_pct", "neq", /* skill——TRIGGER_FACTS/算子词预留在册,tres 零行使用 */ + "ap_rounds_front", "effective_rounds", "mine_hit_halve", "reflect_damage", "refresh_pick", + "reward_weight_bias", "slot_cost", "ap_grant", "risk", "cell_removal", + /* treasure——EFFECT_KINDS/MAGNITUDE_KINDS/DIMENSIONS 在册、tres 零行使用 */ + /* ── 豁免·挂起件(全仓零引用,生涯条目承载处置两途)── */ + "settlement_review_overlay", + /* ── 豁免·假缺口(验证在役、文本关联盲区——按族登记,reason 逐族)── */ + /* cap 九槽:钉值在役(table_structure_guard_test gp/attack 钉值同款+迭代 55 V3 七槽钉值扩全九槽——断言文本以槽短名引用) */ + "curio_cap_ap", "curio_cap_armor", "curio_cap_attack", "curio_cap_gp", "curio_cap_heal", + "curio_cap_max_hp", "curio_cap_rap", "curio_cap_refresh", "curio_cap_time_sand", + /* quota 段 13 槽+value 段 13 槽:和锚/逐槽钉值在役(guard 段结构锚+feat_vouchers_test 逐行断言——文本以槽短名引用) */ + "curio_quota_ap_rare", "curio_quota_armor_common", "curio_quota_armor_excellent", + "curio_quota_attack_common", "curio_quota_attack_excellent", "curio_quota_gp_common", + "curio_quota_gp_excellent", "curio_quota_heal_excellent", "curio_quota_max_hp_common", + "curio_quota_max_hp_excellent", "curio_quota_rap_rare", "curio_quota_refresh_excellent", + "curio_quota_time_sand_rare", + "curio_value_ap_rare", "curio_value_armor_common", "curio_value_armor_excellent", + "curio_value_attack_common", "curio_value_attack_excellent", "curio_value_gp_common", + "curio_value_gp_excellent", "curio_value_heal_excellent", "curio_value_max_hp_common", + "curio_value_max_hp_excellent", "curio_value_rap_rare", "curio_value_refresh_excellent", + "curio_value_time_sand_rare", + /* adventure 表字段/段词:生产消费+行为承载(feats A6 配额满分支读生产值驱动+guard 段结构锚在役) */ + "curio_candidate_count", "feat_ticket_chance", "feat_ticket_limit", "feat_ticket_pity_step", + "quota_segment", "value_segment", + /* feats 28 行:逐行行为断言+父专长通用通道读值(adventure_feats_test 逐行段在役) */ + "blast_accounting", "blood_insight", "blood_tax", "breaker_strike", "chain_traps", + "echo_seals", "execution_scholar", "executioner", "field_dressing", "frontier_miner", + "fuse_pts", "fuse_wire", "geologist", "hard_hat", "last_drop", "last_stand", + "map_exchange", "market_close", "round_blast_spent", "round_digs", "round_instinct_atk", + "seal_echo", "seal_stake", "skip_mining", "skip_pts", "smith_seals", "stop_swap", + "trap_instinct", + /* enemy/skills 四件:池构成与 EV 锚+键序(round_flow_skill_pool_test 池段+guard 键序段在役) */ + "harden", "hook_horror", "shield_bash", "venom_spit", + /* reward 四行:负向字典与直伤当量(seal_polarity 负向段+guard 危险格负向直伤当量段在役) */ + "gp_lose", "heal_backlash", "magic_recoil", "pierce_snare", + /* meta_tree 两行+四词:forge_test 装配段/彩蛋段行为承载+tree_rules/tree_requires_level/负面反例 + 承载(迭代 55 V7 树面真链与侧投影断言、探针 40/60 档真输入) */ + "att_socket2", "att_socket3", + "effects_a", "effects_b", "requires_level", "sense_sharp", + /* 屏与模态 8 屏:探针真实入口可见性/真输入断言在役(ui_struct/ui_click/screen_flow/reward_overlay 各段) */ + "battle_end_overlay", "camp_esc_overlay", "curio_pick_overlay", "legend_overlay", + "level_map_screen", "node_overlay", "save_select_overlay", "settings_overlay", + /* 词表/字段杂项:生产消费+行为承载(消费点在役、断言文料以行为面引用——消费点逐条在案: + pity_threshold/min_safe_count 生成器参数、atk_max/atk_min 敌 AC 带宽、initial_owned/per_level_gain/ + price_ratio_pct/type_axis 锻造轴、*_ratio 层构成、indeg/terminals 图约束、rest_heal_pct 休息兑付、 + gp_drops/price_a/pool_shelf 商店面、mult_oor_warning 校验词、round_direction 结算冻结、wall_band/canvas/ + gate_gap/hud_band_h/hotspot_key/texture_rect 营地布局、per_level_bonus/info_panel_w/info_visible_layers/ + roll_tier_bounds/roll_tier_discounts/gte/self_hp_pct 校验与触发词——self_hp_pct 触发行为由迭代 55 V9 + 真实链断言承载) */ + "seal_place_ap_cost", "pity_threshold", "min_safe_count", "atk_max", "atk_min", + "initial_owned", "per_level_gain", "price_ratio_pct", "type_axis", "battle_ratio", + "fork_ways_max", "rest_ratio", "reward_ratio", "shop_ratio", "indeg", "terminals", + "rest_heal_pct", "gp_drops", "price_a", "pool_shelf", "mult_oor_warning", + "round_direction", "wall_band", "canvas", "gate_gap", "hud_band_h", "hotspot_key", + "texture_rect", "per_level_bonus", "info_panel_w", "info_visible_layers", + "roll_tier_bounds", "roll_tier_discounts", "gte", "self_hp_pct", "pool_tag", + /* pool_tag=reward 池标签字段——guard 九型封闭与池行计数断言承载 */ + /* treasure 现役词 12:treasure_effects_test/treasure_target_violation_test/guard 现役承载 */ + "area_3x3", "board_wide", "dimension_tags", "remove_mines", "passive", "ap_charge_safe", + "charge_reset", "charges_per_adventure", "mine_count_preview", "refresh_stock_grant", + "shop_refresh_bonus", "unlock_keystone_cost" + ] };