From 96123978c159841b5742ead3f450dd931af57cf9 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 7 Nov 2026 03:05:11 +0800 Subject: [PATCH 1/4] =?UTF-8?q?iter54-=E5=8D=A11=EF=BC=9AQA=20harness=20?= =?UTF-8?q?=E7=BC=BA=E9=99=B7=E4=B8=8E=E8=A6=86=E7=9B=96=E9=81=97=E6=BC=8F?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=E6=89=B9=EF=BC=88A2=20=E7=BB=88=E5=B1=80?= =?UTF-8?q?=E7=BB=93=E7=AE=97=E6=A1=86=20has=5Fmeta=20=E5=AE=B9=E9=94=99?= =?UTF-8?q?=EF=BC=8BB1=20=E5=81=9C=E6=89=8B=E7=9C=9F=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E6=8E=A2=E9=92=88=E5=AE=9A=E5=9E=8B=E6=AE=B5=E4=B8=8E=20journe?= =?UTF-8?q?y=20=E5=81=9C=E6=89=8B=E8=A1=8C=E4=B8=BA=EF=BC=8BB2=20=E8=A7=86?= =?UTF-8?q?=E5=9B=BE=E6=96=B9=E6=B3=95=E5=8F=A3=2041=20=E5=A4=84=E5=85=A8?= =?UTF-8?q?=E6=B8=85=E8=BD=AC=E7=9C=9F=E8=BE=93=E5=85=A5=EF=BC=8B=E6=AD=BB?= =?UTF-8?q?=E6=8E=A5=E7=BC=9D=2011=20=E6=96=B9=E6=B3=95=E6=B8=85=E7=90=86?= =?UTF-8?q?=EF=BC=8BLevelMapScreen.candidate=5Frect=20=E8=AF=BB=E5=8F=A3?= =?UTF-8?q?=EF=BC=89=E2=80=94=E2=80=94=E5=AF=B9=E7=85=A7=E8=A1=A8=20R1-R9?= =?UTF-8?q?=20=E5=85=A8=E7=BB=BF=EF=BC=88fx=5Fprobe/screen=5Fflow=5Fprobe?= =?UTF-8?q?=20=E9=9B=B6=E6=8A=A5=E9=94=99=E8=A1=8C/ui=5Fclick=5Fprobe=20?= =?UTF-8?q?=E9=97=AE=E9=A2=980=20=E8=A6=86=E7=9B=96=E7=94=9F=E9=95=BF/?= =?UTF-8?q?=E5=8D=95=E5=85=83=E5=A5=97+=E7=B3=BB=E7=BB=9F=E6=B5=8B?= =?UTF-8?q?=E8=AF=95+82=20=E5=A5=97=E4=BB=B6=20all=20green/=E5=8F=8C?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=20grep=20=E9=9B=B6=E5=91=BD=E4=B8=AD/journey?= =?UTF-8?q?=20=E5=81=9C=E6=89=8B=E9=94=AE=206=20=E6=AC=A1=EF=BC=89?= =?UTF-8?q?=EF=BC=8C=E6=89=B9=E6=9C=AB=20--code=20=E5=85=AD=E9=97=B8?= =?UTF-8?q?=E5=85=A8=E7=BB=BF=E3=80=90=E9=AA=8C=E6=94=B6=E9=80=9A=E8=BF=87?= =?UTF-8?q?=E3=80=91=E6=97=A0=E6=96=B0=E5=A2=9E=E7=BA=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/views/ap_bar_view.gd | 10 -- game/presentation/views/battle_end_overlay.gd | 16 ++-- game/presentation/views/curio_pick_overlay.gd | 20 ---- game/presentation/views/debug_overlay.gd | 14 --- game/presentation/views/level_map_screen.gd | 16 ++-- game/presentation/views/node_overlay.gd | 4 - game/probe/fx_shot_probe.gd | 2 +- game/probe/layout_probe.gd | 2 +- game/probe/shot_probe.gd | 6 +- game/probe/t17_defer_probe.gd | 2 +- game/probe/ui_click_probe.gd | 96 +++++++++++++++---- game/probe/ui_probe.gd | 4 +- game/probe/ui_struct_probe.gd | 2 +- game/probe/ui_tune_probe.gd | 48 ++++++++-- game/sim/journey_player.gd | 58 +++++++---- game/tests/system_test.gd | 5 +- .../unit/presentation/reward_overlay_test.gd | 18 ++-- reports/iter54-反哺点-2026-11-07.md | 11 +++ reports/iter54-实现报告-2026-11-07.md | 65 +++++++++++++ 19 files changed, 279 insertions(+), 120 deletions(-) create mode 100644 reports/iter54-反哺点-2026-11-07.md create mode 100644 reports/iter54-实现报告-2026-11-07.md diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index 4045fbf0..8f7b86a6 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -189,20 +189,10 @@ func is_refresh_ready() -> bool: return not _btn_refresh.disabled -# 探针接缝(合成输入走真实翻译出口口径):刷新按钮点击 → 确认 chips 全链路 -func press_refresh_for_probe() -> void: - _on_refresh_pressed() - - func confirm_visible() -> bool: return _confirm_row.visible -func press_confirm_for_probe() -> void: - if _arm_timer.is_armed(): - _disarm_and_fire() - - func refresh_remaining_text() -> String: return _refresh_sub.text diff --git a/game/presentation/views/battle_end_overlay.gd b/game/presentation/views/battle_end_overlay.gd index 3c9c3ba6..dba79c75 100644 --- a/game/presentation/views/battle_end_overlay.gd +++ b/game/presentation/views/battle_end_overlay.gd @@ -191,10 +191,11 @@ func close() -> void: st.seal.visible = false st.seal.modulate = Color.WHITE st.cele.visible = false - var tw: Tween = get_meta(&"celebrate_tw", null) # 循环句柄随节点存(跨 open/close,close 必杀) - if tw != null and tw.is_valid(): - tw.kill() - remove_meta(&"celebrate_tw") + if has_meta(&"celebrate_tw"): # 循环句柄随节点存(跨 open/close:有则必杀、无则无事——缺 meta 跳过) + var tw: Tween = get_meta(&"celebrate_tw") + if tw.is_valid(): + tw.kill() + remove_meta(&"celebrate_tw") _clear_crest_bricks() @@ -328,9 +329,10 @@ func _spawn_dust_mote(i: int, base: Vector2) -> void: func _play_celebrate() -> void: if UiKit.motion_scale < 1.0: return - var prev: Tween = get_meta(&"celebrate_tw", null) - if prev != null and prev.is_valid(): - prev.kill() + if has_meta(&"celebrate_tw"): # 有句柄才取并 kill(缺 meta 静默跳过——败走后首次胜利同款容错) + var prev: Tween = get_meta(&"celebrate_tw") + if prev.is_valid(): + prev.kill() var tw := st.cele.create_tween() set_meta(&"celebrate_tw", tw) tw.set_loops() diff --git a/game/presentation/views/curio_pick_overlay.gd b/game/presentation/views/curio_pick_overlay.gd index 9d187fb7..a2ca37af 100644 --- a/game/presentation/views/curio_pick_overlay.gd +++ b/game/presentation/views/curio_pick_overlay.gd @@ -153,26 +153,6 @@ func _on_confirm_pressed() -> void: pick_confirmed.emit(_selected) -# 探针/测试复用:以真实按钮路径触发点选、确认与继续(树序遮挡纪律——断言走真实信号路径) -func press_card(index: int) -> void: - # M7:卡载体 Button→bcard(gui_input 点选)——真实路径=同一 _select_card 处理器,探针语义不变 - if index >= 0 and index < _card_meta.size(): - _select_card(String(_card_meta[index].get("id", ""))) - - -func press_confirm() -> void: - _confirm_btn.pressed.emit() - - -func press_continue() -> void: - _continue_btn.pressed.emit() - - -# 探针/测试复用:以真实按钮路径触发刷新意图(信号外发——受理归组合根) -func press_refresh() -> void: - _refresh_btn.pressed.emit() - - # 兑付反馈态(curio_resolved 到达——结果行追加在头部之后;唯一出口 = 继续) func show_result(text: String) -> void: _label.text = _header + "\n" + text diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 922b33cd..d0117d8d 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -123,20 +123,6 @@ func output_text() -> String: return st.sim_out.get_parsed_text() -# ---- 探针接缝(真实翻译出口——按钮 pressed 信号路径,非直调组合根)---- - -func press_apply_for_probe() -> void: - _apply_btn_pressed() - - -func press_reset_for_probe() -> void: - _reset_btn_pressed() - - -func press_simulate_for_probe() -> void: - _sim_btn.pressed.emit() - - # ---- 内部(信号 → 外发)---- func _apply_btn_pressed() -> void: diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index c7ad7bf6..f4deee69 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -465,18 +465,20 @@ func _submit_choice(nid: int) -> void: route_chosen.emit(nid) -# 探针复用:按候选序号走图上命中同路径(候选节点中心坐标 → 命中检测 → 提交——布局决策 2 探针面改造) -func press_candidate(index: int) -> void: +# 探针面命中矩形读口:候选节点的图上命中矩形(全局坐标——真输入 push_click_at 落点)。 +# 命中面与 _candidate_at 同源(节点方块+外扩容差);index 越界或候选未就绪返回空 Rect2(调用方跳过)。 +func candidate_rect(index: int) -> Rect2: if index < 0 or index >= _cand_ids.size() or _map.runner == null: - return + return Rect2() var nodes: Array[Variant] = _map.runner.nodes() var node := _node_by_id(int(_cand_ids[index]), nodes) if node.is_empty(): - return + return Rect2() var pos: Vector2 = _layout(nodes, _map.canvas)[node] - var nid := _candidate_at(pos) # 节点中心必命中自身(命中面含节点方块) - if nid >= 0: - _submit_choice(nid) + var half := NODE_HALF + Vector2(HIT_PAD, HIT_PAD) + var local := Rect2(pos - half, half * 2.0) + var xform := _map.canvas.get_global_transform() + return Rect2(xform * local.position, local.size) # ---- 视口锚点(布局决策 5:当前节点居中;入口停点尚无当前节点=锚入口组包围盒居中) ---- diff --git a/game/presentation/views/node_overlay.gd b/game/presentation/views/node_overlay.gd index cabfcdf5..19954500 100644 --- a/game/presentation/views/node_overlay.gd +++ b/game/presentation/views/node_overlay.gd @@ -29,10 +29,6 @@ func show_rest(healed: int, hp_after: int, max_hp: int) -> void: _btn.text = "休息完毕,继续" visible = true -# 探针复用:以真实按钮路径触发继续 -func press_continue() -> void: - _btn.pressed.emit() - func _emit_continue_pressed() -> void: continue_pressed.emit() diff --git a/game/probe/fx_shot_probe.gd b/game/probe/fx_shot_probe.gd index b7e15910..71c2daa7 100644 --- a/game/probe/fx_shot_probe.gd +++ b/game/probe/fx_shot_probe.gd @@ -45,7 +45,7 @@ func _ready() -> void: return for i in 5: await get_tree().process_frame - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) for i in 20: await get_tree().process_frame var st = main.run_scope.settlement diff --git a/game/probe/layout_probe.gd b/game/probe/layout_probe.gd index cb68fd60..c9fbc57d 100644 --- a/game/probe/layout_probe.gd +++ b/game/probe/layout_probe.gd @@ -30,7 +30,7 @@ func _ready() -> void: for i in range(8): await get_tree().process_frame # 进战斗屏——三区(B 战斗画面/C 角色信息/D 扫雷图)同屏常驻,菜单屏不构成断言面 - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) for i in range(20): await get_tree().process_frame for vp in VIEWPORTS: diff --git a/game/probe/shot_probe.gd b/game/probe/shot_probe.gd index eb66e9bd..43faf8e8 100644 --- a/game/probe/shot_probe.gd +++ b/game/probe/shot_probe.gd @@ -6,7 +6,7 @@ # 流程 = 真实按钮信号路径驱动(ui_click_probe 同款;存档/设置/元进度三面换注隔离档——禁碰真实档, # 坑册「断点写盘随 save_manager 引用走」教训同款覆盖):菜单(启动态)→ 设置面(settingsbtn # 真实信号路径)→ 生涯页(函数直调中间态——卡② mn10)→ 营地(进档开层新路径——mn9)→ 关卡画面(函数直调中间态→停点出图)→ -# 战斗(level_screen.press_candidate(0) 真实选路提交)→ 状态面板(地图屏状态角标真实信号路径)。 +# 战斗(level_screen candidate_rect 命中矩形真点选路提交)→ 状态面板(地图屏状态角标真实信号路径)。 # 每屏双帧 + frame_post_draw 同步后抓 get_viewport().get_texture(),PNG 落输出目录; # 最小自检 = 七件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。 # 商店房不纳屏(真实路径口径):真实路径必经 L1 打穿→L2 商店节点 @@ -149,10 +149,10 @@ func _ready() -> void: var expect := "◈ %d" % int(main.ui.modals.run.level_screen.get("_runner").gp) if gp_label.text != expect: _problems.append("GP 徽章读数不符: %s != %s" % [gp_label.text, expect]) - # ④ 战斗(节点一——真实选路提交路径 press_candidate,ui_click_probe :199 同款) + # ④ 战斗(节点一——candidate_rect 命中矩形真点选路提交,ui_click_probe 同款) # oracle: design/gdd/ux/battle.md:122 B 区构建:站桩/明细带/轮次指示(战斗出图与几何锚组) if main.ui.modals.run.level_screen != null and main.ui.modals.run.level_screen.visible: - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) for i in 10: await get_tree().process_frame await _snap("battle") diff --git a/game/probe/t17_defer_probe.gd b/game/probe/t17_defer_probe.gd index a535e93f..c0edf5b3 100644 --- a/game/probe/t17_defer_probe.gd +++ b/game/probe/t17_defer_probe.gd @@ -37,7 +37,7 @@ func _ready() -> void: return for i in 5: await get_tree().process_frame - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) for i in 20: await get_tree().process_frame if main.run_scope.settlement == null: diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index a405d128..2b420b62 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -40,6 +40,8 @@ var _route_clicks := 0 # 选路覆盖层点击计数(岔路使用 var _locktest_state := 0 # T1 D1 锁期/重放断言状态机(0=待注入 1=等重放 2=完结)——每 run 一次 var _locktest_cell := Vector2i(-9, -9) var _locktest_frames := 0 +var _stoptest_state := 0 # 停手钮定型化覆盖段状态机(0=待命 1=已点击待核效 2=完结)——每 run 一次 +var _stoptest_frames := 0 func _ready() -> void: @@ -421,13 +423,13 @@ func _ready() -> void: var wc3_txt: String = main.ui.battle.log_view._log.get_parsed_text() if not wc3_txt.contains("种子 42") or not wc3_txt.contains("(含面板加成)"): _problems.append("开场行回归失守:局外加成(pct>0)局的开场行应带「(含面板加成)」(wc3)") - # 关卡开始 = 入口选路停点——press_candidate 图上命中路径选入口候选进首战 + # 关卡开始 = 入口选路停点——candidate_rect 命中矩形中心真点选入口候选进首战 #(首格可点 AC 口径随之含入口选路一步:菜单点击 → 停点呈现 → pick → 棋盘安装) if main.ui.modals.run.level_screen.visible: # 独立性断言:独立屏激活期战斗屏宿主整体隐藏 if main.ui.screens.battle_root.visible: _problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) await get_tree().process_frame await get_tree().process_frame var t_entry := Time.get_ticks_msec() - t1 @@ -449,7 +451,7 @@ func _ready() -> void: action_budget -= 1 await get_tree().process_frame if main.ui.modals.run.node_overlay.visible: - main.ui.modals.run.node_overlay.press_continue() + ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn) _seals_placed.clear() continue if main.ui.modals.run.curio_overlay.visible: @@ -474,15 +476,17 @@ func _ready() -> void: if not main.ui.flow.locks_input(): _problems.append("三选一停点打开时输入未锁定") if main.ui.modals.run.curio_overlay._continue_btn.visible: - main.ui.modals.run.curio_overlay.press_continue() + 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(): - main.ui.modals.run.curio_overlay.press_card(0) # 奖励节点待选态:点首件+确认(真实按钮信号路径——与关卡完成停点同形) - main.ui.modals.run.curio_overlay.press_confirm() + ProbeUiUtil.push_click_at(main, (main.ui.modals.run.curio_overlay._bcards[0] as Control).get_global_rect().get_center()) # 奖励节点待选态:真点首卡+确认(gui_input 点选——与关卡完成停点同形) + await get_tree().process_frame # 选卡当帧确认钮才现形、容器重排在途——排版帧后矩形成形再真点 + ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._confirm_btn) elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty(): - main.ui.modals.run.curio_overlay.press_card(0) - main.ui.modals.run.curio_overlay.press_confirm() + 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: - main.ui.modals.run.curio_overlay.press_continue() # 停点已消费的残面——继续关屏 + ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn) # 停点已消费的残面——继续关屏 _seals_placed.clear() continue if main.ui.modals.menu.shop_overlay.visible: @@ -537,7 +541,7 @@ func _ready() -> void: 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 - main.ui.modals.run.level_screen.press_candidate(pick) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(pick).get_center()) _route_clicks += 1 _seals_placed.clear() continue @@ -626,11 +630,13 @@ func _ready() -> void: _ap_spent_this_round = 0 _last_state = st _locktest_tick() + _stoptest_tick() if main.run_scope.settlement.battle_result == "" and main.run_scope.settlement.action_economy.phase == "Active": # T1 D1——锁测在飞期(state 1)冻结探针动作面:排空帧 autoplay 的免费 move 会抢在 # 帧末 deferred 重放之前改变英雄位(seal 拒收 not_adjacent 实证)——「注入→重放」 # 窗口内局面零变化正是 v3.1 被测语义(演播期冻结),动作面恢复待断言收口。 - if _locktest_state != 1: + # 停手定型化段同款冻结:核效窗口内零挖掘提交,排除 AP/盘面漂移对收轮判据的混淆。 + if _locktest_state != 1 and _stoptest_state != 1: _run_modal_probes_once() _play_one_action() # ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提 @@ -651,14 +657,15 @@ func _ready() -> void: break elif main.ui.modals.run.curio_overlay.visible: if main.ui.modals.run.curio_overlay._continue_btn.visible: - main.ui.modals.run.curio_overlay.press_continue() + 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()._pending.is_empty(): - main.ui.modals.run.curio_overlay.press_card(0) - main.ui.modals.run.curio_overlay.press_confirm() + 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: - main.ui.modals.run.curio_overlay.press_continue() + ProbeUiUtil.push_click(main, main.ui.modals.run.curio_overlay._continue_btn) elif main.ui.modals.run.node_overlay.visible: - main.ui.modals.run.node_overlay.press_continue() + ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn) elif main.ui.modals.menu.shop_overlay.visible: var shop_leave2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店") if shop_leave2 != null: @@ -978,10 +985,13 @@ func _run_modal_probes_once() -> void: _tested_refresh_chain = true var rap_before: int = int(main.run_scope.settlement.action_economy.rap) var board_before = main.run_scope.settlement.minesweeper.current_board() - main.ui.battle.board_zone.ap_bar.press_refresh_for_probe() + ProbeUiUtil.push_click(main, main.ui.battle.board_zone.ap_bar._btn_refresh) # 真点弃图刷新(disabled 守卫=真实拒绝路径) if not main.ui.battle.board_zone.ap_bar.confirm_visible(): _problems.append("弃图刷新确认 chips 未出现(异位确认缺失)") - main.ui.battle.board_zone.ap_bar.press_confirm_for_probe() + for c in main.ui.battle.board_zone.ap_bar._confirm_row.get_children(): + if c is Button: + ProbeUiUtil.push_click(main, c) # 真点确认 chips 真钮(异位确认——_confirm_row 内 Button 子件) + break if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8: _problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)]) if main.run_scope.settlement.action_economy.phase != "Active": @@ -1155,6 +1165,56 @@ func _locktest_pick_cell(minesweeper) -> Vector2i: return Vector2i(-1, -1) +# ---- 停手钮定型化覆盖段(B1——每 run 一次:使能窗内真点停手钮,钉命中登记与收轮落效) ---- +# 触发条件=战局挖掘期(相位 Active+停手钮使能+盘面有挖可走+本 run 未测过);触发帧冻结 +# 自动玩家其余随机行为,仅提交停手(驱动与 _click_stop 同型——AP 条真钮 push_input)。 +# 断言两钉:①覆盖登记件停手键在场(零命中=驱动空转假绿,即红);②相位离开 Active(停手 +# 收轮)。前提消失守卫:点击后战局收场(终结演播竞态)→ 静默弃测不计 problem(正常局不触发)。 +func _stoptest_tick() -> void: +# oracle: design/gdd/systems/排雷循环/排雷玩法.md:167 (停手=玩家主动收轮——判定上报通道) + if _stoptest_state == 2: + return + var settlement = main.run_scope.settlement + if settlement == null or settlement.minesweeper == null or settlement.minesweeper.current_board() == null: + return + if _stoptest_state == 0: + if String(settlement.battle_result) != "" or main.ui.modals.run.end_overlay.is_open(): + return # 战局已收场——本 run 无停手窗(静置) + if not main.ui.screens.row_pacer.pending_rows().is_empty(): + return # 演播期不出手:点击会进锁期缓存被吞(同 _locktest_tick 前提) + var ae = settlement.action_economy + if String(ae.phase) != "Active" or int(ae.ap) < 1: + return + var stop_btn: Button = main.ui.battle.board_zone.ap_bar._btn_stop + if stop_btn == null or stop_btn.disabled: + return # 停手钮未使能——下帧重试 + if AutoPlayer.choose_dig(settlement.minesweeper).is_empty(): + return # 盘面已无可挖——不算挖掘期触发窗(防清图混淆) + _stoptest_state = 1 + _stoptest_frames = 0 + _click_stop() + elif _stoptest_state == 1: + _stoptest_frames += 1 + if _stoptest_frames > 120: + _problems.append("停手定型化段:点击后 120 帧相位仍 Active(收轮落效失守)〔QA收口 V2〕") + _stoptest_state = 2 + return + if String(settlement.battle_result) != "" or main.ui.modals.run.end_overlay.is_open(): + _stoptest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测 + return + if String(settlement.action_economy.phase) != "Active": + _stoptest_assert_hit() + _stoptest_state = 2 + + +func _stoptest_assert_hit() -> void: +# oracle: design/gdd/systems/排雷循环/排雷玩法.md:167 (停手钮真输入命中登记——判定上报通道) + var stop_btn: Button = main.ui.battle.board_zone.ap_bar._btn_stop + var stop_key := String(_recorder._node_keys.get(stop_btn.get_instance_id(), "")) + if stop_key == "" or not _recorder._hits.has(stop_key): + _problems.append("停手定型化段:停手钮真点零命中登记(驱动空转假绿)〔QA收口 V2〕") + + func _report_stall(st: Dictionary) -> void: # oracle: design/engineering/test-conventions.md:30 (判定上报通道——验证函数组) var why := "" diff --git a/game/probe/ui_probe.gd b/game/probe/ui_probe.gd index 8a37e1d3..1e2bfbab 100644 --- a/game/probe/ui_probe.gd +++ b/game/probe/ui_probe.gd @@ -75,7 +75,7 @@ func _ready() -> void: await get_tree().process_frame await get_tree().process_frame # ---- 入口选路停点(关卡开始即出节点图——level_started 后、首战前弹选路层; - # press_candidate 图上命中路径 pick 入口单候选进首战)---- + # candidate_rect 命中矩形真点 pick 入口单候选进首战)---- # oracle: design/gdd/ux/level-map.md:11 入口停点候选数=入口个数(入口单候选断言) # ---- 独立性断言(独立屏激活期战斗屏宿主整体隐藏,零透出)---- if not main.ui.modals.run.level_screen.visible: @@ -86,7 +86,7 @@ func _ready() -> void: or main.run_scope.runner.next_candidate_ids().size() != 1: problems.append("入口停点状态失守(选路态 + 入口单候选)") else: - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) await get_tree().process_frame await get_tree().process_frame if not main.ui.screens.battle_root.visible or main.ui.modals.run.level_screen.visible: diff --git a/game/probe/ui_struct_probe.gd b/game/probe/ui_struct_probe.gd index d15fe3f5..0b343c18 100644 --- a/game/probe/ui_struct_probe.gd +++ b/game/probe/ui_struct_probe.gd @@ -272,7 +272,7 @@ func _ready() -> void: if not main.ui.modals.run.node_overlay.visible: problems.append("[node] 节点结算层未打开(rest 兑付路径中断)") await _audit("node_overlay", [main.ui.modals.run.node_overlay]) - main.ui.modals.run.node_overlay.press_continue() + ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn) _settle() if main.ui.modals.run.node_overlay.visible: problems.append("[node] 关闭后层未隐藏(continue_pressed 路径中断)") diff --git a/game/probe/ui_tune_probe.gd b/game/probe/ui_tune_probe.gd index 3c910611..e7509d2d 100644 --- a/game/probe/ui_tune_probe.gd +++ b/game/probe/ui_tune_probe.gd @@ -58,16 +58,47 @@ func _ready() -> void: problems.append("生效预览未联动(%s)" % main.ui.modals.menu.debug_overlay.effect_text()) # ③ 应用(真实信号路径:apply_requested → 组合根)——E4 后加成零表写,生效断言移至 ⑧ 战斗快照 # oracle: design/engineering/test-conventions.md:49 调试面加成链路:滑条联动/应用/重置、hero 表运行期只读(②③④⑦组) - main.ui.modals.menu.debug_overlay.press_apply_for_probe() + var apply_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "应用并关闭") + if apply_btn == null: + problems.append("调试浮窗「应用并关闭」钮缺失") + else: + ProbeUiUtil.push_click(main, apply_btn) # 真点应用(组合根应用=关浮窗——落点断言由 ④ 重开承载) # ④ 重置(重开浮窗 → 重置按钮真实路径;面板百分比归零——下一场战斗生效值回到基线) main._show_debug_overlay() - main.ui.modals.menu.debug_overlay.press_reset_for_probe() + var reset_btn: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "重置为 0") + if reset_btn == null: + problems.append("调试浮窗「重置为 0」钮缺失") + else: + ProbeUiUtil.push_click(main, reset_btn) # ⑤ 批量模拟全链路(N=10——走 level_autoplay 与 headless 扫描同口径;表基线由组合根复位; # 菜单态上下文未锁定=按钮可点,is_running 语义=模拟在跑〔18卡⑦ 与对局禁用态拆分〕) # oracle: design/engineering/test-conventions.md:49 调试面批量模拟链路:N=10 输出关卡完成率/剩余 HP/败北节点(⑤组) main.ui.modals.menu.debug_overlay.set_sliders(100, 0) main.ui.modals.menu.debug_overlay.set_sim_count(10) - main.ui.modals.menu.debug_overlay.press_simulate_for_probe() + # 折叠区控件真输入通道(test-conventions §2——先滚轮滚入可视区再点):sim 钮在滚动列表 boot + # 滚位下跨出视口底边(rect y=639 对 scroll 底 641、钮高 40——hit 面被裁),真点必落空; + # 滚轮下探滚入可视区后真点(实证一档入视口、同帧 is_running 翻真) + var sim_btn: Button = main.ui.modals.menu.debug_overlay._sim_btn + var sim_scroll: ScrollContainer = null + var sim_stack: Array = [main.ui.modals.menu.debug_overlay] + while not sim_stack.is_empty(): + var sn: Node = sim_stack.pop_back() + if sn is ScrollContainer: + sim_scroll = sn + break + for sch in sn.get_children(): + sim_stack.append(sch) + if sim_scroll == null: + problems.append("调试浮窗滚动列表缺失(sim 钮滚入通道无载体)") + else: + for i in 12: + if sim_scroll.get_global_rect().encloses(sim_btn.get_global_rect()): + break + ProbeUiUtil.push_wheel(main, sim_scroll.get_global_rect().get_center(), 1) + await get_tree().process_frame + if not sim_scroll.get_global_rect().encloses(sim_btn.get_global_rect()): + problems.append("sim 钮滚轮滚入可视区失败(12 档未入——折叠区真输入通道断)") + ProbeUiUtil.push_click(main, sim_btn) # 真点「运行模拟」(菜单态未锁可点——disabled 守卫=真实拒绝路径) var guard := SIM_GUARD_FRAMES while main.ui.modals.menu.debug_overlay.is_running() and guard > 0: guard -= 1 @@ -97,7 +128,12 @@ func _ready() -> void: # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, true) # 末参=swap_save(卡②签名换代——断点写盘经 autoload 落隔离档) main.ui.modals.menu.debug_overlay.set_sliders(100, 50) - main.ui.modals.menu.debug_overlay.press_apply_for_probe() + main._show_debug_overlay() # ⑥ 关闭后重开——⑧ 应用真点(应用=组合根关窗,⑧b 显式关浮窗断言照旧承载) + var apply_btn2: Button = ProbeUiUtil.find_button(main.ui.modals.menu.debug_overlay, "应用并关闭") + if apply_btn2 == null: + problems.append("调试浮窗「应用并关闭」钮缺失(⑧)") + else: + ProbeUiUtil.push_click(main, apply_btn2) main.ui.modals.menu.debug_overlay.seed_edit.text = "42" # 种子钉死(18卡⑦审计🔴——空框=非首局随机,显式优先经真实路径断言 master_seed) main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言) await get_tree().process_frame @@ -107,9 +143,9 @@ func _ready() -> void: problems.append("开冒险路径未显式关浮窗(T2 防护失守)") if int(main.run_scope.adventure.master_seed) != 42: problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed)) - # 关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 + # 关卡开始 = 入口选路停点——candidate_rect 命中矩形真点选入口候选进首战再读呈现快照 if main.ui.modals.run.level_screen.visible: - main.ui.modals.run.level_screen.press_candidate(0) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) await get_tree().process_frame await get_tree().process_frame var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero diff --git a/game/sim/journey_player.gd b/game/sim/journey_player.gd index 5e64ae82..a9b04cf1 100644 --- a/game/sim/journey_player.gd +++ b/game/sim/journey_player.gd @@ -10,9 +10,9 @@ # 真实输入-场景热点/真实输入-键盘/方法-地图命中口/方法-棋盘口)。原归因制度残骸=本结构内字段。 # 误报收敛:误报指纹(类+段+归因摘要)落 .gates/coverage/journey_misfire_ledger.json——同一指纹 # 第 2 次起自动登记静默(计数照记、不再进上报清单);连 3 批零真缺陷判据收紧归用户复核轮。 -# 驱动纪律:按钮/场景物件全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道)。 -# 两处方法口如实标注通道:地图候选=press_candidate(自绘布局命中路径,探针族既定入口); -# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)。 +# 驱动纪律:按钮/场景物件/地图候选全走真输入(push_input 管线,同探针判据——ProbeUiUtil 单点通道; +# 地图候选=LevelMapScreen.candidate_rect 命中矩形中心 push_click_at——方法口已清零)。 +# 棋盘格=click_cell(BoardView 公共点击口,ui_input_probe 先例——棋盘管线面由该探针承载)=余下唯一方法口。 # 停点分发的视图内部读数(_continue_btn/_node_pending 等)沿用探针族直读受控例外(iter53 反哺点 9 同款在册)。 # 运行:Godot_console.exe --headless --path . res://sim/journey_player.tscn extends Node @@ -41,6 +41,10 @@ var _map_status_done := false # 地图屏状态角标一次性 var _legend_done := false # 图例开合一次性(每战斗面一次) var _status_done := false # 战斗屏状态面板一次性 var _refresh_done := false # 刷新确认链一次性 +var _stop_done := false # 停手行为一次性(每战至多一次——挖掘期确定性触发) +var _stop_settlement: Variant = null # 停手基线战局(换战局重置一次性) +var _stop_round := -1 # 停手轮次基线(轮换重置已花计数) +var _digs_this_round := 0 # 本轮已提交挖掘数(停手条件——AP 已花 ≥1) func _ready() -> void: @@ -376,11 +380,11 @@ func _phase_adventure() -> void: _record_screens() var st := _screen_state() if bool(st["节点"]): - _last_action = "节点层「继续」(方法-视图口)" - main.ui.modals.run.node_overlay.press_continue() + _last_action = "真点节点层「继续」钮" + ProbeUiUtil.push_click(main, main.ui.modals.run.node_overlay._btn) continue if bool(st["三选一"]): - _drive_curio() + await _drive_curio() continue if bool(st["商店"]): var lv: Button = ProbeUiUtil.find_button(main.ui.modals.menu.shop_overlay, "离开商店") @@ -437,30 +441,42 @@ func _drive_map_once() -> void: var resumed: Button = ProbeUiUtil.find_button(main.ui.modals.run.status_panel, "继续", "prefix") if resumed != null and main.ui.modals.run.status_panel.visible: ProbeUiUtil.push_click(main, resumed) - _last_action = "地图候选命中(方法-地图命中口)" - main.ui.modals.run.level_screen.press_candidate(0) + _last_action = "地图候选命中(真实输入-命中矩形)" + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) func _drive_curio() -> void: - # 三选一停点:待选态点首件+确认;兑付/已消费态点继续(方法-视图口——curio 停点既定驱动面) + # 三选一停点:待选态真点首卡+确认(gui_input 点选——确认钮现形当帧容器重排在途,排版帧后再点); + # 兑付/已消费态真点继续 var co: Control = main.ui.modals.run.curio_overlay - _last_action = "三选一停点分发(方法-视图口)" + _last_action = "三选一停点分发(真输入)" if co._continue_btn.visible: - co.press_continue() + ProbeUiUtil.push_click(main, co._continue_btn) elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._node_pending.is_empty(): - co.press_card(0) - co.press_confirm() + ProbeUiUtil.push_click_at(main, (co._bcards[0] as Control).get_global_rect().get_center()) + await _frames(2) + ProbeUiUtil.push_click(main, co._confirm_btn) elif main.run_scope.adventure != null and not main.run_scope.adventure.stop()._pending.is_empty(): - co.press_card(0) - co.press_confirm() + ProbeUiUtil.push_click_at(main, (co._bcards[0] as Control).get_global_rect().get_center()) + await _frames(2) + ProbeUiUtil.push_click(main, co._confirm_btn) else: - co.press_continue() + ProbeUiUtil.push_click(main, co._continue_btn) func _drive_battle_once() -> void: # 战斗单步:挂死检测+功能钮真点(图例/状态/刷新确认链/停手)+棋盘挖格(方法-棋盘口) var settlement = main.run_scope.settlement var diag: Dictionary = settlement.diagnostic_snapshot() + if settlement != _stop_settlement: # 换战局——停手一次性与轮内计数重置 + _stop_settlement = settlement + _stop_done = false + _stop_round = -1 + _digs_this_round = 0 + var round_now := int(diag.round) + if round_now != _stop_round: + _stop_round = round_now + _digs_this_round = 0 var st := { "round": diag.round, "phase": diag.action_economy.phase, "ap": diag.action_economy.ap, "revealed": diag.minesweeper_revealed, @@ -514,6 +530,15 @@ func _drive_battle_once() -> void: # 棋盘动作(方法-棋盘口——BoardView 公共点击口,ui_input_probe 先例;C 的按钮面全走真输入) var minesweeper = settlement.minesweeper var plan: Dictionary = AutoPlayer.choose_dig(minesweeper) + # 停手行为(每战至多一次——挖掘期确定性触发,无随机占比;触发时点微调在代拍板白名单): + # 相位 Active+停手钮使能+AP 已花 ≥1+盘面有挖可走 → 真点停手钮收轮(真输入行为之一) + if not _stop_done and String(diag.action_economy.phase) == "Active" \ + and not ap_bar._btn_stop.disabled and _digs_this_round >= 1 and not plan.is_empty(): + _stop_done = true + _last_action = "真点「停手」钮(挖掘期定型化触发)" + ProbeUiUtil.push_click(main, ap_bar._btn_stop) + await _frames(2) + return if plan.is_empty(): plan = AutoPlayer.gamble_pick(minesweeper) if plan.is_empty(): @@ -525,6 +550,7 @@ func _drive_battle_once() -> void: if plan.hero_cell != minesweeper.hero_pos(): main.ui.battle.board_zone.board_view.click_cell(plan.hero_cell, MOUSE_BUTTON_LEFT) main.ui.battle.board_zone.board_view.click_cell(plan.dig_cell, MOUSE_BUTTON_LEFT) + _digs_this_round += 1 await _frames(1) diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index c9c951b1..9ccd6677 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -9,6 +9,7 @@ extends SceneTree const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现(lifecycle 活战斗驱动;smoke 库件迁移期按现路径 preload) +const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # 真输入合成单点(tests 面复用共用件——禁复制第二份合成输入件) var _failures: int = 0 var _checks: int = 0 @@ -436,8 +437,8 @@ func _smoke_lifecycle_matrix() -> void: await process_frame await _stage_faces(main, "V3b 开局停点", "alive", "alive", "null", {"mf_start": false, "level_vis": true}) - # 阶段③地图选路→战斗中(真实选路提交 press_candidate) - main.ui.modals.run.level_screen.press_candidate(0) + # 阶段③地图选路→战斗中(真实选路提交——candidate_rect 命中矩形真点) + ProbeUiUtil.push_click_at(main, main.ui.modals.run.level_screen.candidate_rect(0).get_center()) for i in range(10): await process_frame await _stage_faces(main, "V3c 战斗中", "alive", "alive", "alive", {"mf_start": false}) diff --git a/game/tests/unit/presentation/reward_overlay_test.gd b/game/tests/unit/presentation/reward_overlay_test.gd index 51937212..3a442146 100644 --- a/game/tests/unit/presentation/reward_overlay_test.gd +++ b/game/tests/unit/presentation/reward_overlay_test.gd @@ -6,6 +6,7 @@ extends SceneTree # 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0) # 断言走 test_kit 计数(Kit.check)+ 失败即停早退协议(顺序状态机——坏态下后继断言无效)。 const Kit := preload("res://tests/unit/test_kit.gd") +const ProbeUiUtil := preload("res://probe/probe_ui_util.gd") # 真输入合成单点(tests 面复用共用件——禁复制第二份合成输入件) # 断言 + 失败即停(原直写 print FAIL + quit(1) + return 样板单点化——早退语义逐位保持) func _req(desc: String, ok: bool) -> bool: @@ -60,13 +61,15 @@ func _run() -> void: if not _req("付费档余额可付:免费位 0 但钮点亮(对齐商店——GP 档可刷)", ov._refresh_btn.visible and not ov._refresh_btn.disabled): return - ov.press_refresh() - if not _req("press_refresh 未外发 refresh_requested", refreshed.size() == 1): + await process_frame # 刷新钮现形当帧容器重排在途——排版帧后矩形成形再真点 + ProbeUiUtil.push_click(root, ov._refresh_btn) # 真点刷新钮(disabled 守卫=真实拒绝路径) + if not _req("真点刷新未外发 refresh_requested", refreshed.size() == 1): return - # ② 点选→高亮→确认外发(真实按钮路径);确认前改选以最后点选为准 - ov.press_card(0) - ov.press_card(1) - ov.press_confirm() + # ② 点选→高亮→确认外发(真输入管线:bcard gui_input 点选——与 press_card 同一 _select_card 处理器);确认前改选以最后点选为准 + ProbeUiUtil.push_click_at(root, (ov._bcards[0] as Control).get_global_rect().get_center()) + ProbeUiUtil.push_click_at(root, (ov._bcards[1] as Control).get_global_rect().get_center()) + await process_frame # 确认钮现形当帧容器重排在途——排版帧后矩形成形再真点 + ProbeUiUtil.push_click(root, ov._confirm_btn) if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked), ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"): return @@ -76,7 +79,8 @@ func _run() -> void: if not _req("结果态应点亮继续且收三卡与刷新钮", ov._continue_btn.visible and not ov._choice_box.visible and not ov._refresh_btn.visible): return - ov.press_continue() + await process_frame # 继续钮现形当帧容器重排在途——排版帧后矩形成形再真点 + ProbeUiUtil.push_click(root, ov._continue_btn) if not _req("继续未外发 continue_pressed", continued.size() == 1): return ov.close_pick() diff --git a/reports/iter54-反哺点-2026-11-07.md b/reports/iter54-反哺点-2026-11-07.md new file mode 100644 index 00000000..e072398a --- /dev/null +++ b/reports/iter54-反哺点-2026-11-07.md @@ -0,0 +1,11 @@ +# 迭代 54 反哺点清单(2026-11-07) + +> 本清单逐卡增量落笔;用户在会话中裁决后由文档批写入 `design/` 并把该条清掉(AGENTS 红线:实现批禁写 `design/`,迭代实现报告与反哺点归 `reports/`)。 + +## 卡 1|QA harness 缺陷与覆盖遗漏收口批——申报:零条 + +卡 1 §6 申报「无关联文档」——A2 为容错修复、B1/B2 为测试驱动面,零设计语义变化。核过面:`design/gdd/systems/排雷循环/排雷玩法.md`(停手语义句 :162/:167 与实装一致)、`design/engineering/test-conventions.md`(真输入管线条文 §2 与实装一致)——两处无需改句。 + +## 卡 2|营地交互修复批——(随卡 2 推进补齐) + +## 卡 3|精英专属池——(随卡 3 推进补齐) diff --git a/reports/iter54-实现报告-2026-11-07.md b/reports/iter54-实现报告-2026-11-07.md new file mode 100644 index 00000000..2af4b0df --- /dev/null +++ b/reports/iter54-实现报告-2026-11-07.md @@ -0,0 +1,65 @@ +# 迭代 54 实现报告(2026-11-07) + +> 三卡:QA harness 缺陷与覆盖遗漏收口批 → 营地交互修复批 → 精英专属池。本报告随各卡末卡增量落笔(卡 1 QA 收口段先行,卡 2/卡 3 段随后补齐),总收口前为完整版。 +> 独立验收审计入口预约:`reports/iter54-验收审计-2026-11-07.md`(未参与实现的只读会话/子代理执行,未过不合回)。审计深度:卡 1=中档(对照表逐行全量、最窄复跑省略以远端 CI 现查代证);卡 2=中档;卡 3=高档(全量)。 + +## 卡 1|QA harness 缺陷与覆盖遗漏收口批——状态:已完成 + +### 各任务卡状态 + +| 任务卡 | 状态 | 摘要 | +|---|---|---| +| T1 A2 关闭容错修复 | 完成 | battle_end_overlay.gd close(:194) 与 _play_celebrate(:331) 两处 `get_meta(&"celebrate_tw", null)` 改 `has_meta` 前置判断,语义不变(有则必杀、无则无事)〔提交 57f3cc75〕 | +| T2 B1 探针定型化停手段 | 完成 | ui_click_probe 新增 `_stoptest_tick` 状态机(镜像 `_locktest_tick` 形态)+`_stoptest_assert_hit`,挖掘期触发窗真点停手钮,断言①覆盖登记停手键在场②相位离开 Active〔提交 816aaae5〕 | +| T3 B1 journey 停手+S4c 8 处转换 | 完成 | journey_player 新增停手行为(每战至多一次,挖掘期确定性触发)+8 处真输入转换+头注方法口句换代〔提交 2f5c734e〕 | +| T4 B2 ui_click_probe 16 处+candidate_rect | 完成 | LevelMapScreen 删 press_candidate 增 candidate_rect(index)->Rect2(命中面与 _candidate_at 同源,全局坐标,越界空 Rect2);ui_click_probe 候选 2+节点继续 2+三选一 10+弃图刷新链 2 全转真输入,既有落点断言逐条原样〔提交 afcfbf37〕 | +| T5 B2 其余 7 探针 11 处 | 完成 | 候选 6+停点 1+调试浮窗 4;shot_probe 头注同步〔提交 22903043〕 | +| T6 B2 非探针层 6 处+死接缝清理 | 完成 | system_test 1 处+reward_overlay_test 5 处真输入;死接缝 11 方法删除(ap_bar_view 2+curio_pick_overlay 4+node_overlay 1+debug_overlay 3,level_map_screen 1 随 T4)〔提交 710df6ce〕 | +| T7 产出物落位 | 完成 | 本报告+反哺点清单+批末选档跑闸(读数见下)+审计入口预约 | + +### 实际对照表(R1–R9,补「结果」格) + +| 行 | 验收测试 | 结果 | +|---|---|---| +| R1 终局结算框关闭动线零引擎报错 | fx_probe+screen_flow_probe 直跑 grep「SCRIPT ERROR」「celebrate_tw」 | 绿——两探针各 exit=0、SCRIPT ERROR 0、celebrate_tw 0(提交 57f3cc75 读数) | +| R2 停手钮真输入命中与落效 | ui_click_probe 直跑(_stoptest_tick 断言①②) | 绿——exit=0、问题=0、覆盖落盘停手键命中 1 次(确定性触发实证;提交 816aaae5 读数) | +| R3 journey 停手+方法口清零 | journey 全线跑批 | 绿(读数判据满足)——fatal 0、上报结构不变(2 条=已知缺陷 A1 双指纹,改前基线同源)、覆盖登记停手键 6 次+停点键在场(结算面继续 6/确认 3)、屏到达 16 类含战斗/终局;exit=1 为 A1 基线态(营地交互修复批承接中,本卡不做动作——卡 1 §5 范围边界) | +| R4 停点链真输入化(ui_click_probe 16 处) | ui_click_probe 直跑+既有落点断言+登记键集不回退 | 绿——exit=0、问题=0、覆盖 43 命中/30 控件较改前 39/28 不回退、新增停点链键(curio 确认选择 2/继续 2)、冒险终局在案选路口点击 9 次 | +| R5 到达型候选+停点(7 探针) | 7 探针逐件孤立直跑 | 绿——headless 五件(ui_struct/layout/ui_probe/t17_defer/ui_tune)exit=0+SCRIPT ERROR 0;窗口化两件(shot 七屏 PNG 齐、fx_shot PASS 演出帧 7 张挖格 3)exit=0 | +| R6 调试浮窗面(4 处) | ui_tune_probe 直跑 | 绿——PASS 全链路(含模拟 N=10 输出与种子三分断言) | +| R7 系统测试宿主候选真输入化 | 系统测试直跑 | 绿——69 项检查 0 失败、三闸过(V3c 阶段真实选路提交在跑批中执行) | +| R8 三选一覆盖层单元真输入化 | 单元与集成套件直跑 | 绿——82 套件 all green、exit=0、0 FAIL(reward_verify 断言在册) | +| R9 死接缝同批清理(5 文件 11 方法) | 全仓 .gd 双模式 grep | 绿——定义清 `func press_(candidate\|continue\|card\|confirm\|refresh\|apply\|reset\simulate)` 与调用清 `.press_(…)` 双零命中 | + +### 卡 1 批末选档跑闸读数(T7) + +`bash tools/run_all_gates.sh --code`(游戏码批选档)=**【验收通过】闸况与基线一致,无新增红**。回执六闸全绿:冒烟测试(smoke_boot.log)=绿/单元与集成测试(unit.log)=绿/系统测试(system_test.log)=绿/场景探针(probe.log)=绿/渲染闸(render.log)=绿/静态扫描(static_scan.log)=绿。改前改后对拍:改前基线=master 40a3ac12 远端 CI run 99 绿;改后=本跑批无新增红(闸况与基线一致)。 + +### 实机目检总清单(卡 1) + +| 目检项 | 登记处 | 义务口径 | +|---|---|---| +| 覆盖面板控件下钻停手键与停点链键在场 | 本表(数据面读数:probe_hits_ui_click.js 停手 1 次、停点链键在案) | 真人检测只登记不阻塞(AGENTS 红线)——登记即完成义务 | + +### 遗留读数义务(卡 1) + +- 无 MC/verifier 义务(本卡零数值零随机面)。 +- 远端 CI 现查读数(合回后):审计与追绿按 process-conventions §1 第 3 条按需现查。 + +### 实现决定记录(卡 1,超白名单事项备案) + +1. **reward_overlay_test.gd 五处行号就地校正**:卡面申报 :60/:64/:65/:66/:76,实查为 :63/:67/:68/:69/:79(漂移 +3,内容与描述全一致)——依卡内 T1 开工核对条款就地校正,未呈报(漂移远小于一屏)。 +2. **ui_click_probe 停手段与 system_test 行号就地校正**:停手触发/驱动 :1041-1045/:1063-1065 → 实查 :1051-1055/:1074-1077;system_test :433 → :440。同上依据。 +3. **排版帧插入**:三选一选卡→确认之间、reward_overlay_test 刷新/确认/继续点击前、journey _drive_curio 卡→确认之间插入 1–2 帧等待——确认钮/继续钮现形当帧容器重排在途,矩形未成形真点必落空。属驱动机制细节(白名单「使能窗等待时长、触发条件的具体写法」同族)。 +4. **ui_tune ⑤ sim 真点的折叠区滚轮通道**:初跑发现 sim 钮真点落空——根因=滚动列表 boot 滚位下钮矩形跨出 ScrollContainer 视口底边(hit 面被裁;emit 路径历史性掩盖)。按 test-conventions §2「折叠区控件先以真输入滚轮滚入可视区再点」落滚轮通道(一档滚入实证后定装),并配 12 档上限+滚入失败 problem 上报(正面效力自检)。⑧ 应用前置重开浮窗(同款机制先例=④ 重开浮窗)。属真输入通道的实现形态(卡片白名单「具体写法」族),随提交留痕。 +5. **候选/bcard 覆盖登记键的结构性缺口**:覆盖登记件自动登记通道=BaseButton pressed+营地自绘物件两源;地图画布与 BCard 非 BaseButton、无 pressed 信号,真输入命中不产键(登记件零改动——卡 1 §13 申报)。候选/弃图确认键的对照表括注预期键与登记件能力不符:效力由既有落点断言承载(R4 判据绑定面=既有落点断言逐条+键集不回退,两者均成立)。弃图确认链在 ui_click_probe 受 is_refresh_ready(RAP≥8)门槛四种子未触发——改前同门槛不可达,链面真输入效力由 journey 既有真点链承载。此为观测面局限申报,非缺陷。 +6. **T2 断言①的登记件读取形态**:`_stoptest_assert_hit` 经 `_recorder._node_keys`/`_recorder._hits` 直读登记件命中表(登记件为探针共用面、卡 1 §13 申报零改动——不新增公开读口,探针面读私有沿现役惯例:ap_bar._btn_stop 同型先例)。 +7. **卡 2 T4.4「等用户发令再走收口」与迭代单合并语义的冲突处置**:卡 2 写卡时为独立批(编入迭代 54 前),其末卡收口语义已被编排段换代为迭代级总收口(iter 技能第 5 步:一次合并、五步登记)。本批按迭代级收口执行,不逐卡等发令;此冲突为编排换代遗留,随本报告呈报。 + +## 卡 2|营地交互修复批——状态:进行中 + +(随卡 2 任务卡推进补齐。) + +## 卡 3|精英专属池——状态:待开工 + +(随卡 3 任务卡推进补齐。) From 4c424af358ad09d411cf5e1ead609ef65b7ff82a Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 7 Nov 2026 03:05:11 +0800 Subject: [PATCH 2/4] =?UTF-8?q?iter54-=E5=8D=A12=EF=BC=9A=E8=90=A5?= =?UTF-8?q?=E5=9C=B0=E4=BA=A4=E4=BA=92=E4=BF=AE=E5=A4=8D=E6=89=B9=EF=BC=88?= =?UTF-8?q?=E5=BC=80=E5=B1=80=E9=9A=90=E8=97=8F=E7=82=B9=E6=94=B6=E4=B8=80?= =?UTF-8?q?=E5=A4=84=20=5Fstart=5Fadventure=EF=BC=8B=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E6=8C=89=E9=92=AE=E4=BD=8D=E6=AC=A1=E9=95=9C?= =?UTF-8?q?=E5=83=8F=E7=A1=AE=E8=AE=A4=E2=86=92=E6=AC=A1=E2=86=92=E5=8F=96?= =?UTF-8?q?=E6=B6=88=EF=BC=8B=E8=90=A5=E5=9C=B0=E9=A1=B6=E6=A0=8F=E5=8F=AF?= =?UTF-8?q?=E8=A7=81=E6=A0=8F=E4=BD=93=20camp=5Fhud=5Fband=5Fsurface=20?= =?UTF-8?q?=E5=8D=95=E5=8F=82=E5=87=BA=E5=8F=A3=20CAMP=5FHUD=5FBAND=C3=970?= =?UTF-8?q?.80=EF=BC=89=E2=80=94=E2=80=94=E5=AF=B9=E7=85=A7=E8=A1=A8=20R1-?= =?UTF-8?q?R8=20=E5=85=A8=E7=BB=BF=EF=BC=88=E6=8E=A2=E9=92=88=20R1/R2/R4?= =?UTF-8?q?=20=E8=90=BD=E7=82=B9=E6=96=AD=E8=A8=80/confirm=5Fdialog=5Ftest?= =?UTF-8?q?=20=E4=B8=83=E6=96=AD=E8=A8=80/=E7=B3=BB=E7=BB=9F=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=20R5=20=E4=BA=94=E9=92=89=2076=20=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=200=20=E5=A4=B1=E8=B4=A5/R6=20WCAG=204.29:1=EF=BC=89=EF=BC=8CS?= =?UTF-8?q?13=20ui=5Fkit=20=E5=9F=BA=E7=BA=BF=2023=E2=86=9224=20=E9=9A=8F?= =?UTF-8?q?=E5=8D=A1=E9=A2=84=E7=94=B3=E6=8A=A5=E4=BF=AE=E8=AE=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/main.gd | 5 +- game/presentation/views/camp_hud_band.gd | 11 +++- game/presentation/views/confirm_dialog.gd | 6 +- game/presentation/views/ui_kit.gd | 14 +++++ game/probe/ui_click_probe.gd | 35 ++++++++++- game/tests/run_all.gd | 1 + game/tests/system_test.gd | 48 +++++++++++++- .../presentation/camp_scene_layout_test.gd | 62 ++++++++++++++++++- .../unit/presentation/confirm_dialog_test.gd | 59 ++++++++++++++++++ reports/iter54-反哺点-2026-11-07.md | 7 ++- reports/iter54-实现报告-2026-11-07.md | 45 +++++++++++++- tools/gdstyle_baseline.txt | 2 +- 12 files changed, 278 insertions(+), 17 deletions(-) create mode 100644 game/tests/unit/presentation/confirm_dialog_test.gd diff --git a/game/presentation/main.gd b/game/presentation/main.gd index bd941859..3c4887df 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -190,8 +190,8 @@ func _start_adventure_from_menu() -> void: # 主菜单「开始冒险」钮已随三入口化撤出(卡② mn6——主菜单侧断点互斥确认链同步摘除, # 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针 # 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝 - # 由卡① sv3 承接(结算不落盘)。 - ui.screens.start_overlay.visible = false + # 由卡① sv3 承接(结算不落盘)。开始层隐藏已收进 _start_adventure()(营地交互修复批契约 1 + # ——开局路径隐藏点唯一出口)。 _start_adventure() @@ -290,6 +290,7 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: # 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接; # 信号连接只在组合根) func _start_adventure() -> void: + ui.screens.start_overlay.visible = false # 开局路径开始层隐藏唯一出口(营地交互修复批契约 1——三处调用方经此收口;失败分支开始层终态=隐藏,致命屏接管) ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开) begin_session(true) _wire_adventure() diff --git a/game/presentation/views/camp_hud_band.gd b/game/presentation/views/camp_hud_band.gd index a228626a..530d735a 100644 --- a/game/presentation/views/camp_hud_band.gd +++ b/game/presentation/views/camp_hud_band.gd @@ -11,10 +11,15 @@ func _init() -> void: set_anchors_preset(Control.PRESET_TOP_WIDE) custom_minimum_size = Vector2(0, 48) # 高 48px 浮层覆盖、不挤压画面(camp.md §顶栏) mouse_filter = Control.MOUSE_FILTER_IGNORE # HUD 栏不挡场景命中区(浮层覆盖语义) + UiKit.camp_hud_band_surface(self) # 栏体色面——整件外观单一出口(色值与 α、铺满、不吃点击、挂载全在出口内);先入=绘制序底,徽章后入不被盖 _badge = UiKit.keystone_badge("钥石 0") - _badge.set_anchors_preset(Control.PRESET_CENTER_RIGHT) - _badge.position = Vector2(-160, 8) # 顶栏右位次(UiKit 现行件域内——代拍板 3) - add_child(_badge) + var dock := HBoxContainer.new() # 右对齐容器承载徽章(Q2 已裁乙——位数增宽自然向左展;摆位机制先例=confirm_dialog 钮行同款容器 alignment) + dock.set_anchors_preset(Control.PRESET_FULL_RECT) + dock.offset_right = -12.0 # 右缘内缩 12px(UiKit 钮间距同档——白名单 3) + dock.alignment = BoxContainer.ALIGNMENT_END + dock.mouse_filter = Control.MOUSE_FILTER_IGNORE # 容器自身默认 STOP——与出口内 IGNORE 是两个点,必须同批落(漏一处顶排物件点不动) + dock.add_child(_badge) + add_child(dock) ## 余额写入(格式串=既有消费面同款「钥石 %d」——badge 工厂约定单点,禁第二格式串) diff --git a/game/presentation/views/confirm_dialog.gd b/game/presentation/views/confirm_dialog.gd index 6ce39f15..8667c07b 100644 --- a/game/presentation/views/confirm_dialog.gd +++ b/game/presentation/views/confirm_dialog.gd @@ -30,13 +30,15 @@ func _init() -> void: box.add_child(btn_row) _cancel_btn = UiKit.button("取消") _cancel_btn.pressed.connect(_emit_cancelled) - btn_row.add_child(_cancel_btn) _secondary_btn = UiKit.button("次选项") _secondary_btn.pressed.connect(_emit_secondary) - btn_row.add_child(_secondary_btn) _confirm_btn = UiKit.button("确认") _confirm_btn.pressed.connect(_emit_confirmed) + # 位次定值(HUD框架.md:90 三条纪律·其三——推进钮在左、次推进居中、维持在右;四处消费方 + # 共用本件同一序。信号按按钮对象路由、不按位次——换序不改路由〔营地交互修复批契约 3〕) btn_row.add_child(_confirm_btn) + btn_row.add_child(_secondary_btn) + btn_row.add_child(_cancel_btn) visible = false diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index b8c615c8..57024b08 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -193,6 +193,7 @@ const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底 const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色) const PLAZA_EDGE := Color8(76, 64, 56) # 小石台描边(art-bible §4.1 换色合并②合并目标——贴图已落此值,缺位回退同值) const CAMP_HEADER_BAND := Color8(110, 69, 38) # 营地柜台头带深棕 #6E4526(4-camp.png 方向稿取色;对齐 demo --wood-deep) +const CAMP_HUD_BAND := Color(CAMP_HEADER_BAND, 0.80) # 营地顶栏栏体=头带深棕×α0.80(camp.md §顶栏定值——派生收口单点,非色板新增;同文件 TOAST_BG/TIP_BG 带 α 先例) const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上) const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上) const HP_BAR_BACK := Color8(40, 34, 32) # 迷你血条深底 #282220(兼血数垫牌;站桩名字行素底同源——§6.1/§7.2 文字禁压地台) @@ -656,6 +657,19 @@ static func keystone_badge(text: String, icon_path := TEX_ICON_KEYSTONE, icon_px return KeystoneBadge.new(text, icon_path, icon_px) +## 营地顶栏栏体面(整件外观单一出口——营地交互修复批契约 4/5):色值与 α、铺满、不吃点击、 +## 挂上宿主五件事全写死在体内;形参只有 host——调用方没有可以忘记的形参。返回句柄供调用方 +## 读取与断言;徽章摆位不由本出口负责(归顶栏件内右对齐容器)。须在宿主入树前调用 +## (set_anchors_preset 是入树前动作——_modal_dim/overlay_bg 同形)。 +static func camp_hud_band_surface(host: Control) -> ColorRect: + var rect := ColorRect.new() + rect.color = CAMP_HUD_BAND + rect.set_anchors_preset(Control.PRESET_FULL_RECT) # 四边 offset 归零铺满宿主 + rect.mouse_filter = Control.MOUSE_FILTER_IGNORE # 栏体不吃点击(顶排物件命中区不受阻——契约 5) + host.add_child(rect) + return rect + + # 按钮皮·五态矩阵(逐实例 override 不动主题层) # 悬停:常态图+modulate 提亮(0.06 档与 primary_button 现行 lightened(0.06) 同纪律,零新素材); # 禁用:本族底+α0.45 幽灵化(与 font_disabled_color 同纪律); diff --git a/game/probe/ui_click_probe.gd b/game/probe/ui_click_probe.gd index 2b420b62..720f4e3d 100644 --- a/game/probe/ui_click_probe.gd +++ b/game/probe/ui_click_probe.gd @@ -744,10 +744,30 @@ func _ready() -> void: _problems.append("大门取消:未无变更退回营地(写口零调用/营地根 visible 失守)〔营地图批 V3〕") ProbeUiUtil.push_click_at(main, main.ui.modals.camp.scene.get_hotspot_rect(&"gate").get_center()) await get_tree().process_frame - ProbeUiUtil.push_click(main, main.ui.modals.menu.confirm_dialog._confirm_btn) # 真点「确认」→开始新冒险 - await get_tree().process_frame + # R4 位次真点(营地交互修复批——位次与语义绑定钉):真点前消费方前置读(gate_new 在册)→ + # 按类型定位按钮行 child(0) 全局矩形中心真点——位次若错接,此点命中「否」,下方主证(翻转)即红。 + # oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 确认弹窗位次定值:推进在左、维持居右(R4 位次真点) + if String(main._confirm_consumer) != "gate_new": + _problems.append("R4 前置:确认消费方非 gate_new(分派面漂移)〔营地交互批 V3〕") + var gate_row := _first_hbox(main.ui.modals.menu.confirm_dialog) + if gate_row == null or gate_row.get_child_count() != 3: + _problems.append("R4:按钮行缺失或子节点数≠3(位次判据面塌)〔营地交互批 V3〕") + else: + ProbeUiUtil.push_click_at(main, (gate_row.get_child(0) as Control).get_global_rect().get_center()) # 真点推进钮(左位) + for i in 3: + await get_tree().process_frame # 停点成形帧口径(先例 3 帧——screen_flow_probe 选路停点成形窗) if not main.run_scope.is_adventure or main.ui.modals.camp.scene.visible: _problems.append("大门确认:冒险未发起或营地场景屏未隐藏〔营地图批 V3〕〔营地图批 B1〕") + if String(main._confirm_consumer) != "": + _problems.append("R4 主证:确认后消费方未清零〔营地交互批 V3〕") + # 开局终态两钉(营地交互修复批——开始层隐藏收一处后的落点断言): + # oracle: design/gdd/systems/冒险循环/关卡节点地图.md:78 关卡开始 → 呈现节点图选路停点(开局终态两钉) + # R1=确认后开始层不可见(被测改动特有提交面——修复前该句恒红=「开局被开始层盖住」缺陷本体); + # R2=关卡画面(选路停点)在位(开局终态呈现面,与既有 is_adventure 断言同批缺一即红) + if main.ui.screens.start_overlay.visible: + _problems.append("大门无断点确认后:开始层滞留可见(开局被开始层盖住缺陷回潮)〔营地交互批 V1〕") + if not main.ui.modals.run.level_screen.visible: + _problems.append("大门无断点确认后:关卡画面(选路停点)未在位〔营地交互批 V2〕") # 有断点三出口(选路停点形注入——同开场行③ set_breakpoint 先例) main.save_manager.set_breakpoint({ "master_seed": 42, "rng_counter": 0, "hero_hp": 30, "gp": 10, "keystone": 0, @@ -1215,6 +1235,17 @@ func _stoptest_assert_hit() -> void: _problems.append("停手定型化段:停手钮真点零命中登记(驱动空转假绿)〔QA收口 V2〕") +func _first_hbox(node: Node) -> HBoxContainer: +# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 (位次真点定位——R4 判据面辅助) + if node is HBoxContainer: + return node + for ch in node.get_children(): + var found := _first_hbox(ch) + if found != null: + return found + return null + + func _report_stall(st: Dictionary) -> void: # oracle: design/engineering/test-conventions.md:30 (判定上报通道——验证函数组) var why := "" diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 4fb4788e..02b76ac2 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -200,6 +200,7 @@ func _suites() -> Array: {"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线) {"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V12(血条危险红/名字行素底不压地台与提档 FS_HINT/徽标/护甲 PNG 绑定/素材入包/地台色合并退役) {"path": "res://tests/unit/presentation/reward_overlay_test.gd", "expect": 0}, # 奖励节点 UI 直验(盲区补漏) + {"path": "res://tests/unit/presentation/confirm_dialog_test.gd", "expect": 0}, # 确认弹窗按钮位次与语义同钉(推进/次推/维持子节点序——营地交互修复批 R3) {"path": "res://tests/unit/presentation/level_map_info_fog_test.gd", "expect": 0}, # 信息迷雾——呈现显隐/Boss 豁免/详情零显示/大图拟合 {"path": "res://tests/unit/presentation/forge_workbench_test.gd", "expect": 0}, # 工作间两区六路线 UI 直验(V13 自动化前置面) {"path": "res://tests/unit/presentation/camp_scene_layout_test.gd", "expect": 0}, # 营地场景布局数据件校验(V14——fail-fast 判据+七命中区几何红线) diff --git a/game/tests/system_test.gd b/game/tests/system_test.gd index 9ccd6677..aaf53736 100644 --- a/game/tests/system_test.gd +++ b/game/tests/system_test.gd @@ -44,6 +44,7 @@ func _split_tail() -> void: await _smoke_run_lifecycle() await _smoke_sim_guards() await _smoke_lifecycle_matrix() + await _smoke_camp_hud_band() else: check(false, "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)") print("[system_test] 摘要:%d 项检查,%d 项失败" % [_checks, _failures]) @@ -603,4 +604,49 @@ func _smoke_lifecycle_matrix() -> void: for p in ["user://smoke_lifecycle.json", "user://smoke_lifecycle.json.tmp", "user://smoke_lifecycle_resume.json", "user://smoke_lifecycle_resume.json.tmp"]: if FileAccess.file_exists(p): - DirAccess.remove_absolute(p) \ No newline at end of file + DirAccess.remove_absolute(p) + + +func _smoke_camp_hud_band() -> void: +# oracle: design/gdd/ux/camp.md:41 (营地顶栏栏体定值与落座——几何/色值/吃点击五钉组) + print("--- 营地交互修复批 T3 顶栏栏体(R5:色值/铺满/不吃点击/首子件/徽章落座)---") + var main: Node = load("res://presentation/main.tscn").instantiate() + root.add_child(main) + await process_frame + await process_frame + main._camp_pres.open_camp() # 营地屏 boot 态为隐——先开营地(装配链 main_ui_build→camp_scene_view 落座) + for i in range(3): + await process_frame # 排版帧(容器重排在途读数不可信) + var band: Control = main.ui.modals.camp.scene.hud_band + check(band != null and band.is_inside_tree(), "R5 前置:顶栏件经装配链在树(camp_scene_view.hud_band 公开句柄)") + if band == null: + main.queue_free() + await process_frame + return + var badge_min: float = band.get_child(band.get_child_count() - 1).get_child(0).get_combined_minimum_size().y if band.get_child_count() > 0 else -1.0 + print("[system_test] 徽章 get_combined_minimum_size().y 实测 = %s(T3.0 前置读数——48=顶格落座前提)" % str(badge_min)) + check(band.get_child_count() >= 2 and band.get_child(0) is ColorRect, + "R5④ 栏体色面是顶栏件第一个子节点(子节点序=绘制序——色面先入不盖徽章)") + if band.get_child_count() >= 2 and band.get_child(0) is ColorRect: + var surface: ColorRect = band.get_child(0) + check(String(surface.color.to_html(true)) == "6e4526cc", + "R5① 栏体色=#6E4526×α0.80(to_html 字面编码——禁浮点分量直等)〔营地交互批 V4〕") + var band_rect: Rect2 = band.get_global_rect() + var surf_rect: Rect2 = surface.get_global_rect() + print("[system_test] 顶栏件实测 get_global_rect=%s、栏体=%s(R5② 承重=ui_registry FULL_RECT 注册)" % [band_rect, surf_rect]) + check(absf(surf_rect.size.x - band_rect.size.x) < 0.51 and absf(surf_rect.size.y - band_rect.size.y) < 0.51 + and absf(surf_rect.size.y - 48.0) < 0.51, + "R5② 栏体铺满宿主且高 48(全局矩形等值)〔营地交互批 V4〕") + check(surface.mouse_filter == Control.MOUSE_FILTER_IGNORE, + "R5③ 栏体不吃点击(MOUSE_FILTER_IGNORE——顶排物件命中区不受阻)〔营地交互批 V4〕") + var dock: Control = band.get_child(band.get_child_count() - 1) + var badge: Control = dock.get_child(0) if dock.get_child_count() > 0 else null + check(badge != null, + "R5⑤ 前置:徽章经右对齐容器在座(dock 尾子件)") + if badge != null: + var brect: Rect2 = badge.get_global_rect() + check(absf(brect.position.y - band_rect.position.y) < 0.51 + and absf(brect.end.x - (band_rect.end.x - 12.0)) < 0.51, + "R5⑤ 徽章顶格右位(y=栏顶格、end.x=栏右缘−12——全局矩形不与局部 position 混写)〔营地交互批 V4〕") + main.queue_free() + await process_frame \ No newline at end of file diff --git a/game/tests/unit/presentation/camp_scene_layout_test.gd b/game/tests/unit/presentation/camp_scene_layout_test.gd index a4a74b07..c3647bf5 100644 --- a/game/tests/unit/presentation/camp_scene_layout_test.gd +++ b/game/tests/unit/presentation/camp_scene_layout_test.gd @@ -11,7 +11,14 @@ const _EXPECTED_KEYS: Array[StringName] = [&"gate", &"forge", &"dummy", &"chest" func _initialize() -> void: # oracle: design/gdd/ux/camp.md:29 (营地场景布局数据件——纵深与地平线组) - call_deferred("_test_layout_hotspot_geometry") + call_deferred("_run_all") + + +func _run_all() -> void: +# oracle: design/gdd/ux/camp.md:29 (套件统一收尾——双测试函数单进程单 quit) + await _test_layout_hotspot_geometry() + await _test_hud_band_contrast_over_bg() + quit(Kit.finish("camp_scene_layout_test", "PASS — 布局几何+顶栏栏体对比 全过")) func _test_layout_hotspot_geometry() -> void: @@ -19,7 +26,6 @@ func _test_layout_hotspot_geometry() -> void: var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres") Kit.check("V14 布局表加载且 schema 类在位〔营地图批 V14〕", table is CampSceneLayoutTable and table != null) if table == null: - quit(Kit.finish("camp_scene_layout_test")) return var problems: Array[String] = table.validate() Kit.check("V14 校验函数通过(问题清单空,实得 %s)〔营地图批 V14〕" % [problems], problems.is_empty()) @@ -88,4 +94,54 @@ func _test_layout_hotspot_geometry() -> void: var load_ok: bool = db.load_tables("res://data/tables", true) Kit.check("V14 ConfigDB 装载含营地布局表且校验过(is_ready=%s)〔营地图批 V14〕" % db.is_ready, load_ok and db.is_ready and db.presentation.camp_scene_layout != null) - quit(Kit.finish("camp_scene_layout_test")) + + +# 顶栏栏体规格侧对比核验(R6——与 R5 分工:R5 核实装等值,本行核规格值本身达标): +# 按规格句在测试内写死 #6E4526 与 α0.80,与背景贴图顶带实测均色做 α 合成后算 WCAG 对比 ≥3.0。 +# 禁把视图件的 color 直接当预期值喂进算式(oracle 镜像——test-conventions §8.7 判红)。 +func _test_hud_band_contrast_over_bg() -> void: +# oracle: design/gdd/ux/camp.md:41 (顶栏栏体定值——对落位带底色对比 ≥3:1 组) + var table: CampSceneLayoutTable = load("res://data/tables/camp_scene_layout.tres") + var band_h := 48 + if table != null: + band_h = int(table.hud_band_h) # 带高取数据出口(禁字面量) + var tex = load("res://assets/art/scene/scene_camp_bg.png") + var img: Image = null + if tex != null and tex.get_image() != null: + img = tex.get_image() + var scan_n := 0 + var r_sum := 0.0 + var g_sum := 0.0 + var b_sum := 0.0 + if img != null: + for y in range(0, mini(band_h, img.get_height())): + for x in range(img.get_width()): # 双层全像素扫描(非步长抽样——terrain_tile_sets_test 同款) + var c := img.get_pixel(x, y) + r_sum += c.r + g_sum += c.g + b_sum += c.b + scan_n += 1 + Kit.check("R6 扫描面非空前置(顶带像素 %d > 0——贴图装载且带高在界)〔营地交互批 V5〕" % scan_n, scan_n > 0) + if scan_n == 0: + return + var bg := Color(r_sum / float(scan_n), g_sum / float(scan_n), b_sum / float(scan_n)) + # 规格侧算式(camp.md:41 写死——#6E4526 × α0.80 对落位带背景合成) + var fg := Color(0x6e / 255.0, 0x45 / 255.0, 0x26 / 255.0, 0.80) + var mixed := Color( + fg.r * fg.a + bg.r * (1.0 - fg.a), + fg.g * fg.a + bg.g * (1.0 - fg.a), + fg.b * fg.a + bg.b * (1.0 - fg.a)) + var lum_fg: float = _wcag_lum(mixed) + var lum_bg: float = _wcag_lum(bg) + var ratio: float = (maxf(lum_fg, lum_bg) + 0.05) / (minf(lum_fg, lum_bg) + 0.05) + print("[camp_scene_layout_test] 顶带均色=%s 合成后=%s WCAG 对比=%.2f:1(规格 ≥3:1)" % [bg, mixed, ratio]) + Kit.check("R6 栏体合成色对落位带底色对比 ≥3.0(实测 %.2f)〔营地交互批 V5〕" % ratio, ratio >= 3.0) + + +## WCAG 相对亮度(sRGB 线性化——命名函数形态,避多行 lambda 嵌套三元解析坑) +func _wcag_lum(c: Color) -> float: + return 0.2126 * _srgb_lin(c.r) + 0.7152 * _srgb_lin(c.g) + 0.0722 * _srgb_lin(c.b) + + +func _srgb_lin(v: float) -> float: + return v / 12.92 if v <= 0.03928 else pow((v + 0.055) / 1.055, 2.4) diff --git a/game/tests/unit/presentation/confirm_dialog_test.gd b/game/tests/unit/presentation/confirm_dialog_test.gd new file mode 100644 index 00000000..ff7bbbfc --- /dev/null +++ b/game/tests/unit/presentation/confirm_dialog_test.gd @@ -0,0 +1,59 @@ +extends SceneTree +# 确认弹窗按钮位次与语义同钉单元套—— +# 守 ConfirmDialog 公开入口 open()/open_three()(四处消费方实际用的两个公开方法)的按钮行子节点序: +# 推进钮恒 child(0)、次推进 child(1)、维持 child(2)(HUD框架.md:90 三条纪律·其三——营地交互修复批 R3)。 +# 判据面=树的公开子节点序+Button 公开 text,零私有成员直读;次钮在 open() 里只置 visible=false +# 不从子节点表移除——双钮形态下子节点仍为 3,按下标断必取 child(2)。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/confirm_dialog_test.gd(expect 0) +const Kit := preload("res://tests/unit/test_kit.gd") +const ConfirmDialogScript := preload("res://presentation/views/confirm_dialog.gd") + + +func _initialize() -> void: +# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 (确认弹窗按钮位次定值——位次与语义同钉组) + call_deferred("_run") + + +# 按类型向下定位第一个 HBoxContainer(该弹窗树内唯一横排——modal_shell 产 CenterContainer+ +# PanelContainer+VBox 不产横排;HSeparator 与 UiKit.button 产件也不是 HBoxContainer) +func _find_btn_row(node: Node) -> HBoxContainer: + if node is HBoxContainer: + return node + for ch in node.get_children(): + var found := _find_btn_row(ch) + if found != null: + return found + return null + + +func _test_two_button_row_order() -> void: +# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 双钮形态位次=推进在左、维持在右(两钮同钉组) + var ov: Control = ConfirmDialogScript.new() + root.add_child(ov) + ov.open("t", "推进", "维持") + var row := _find_btn_row(ov) + Kit.check("双钮形态按钮行子节点数=3(次钮只隐藏不移除——非空前置)", row != null and row.get_child_count() == 3) + if row != null and row.get_child_count() == 3: + Kit.check("双钮形态位次 child(0)=推进", String((row.get_child(0) as Button).text) == "推进") + Kit.check("双钮形态位次 child(2)=维持(次钮占 child(1) 位——按下标断必取 child(2))", String((row.get_child(2) as Button).text) == "维持") + ov.queue_free() + + +func _test_three_button_row_order() -> void: +# oracle: design/gdd/systems/表现与支撑/HUD框架.md:90 三钮形态位次=推进/次推/维持(三钮同钉组) + var ov: Control = ConfirmDialogScript.new() + root.add_child(ov) + ov.open_three("t", "推进", "次推", "维持") + var row := _find_btn_row(ov) + Kit.check("三钮形态按钮行子节点数=3(非空前置)", row != null and row.get_child_count() == 3) + if row != null and row.get_child_count() == 3: + Kit.check("三钮形态位次 child(0)=推进", String((row.get_child(0) as Button).text) == "推进") + Kit.check("三钮形态位次 child(1)=次推", String((row.get_child(1) as Button).text) == "次推") + Kit.check("三钮形态位次 child(2)=维持", String((row.get_child(2) as Button).text) == "维持") + ov.queue_free() + + +func _run() -> void: + _test_two_button_row_order() + _test_three_button_row_order() + quit(Kit.finish("confirm_dialog_test", "PASS — 双钮/三钮形态位次与语义同钉 全过")) diff --git a/reports/iter54-反哺点-2026-11-07.md b/reports/iter54-反哺点-2026-11-07.md index e072398a..13387792 100644 --- a/reports/iter54-反哺点-2026-11-07.md +++ b/reports/iter54-反哺点-2026-11-07.md @@ -6,6 +6,11 @@ 卡 1 §6 申报「无关联文档」——A2 为容错修复、B1/B2 为测试驱动面,零设计语义变化。核过面:`design/gdd/systems/排雷循环/排雷玩法.md`(停手语义句 :162/:167 与实装一致)、`design/engineering/test-conventions.md`(真输入管线条文 §2 与实装一致)——两处无需改句。 -## 卡 2|营地交互修复批——(随卡 2 推进补齐) +## 卡 2|营地交互修复批——两条 + +1. **`design/engineering/architecture/architecture.md` 呈现层模块表 CampHudBand 行(约 :175)**:现句「顶栏 48px 全宽窄条:钥石余额 badge 唯一呈现」已失真——本批落地后顶栏呈现物=栏体色面(`CAMP_HEADER_BAND`×α0.80 全宽)+钥石徽章(右位顶格)两件。建议改成:栏体与徽章两呈现物的实际形态描述;依据=实装读数(栏体 1280×48 全局矩形、徽章 end.x=栏右缘−12,system_test R5 五钉读数)。实现批禁写该文档,由文档批落文。 +2. **根 `体验验证清单.md` 增一行观测项**:营地顶栏观感(「成一条可辨窄带、徽章落在带内」)——需真人试玩下结论,已在实现报告目检总清单登记;由文档批落进清单正式在册。依据=卡 2 §6 同步面表行。 + +(未列项核对:camp.md §顶栏 与 art-bible §6.2 —— α0.80/栏高 48/色值零改值,按卡 2 §6「未改值不列」不列;HUD框架 §提示与弹层·其三「四处共用同一件同一序」——实装后句码一致,无发现不列;architecture.md :168 D1 HUD 视图单元接口——CampHudBand 仍为信号外发+setter 只读注入形态、新出口为 UiKit 工厂面(工厂单点先例在案),未失真不列。) ## 卡 3|精英专属池——(随卡 3 推进补齐) diff --git a/reports/iter54-实现报告-2026-11-07.md b/reports/iter54-实现报告-2026-11-07.md index 2af4b0df..e733570b 100644 --- a/reports/iter54-实现报告-2026-11-07.md +++ b/reports/iter54-实现报告-2026-11-07.md @@ -56,9 +56,50 @@ 6. **T2 断言①的登记件读取形态**:`_stoptest_assert_hit` 经 `_recorder._node_keys`/`_recorder._hits` 直读登记件命中表(登记件为探针共用面、卡 1 §13 申报零改动——不新增公开读口,探针面读私有沿现役惯例:ap_bar._btn_stop 同型先例)。 7. **卡 2 T4.4「等用户发令再走收口」与迭代单合并语义的冲突处置**:卡 2 写卡时为独立批(编入迭代 54 前),其末卡收口语义已被编排段换代为迭代级总收口(iter 技能第 5 步:一次合并、五步登记)。本批按迭代级收口执行,不逐卡等发令;此冲突为编排换代遗留,随本报告呈报。 -## 卡 2|营地交互修复批——状态:进行中 +## 卡 2|营地交互修复批——状态:已完成 -(随卡 2 任务卡推进补齐。) +### 各任务卡状态 + +| 任务卡 | 状态 | 摘要 | +|---|---|---| +| T1 开局路径隐藏点收一处 | 完成 | `_start_adventure()` 首句加开始层隐藏(开局唯一出口,契约 1)+删 `_start_adventure_from_menu()` 冗余句;探针大门无断点段追加 R1/R2〔提交 4d9687eb〕 | +| T2 确认弹窗按钮位次镜像 | 完成 | `add_child` 三句次序镜像(确认→次→取消,其余零动——契约 3);新单元套 confirm_dialog_test 入册;探针 R4 位次真点〔提交 46287043 区段〕 | +| T3 营地顶栏可见栏体 | 完成 | ui_kit 新增 CAMP_HUD_BAND+camp_hud_band_surface(host) 出口(契约 4/5);camp_hud_band 调出口+徽章右对齐容器落座;R5 五钉(系统测试)+R6 对比(单元套)〔提交 1958d329〕 | +| T4 产出物落位 | 完成 | 本报告卡 2 段+反哺点+gdstyle 基线修订(见下)+能力行名实核对 | + +### 实际对照表(R1–R8,补「结果」格) + +| 行 | 验收测试 | 结果 | +|---|---|---| +| R1 大门无断点按「是」后开始层不可见 | ui_click_probe 大门段〔营地交互批 V1〕 | 绿——探针直跑问题=0,断言在跑批中执行(修复前该句恒红判据零命中) | +| R2 开局落节点图选路停点 | ui_click_probe 大门段〔营地交互批 V2〕 | 绿——同批(3 帧口径下 level_screen.visible 断言过) | +| R3 两/三钮形态位次=推进→次→维持 | confirm_dialog_test 两函数 | 绿——直跑 exit=0 七断言全 PASS | +| R4 左位真点即推进 | ui_click_probe R4〔营地交互批 V3〕 | 绿——真点前 consumer=="gate_new" 前置读+child(0) 中心真点+真点后 is_adventure 翻转+consumer 清零,全部在跑批中执行 | +| R5 顶栏栏体几何与徽章落座 | system_test._smoke_camp_hud_band | 绿——76 检查 0 失败;T3.0 前置读数=徽章 min 高 48.0;五钉全过(色面 6e4526cc、铺满 1280×48、IGNORE、首子件、徽章顶格右位 end.x−12) | +| R6 栏体对落位带底色对比 ≥3:1 | camp_scene_layout_test._test_hud_band_contrast_over_bg | 绿——直跑 exit=0,规格侧算式实测 4.29:1(与卡面复核推算一致,顶带均色≈#DEEAE7) | +| R7 回归·件1 未动三条动线 | 既有大门两分支段+篝火段断言 | 绿——探针直跑问题=0(既有断言原样通过) | +| R8 回归·件2 路由与件数断言 | 探针次钮隐藏断言+system_test 三钮计数 | 绿——system_test 76/0(三钮计数断言在册);探针既有次钮断言原样通过 | + +### 卡 2 批末读数 + +- 静态扫描(S13)直跑:首跑出 `ui_kit.gd|quality/max-public-methods = 24(基线登记 23)` 恶化键——卡 2 §13 预申报的例外(用户 11-06 终裁「整件外观封公共件一个具名出口」的直接代价),按申报把 `tools/gdstyle_baseline.txt` 该行 23→24 修订后复跑绿(基线内 7 条、恶化 0)。基线修订的批准依据=卡 2 §13「本条即申报与请求」+U0 批卡。 +- 系统测试 76 检查 0 失败;单元侧 confirm_dialog_test/camp_scene_layout_test 直跑绿;探针直跑问题=0。卡 3 T3.8 的里程碑出口跑闸(--code 全档)覆盖本卡改动面的全档回归。 + +### 实机目检总清单(卡 2) + +| 目检项 | 登记处 | 义务口径 | +|---|---|---| +| 营地顶栏观感——成一条可辨窄带、徽章落在带内 | 本表(渲染闸七屏段营地截图随批产出可对照;卡 2 §1 真人目检项) | 真人检测只登记不阻塞——登记即完成义务 | + +### 遗留读数义务(卡 2) + +- 远端 CI 现查读数(合回后,按需现查)。 + +### 实现决定记录(卡 2 新增条目) + +8. **T2.3「前置重开」步重构为原位 R4**:卡面 T2.3 设计=既有确认真点后重开营地再测 R4;但该流程下 is_adventure 已为真、R4 主证「翻转」恒空转(且 `_on_confirm_declined` 同样清零 consumer,清零判据无判别力)。R4 对照表行的判据绑定面(主证=is_adventure 翻转)优先于任务卡步骤机制——改为真点前 is_adventure=false 的原位确认:既有确认成员点击替换为按钮行 child(0) 坐标真点,翻转成为真判据。既有断言(is_adventure+营地隐藏)原样保留。 +9. **T1.5 `--quick` 闸壳步以探针直跑代证**:改完即测铁规频率口径(轮内不跑闸壳全套含 --quick)下,T1.4 探针直跑即该件最窄确认(同一判据同源读数),未另起 --quick 闸壳跑批。 +10. **T3.0 读数实测 48.0**(卡面 L3 第 5 条警戒项未触发)——顶格落座前提成立,R5⑤ 判据照卡落装。 ## 卡 3|精英专属池——状态:待开工 diff --git a/tools/gdstyle_baseline.txt b/tools/gdstyle_baseline.txt index 84f9aaeb..66e4f3f5 100644 --- a/tools/gdstyle_baseline.txt +++ b/tools/gdstyle_baseline.txt @@ -9,6 +9,6 @@ game/core/adventure_runner.gd|quality/max-class-variables = 23 game/core/level_runner.gd|quality/max-class-variables = 30 game/core/level_runner.gd|quality/max-public-methods = 33 game/presentation/main.gd|quality/max-public-methods = 32 -game/presentation/views/ui_kit.gd|quality/max-public-methods = 23 +game/presentation/views/ui_kit.gd|quality/max-public-methods = 24 game/presentation/views/board_view.gd|quality/max-class-variables = 31 game/presentation/views/board_view.gd|quality/max-public-methods = 27 From f9c9caf56b2c1c2c9d8b5686b896dee8376f2f0d Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 7 Nov 2026 03:05:12 +0800 Subject: [PATCH 3/4] =?UTF-8?q?iter54-=E5=8D=A13=EF=BC=9A=E7=B2=BE?= =?UTF-8?q?=E8=8B=B1=E4=B8=93=E5=B1=9E=E6=B1=A0=E6=89=B9=EF=BC=88elite=20?= =?UTF-8?q?=E8=A1=8C=E7=B1=BB=E4=B8=8E=E5=AD=97=E6=AE=B5=E4=BA=92=E9=94=81?= =?UTF-8?q?/dangling=5Fbase/effective=5Fpool=20=E6=B1=A0=E7=BB=A7=E6=89=BF?= =?UTF-8?q?=E5=90=88=E5=B9=B6/ENCOUNTER=5FROW=5FKINDS=20=E5=AF=BB=E5=9D=80?= =?UTF-8?q?=E9=97=A8=E6=9A=82=E4=B8=8D=E6=8E=A5=E7=BA=BF/goblin=5Felite=20?= =?UTF-8?q?=E8=90=BD=E8=A1=A8=EF=BC=8B=E5=AD=98=E9=87=8F=E5=86=92=E7=83=9F?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=EF=BC=8B=E5=9B=BE=E9=89=B4=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E8=AE=A4=E5=BE=97=E7=B2=BE=E8=8B=B1=E8=A1=8C=EF=BC=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=AF=B9=E7=85=A7=E8=A1=A8=20R1-R7=20=E5=85=A8?= =?UTF-8?q?=E7=BB=BF=EF=BC=88elite=5Frow=5Ftest=20=E5=85=AD=E7=BB=84/?= =?UTF-8?q?=E4=BA=94=E4=BB=B6=E5=AD=98=E9=87=8F=E5=A5=97=E4=BB=B6=E7=9B=B4?= =?UTF-8?q?=E8=B7=91=E7=BB=BF/=E7=94=9F=E4=BA=A7=E8=A1=A8=20error=20?= =?UTF-8?q?=E8=AF=8A=E6=96=AD=200=EF=BC=89=EF=BC=8CT3.8=20=E9=87=8C?= =?UTF-8?q?=E7=A8=8B=E7=A2=91=E5=87=BA=E5=8F=A3=20--code=20=E5=85=AD?= =?UTF-8?q?=E9=97=B8=E5=85=A8=E7=BB=BF=E3=80=90=E9=AA=8C=E6=94=B6=E9=80=9A?= =?UTF-8?q?=E8=BF=87=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/config_db_cross_validator.gd | 2 +- game/core/config_db_resolver.gd | 2 +- game/data/schemas/enemy_table.gd | 131 +++++++- game/data/tables/enemy.tres | 22 +- game/sim/compendium_dump.gd | 19 +- game/tests/run_all.gd | 1 + .../round_flow_skill_pool_test.gd | 18 +- .../unit/core/level_shop_economy_test.gd | 3 +- game/tests/unit/data_config/config_db_test.gd | 6 +- game/tests/unit/data_config/elite_row_test.gd | 305 ++++++++++++++++++ .../data_config/table_structure_guard_test.gd | 16 +- reports/iter54-反哺点-2026-11-07.md | 6 +- reports/iter54-实现报告-2026-11-07.md | 44 ++- tools/compendium_gen.py | 5 +- 14 files changed, 540 insertions(+), 40 deletions(-) create mode 100644 game/tests/unit/data_config/elite_row_test.gd diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index 0ca4c90d..9b1e3a53 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -191,7 +191,7 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, for row in enemy_table.rows: var eid: String = String(row.get("enemy_id", "")) var pool_n := {"basic": 0, "buff": 0, "debuff": 0, "uncond_special": 0} - for entry in row.get("pool", []): + for entry in enemy_table.effective_pool(row): # 合并池(elite=基础池+附加技;battle/boss=自身 pool——池构成纪律校验合并后的池) var sid: String = String(entry.get("skill_id", "")) if not skill_types.has(sid): errs.append({"category": "dangling_skill", "message": "EnemyTable %s 池引用不存在的 skill_id: %s(悬空致命)" % [eid, sid]}) diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index f8fb164b..db2ef49e 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -38,7 +38,7 @@ func resolve_enemy_rowset(L: int, D: int, row_kind: String) -> Array[Dictionary] if L < 1 or D < 0: _diag.call("fatal", "request_oob", "enemy 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [L, D]) return [] - if not EnemyTable.ROW_KINDS.has(row_kind): + if not EnemyTable.ENCOUNTER_ROW_KINDS.has(row_kind): # 开战寻址行类门——elite 行静置合法、任何寻址拿不到它(敌人系统.md:151) _diag.call("fatal", "request_oob", "enemy 请求行类非法 (%d,%d,\"%s\")——row_kind 域 {battle, boss}" % [L, D, row_kind]) return [] var rows: Array[Dictionary] = [] diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index f99d9ab4..0f944f8c 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -8,8 +8,10 @@ extends Resource @export var rows: Array[Dictionary] = [] # Array[Dictionary] -# 行类封闭枚举(对齐关卡节点 type 词表):battle = 战斗节点配池,boss = 终点行集 -const ROW_KINDS := ["battle", "boss"] +# 行类封闭枚举:表内允许出现的行类(battle/boss 对齐关卡节点 type 词表,elite=精英行) +const ROW_KINDS := ["battle", "boss", "elite"] +# 开战寻址可读的行类——elite 接线前不在此列(敌人系统 v1.9 暂不接线;resolver 行类门单点消费) +const ENCOUNTER_ROW_KINDS := ["battle", "boss"] # 护甲类型封闭枚举(克制矩阵本体在 SettlementTable; # 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲) @@ -49,6 +51,7 @@ func validate() -> Array: errs.append_array(_validate_row_numbers(row, eid2)) errs.append_array(_validate_row_atk_range(row, eid2)) errs.append_array(_validate_row_pool(row, eid2)) + errs.append_array(_validate_elite_fields(row, eid2)) errs.append_array(_validate_row_types(row, eid2)) # 行集构成纪律②(行序即契约,敌人系统 v0.15):同带键多行必须 # 文件内相邻。迭代中记录上一行行集键;当前行集键曾出现过但 ≠ 上一行行集键 = 同带键行被隔断,报错。 @@ -67,6 +70,7 @@ func validate() -> Array: boss_ok[L2] = true if not anchor_ok.has(L2): anchor_ok[L2] = false + errs.append_array(_validate_elite_refs()) errs.append_array(_validate_anchor_rows(anchor_ok, boss_ok)) return errs @@ -85,7 +89,7 @@ func _validate_row_base(row: Dictionary, seen: Dictionary) -> Array[Dictionary]: if L < 1 or band < 0: errs.append({"category": "domain", "message": "进度键越域 (%d,band %d)" % [L, band]}) if not ROW_KINDS.has(rk): - errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss})" % [eid, rk]}) + errs.append({"category": "domain", "message": "%s row_kind 未知枚举: \"%s\"(域 {battle, boss, elite})" % [eid, rk]}) if eid == "": errs.append({"category": "domain", "message": "enemy_id 空"}) if String(row.get("display_name", "")) == "": @@ -136,25 +140,99 @@ func _validate_row_atk_range(row: Dictionary, eid: String) -> Array[Dictionary]: ## 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。 ## 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。 func _validate_row_pool(row: Dictionary, eid: String) -> Array[Dictionary]: + var no_errs: Array[Dictionary] = [] + if String(row.get("row_kind", "")) == "elite": + return no_errs # elite 行不带 pool 键(pool_extra 承载附加技——条目域校验随 _validate_elite_fields) var errs: Array[Dictionary] = [] var pool: Array[Variant] = row.get("pool", []) if pool.is_empty(): errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid}) + errs.append_array(_validate_pool_entries(pool, eid, "pool")) + return errs + + +## 池条目域共用校验(pool 与 pool_extra 共用——权重上限 9999 不出现第二份,契约 3 单点); +## field 承载报错定位段("pool"=既有文案逐字不变,"pool_extra"=附加技清单定位)。 +func _validate_pool_entries(entries: Array, eid: String, field: String) -> Array[Dictionary]: + var errs: Array[Dictionary] = [] var pool_ids := {} - for entry_v in pool: + for entry_v in entries: if not (entry_v is Dictionary): - errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]}) + errs.append({"category": "domain", "message": "%s %s 条目非字典: %s" % [eid, field, str(entry_v)]}) continue var entry: Dictionary[Variant, Variant] = entry_v var sid: String = String(entry.get("skill_id", "")) if sid == "": - errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid}) + errs.append({"category": "domain", "message": "%s %s 条目 skill_id 空" % [eid, field]}) elif pool_ids.has(sid): - errs.append({"category": "dup_key", "message": "%s pool 内 skill_id 重复: %s" % [eid, sid]}) + errs.append({"category": "dup_key", "message": "%s %s 内 skill_id 重复: %s" % [eid, field, sid]}) pool_ids[sid] = true var w: int = int(entry.get("weight", 0)) if w <= 0 or w > 9999: - errs.append({"category": "domain", "message": "%s pool 权重越域 (0,9999]: %d" % [eid, w]}) + errs.append({"category": "domain", "message": "%s %s 权重越域 (0,9999]: %d" % [eid, field, w]}) + return errs + + +## 精英行字段互锁(敌人系统 v1.9 :281/:282/:283):elite 行必备 base_ref/pool_extra、禁 pool、 +## enemy_id 不与 base_ref 同名;非 elite 行禁带 base_ref/pool_extra。base_ref 引用存在性归 _validate_elite_refs。 +func _validate_elite_fields(row: Dictionary, eid: String) -> Array[Dictionary]: + var errs: Array[Dictionary] = [] + if String(row.get("row_kind", "")) == "elite": + var base_ref := String(row.get("base_ref", "")) + if base_ref == "": + errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 缺或空(基础敌种引用必带)" % eid}) + if base_ref == eid: + errs.append({"category": "cross_field", "message": "%s elite 行 base_ref 与自身 enemy_id 同名(禁)" % eid}) + if not row.has("pool_extra") or (row.get("pool_extra") as Array).is_empty(): + errs.append({"category": "cross_field", "message": "%s elite 行 pool_extra 缺键或空(附加技清单非空必带)" % eid}) + if row.has("pool"): + errs.append({"category": "cross_field", "message": "%s elite 行带 pool 键(禁——池走继承+附加)" % eid}) + if row.has("pool_extra") and (row.get("pool_extra") as Array).size() > 0: + errs.append_array(_validate_pool_entries(row.get("pool_extra"), eid, "pool_extra")) + else: + if row.has("base_ref"): + errs.append({"category": "cross_field", "message": "%s 非 elite 行带 base_ref 键(禁——基础敌种引用为 elite 行专属)" % eid}) + if row.has("pool_extra"): + errs.append({"category": "cross_field", "message": "%s 非 elite 行带 pool_extra 键(禁——附加技清单为 elite 行专属)" % eid}) + return errs + + +## 精英行跨行引用校验(敌人系统 v1.9 :148/:282):base_ref 指向同关 battle 行类敌种(缺/空已由 +## _validate_elite_fields 报——此处跳过);指向 elite 行判别同门 _min_band_row;附加技与基础池 +## skill_id 交集非空报 dup_key(敌人系统.md:149 附加技与基础池重复拒绝)。 +func _validate_elite_refs() -> Array[Dictionary]: + var errs: Array[Dictionary] = [] + for row in rows: + if String(row.get("row_kind", "")) != "elite": + continue + var base_ref := String(row.get("base_ref", "")) + if base_ref == "": + continue + var level := int(row.get("level_index", 0)) + var eid := String(row.get("enemy_id", "")) + var base := _min_band_row(level, base_ref, "battle") + if base.is_empty(): + if not _min_band_row(level, base_ref, "elite").is_empty(): + errs.append({"category": "dangling_base", "message": "%s base_ref 指向 elite 行: %s(基础敌种须为 battle 行类)" % [eid, base_ref]}) + else: + errs.append({"category": "dangling_base", "message": "%s base_ref 同关 battle 行类无此敌种: %s" % [eid, base_ref]}) + continue + errs.append_array(_extra_overlap_errs(row, base, eid)) + return errs + + +## 附加技与基础池重叠判(_validate_elite_refs 交集段提出——分支预算内聚) +func _extra_overlap_errs(row: Dictionary, base_row: Dictionary, eid: String) -> Array[Dictionary]: + var errs: Array[Dictionary] = [] + var base_ids := {} + for entry_v in base_row.get("pool", []): + if entry_v is Dictionary: + base_ids[String(entry_v.get("skill_id", ""))] = true + for entry_v in row.get("pool_extra", []): + if entry_v is Dictionary: + var sid := String(entry_v.get("skill_id", "")) + if sid != "" and base_ids.has(sid): + errs.append({"category": "dup_key", "message": "%s pool_extra 与基础池 skill_id 重复: %s" % [eid, sid]}) return errs @@ -193,6 +271,43 @@ func _validate_anchor_rows(anchor_ok: Dictionary, boss_ok: Dictionary) -> Array[ return errs +# 基础行查找单点(按 (关, 敌种, 行类) 取深度带最小行——精英有效池与「指向 elite 判别」共用, +# 禁第二份查找算法):无匹配返回空字典。行字段一律 row.get 读取(夹具行可缺键)。 +func _min_band_row(level: int, eid: String, kind: String) -> Dictionary: + var best := {} + var best_band := -1 + for row in rows: + if int(row.get("level_index", -1)) != level or String(row.get("row_kind", "")) != kind \ + or String(row.get("enemy_id", "")) != eid: + continue + var band := int(row.get("depth_band", -1)) + if best.is_empty() or band < best_band: + best = row + best_band = band + return best + + +## 精英有效池(按需现算、零缓存零写回——敌人系统.md:149 继承不复制):非 elite 行 = 自身 pool +## 字典条目副本;elite 行 = 基础行(同关 battle 行类基础敌种深度带最小行)pool 条目 + 本行 +## pool_extra 条目(均只取 is Dictionary 条目并 duplicate——返回值改写不回灌表内行)。 +func effective_pool(row: Dictionary) -> Array[Dictionary]: + var out: Array[Dictionary] = [] + if String(row.get("row_kind", "")) != "elite": + for entry_v in row.get("pool", []): + if entry_v is Dictionary: + out.append((entry_v as Dictionary).duplicate()) + return out + var base := _min_band_row(int(row.get("level_index", 0)), String(row.get("base_ref", "")), "battle") + if not base.is_empty(): + for entry_v in base.get("pool", []): + if entry_v is Dictionary: + out.append((entry_v as Dictionary).duplicate()) + for entry_v in row.get("pool_extra", []): + if entry_v is Dictionary: + out.append((entry_v as Dictionary).duplicate()) + return out + + # 深度带切法单点(唯一实现,禁第二份算法):band_keys = 该 (level_index, row_kind) 的 # depth_band 键升序数组;返回首个 ≥ D 的键(请求落带取偏难一侧)。 # D 高于全部键 → 返回 -1——resolver 侧 clamp 顶带+warning,跨表校验侧判「顶 clamp 消解」违例; diff --git a/game/data/tables/enemy.tres b/game/data/tables/enemy.tres index b27a62ad..d3a2ab18 100644 --- a/game/data/tables/enemy.tres +++ b/game/data/tables/enemy.tres @@ -207,6 +207,25 @@ rows = [{ "armor_type": "light", "armor_class": 8 }, { +"level_index": 1, +"row_kind": "elite", +"depth_band": 0, +"enemy_id": "goblin_elite", + "display_name": "精英·地精", + "unit_count": 1, + "hp": 20, + "atk": 2, + "atk_min": 1, + "atk_max": 3, +"base_ref": "goblin", +"pool_extra": [{ +"skill_id": "heavy", +"weight": 2 +}], +"attack_type": "normal", +"armor_type": "light", +"armor_class": 2 +}, { "level_index": 2, "row_kind": "battle", "depth_band": 0, @@ -943,6 +962,7 @@ rows = [{ ; 不做差异化(pierce/magic 属新设计另立项)。全局平移效应=主角穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0, ; 锻造甲路线首次具备战斗意义;克制矩阵数值零触碰(settlement.tres armor_effectiveness 不改)。 ; -; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss}——battle=战斗节点配池,boss=终点行集; +; 行键 = (level_index, row_kind, depth_band, enemy_id);row_kind ∈ {battle, boss, elite}——battle=战斗节点配池,boss=终点行集,elite=精英行(独立 enemy_id、池走继承+附加,暂不接线任何节点寻址); ; depth_band=节点深度所在的带键,请求落带取首个 ≥ 深度的键(偏难一侧);越顶带由 resolver clamp、跨表校验判为调参事故。 ; (1,3) 大地精行 2026-11-04 用户裁退役,改地精+熊地精混编(行序熊地精在前、行值整行复用 (1,2)/(1,0)、静态骰和 3);沿革见 git 提交史 +; 精英专属池批首只精英=地精精英(1,elite,0,goblin_elite),行值 provisional(投值所有权归难度曲线与Boss 统一修正),来源=2026-11-06 用户点头草案 v0 diff --git a/game/sim/compendium_dump.gd b/game/sim/compendium_dump.gd index c30c0206..de52c0f5 100644 --- a/game/sim/compendium_dump.gd +++ b/game/sim/compendium_dump.gd @@ -3,8 +3,9 @@ ## 运行:Godot_console.exe --headless --path . --script res://sim/compendium_dump.gd -- [out=<绝对路径>] [tables_root=] ## out 缺省 = 仓库根 sim-data/compendium/dump.json(res:// 的上一级);目录不存在则创建。 ## tables_root 缺省 = res://data/tables;fail-fast 路径验证时可指向临时表目录。 -## 导出形态:顶层 counts 带两表行数计数 + enemy_rows/skill_rows 行字典原样(零新增字段、零改名; -## 表内可省略字段缺席即缺席)。本件零随机、零数值硬编码、零判定逻辑。 +## 导出形态:顶层 counts 带两表行数计数 + enemy_rows/skill_rows 行字典原样,elite 行另补合并后的 +## pool 供展示(表内行零改写;battle/boss 行=自身 pool 原样;表内可省略字段缺席即缺席)。 +## 本件零随机、零数值硬编码、零判定逻辑。 ## fail-fast:表加载失败(缺表/行域校验/跨表悬空引用)、行缺关键字段、pool.skill_id 在技能表无对应行 ## → 逐条报错并以退出码 1 中止,禁软回退。 extends SceneTree @@ -29,15 +30,23 @@ func _initialize() -> void: return var enemy_rows: Array = db.combat.enemy.rows var skill_rows: Array = db.combat.skill.rows - var errs := _validate_rows(enemy_rows, skill_rows) + # 导出行数组:逐行 duplicate——elite 行副本写合并后的 pool(按需现算,表内行不被改写); + # battle/boss 行=自身 pool 原样(读法等价面) + var export_enemy_rows: Array = [] + for row_v in enemy_rows: + var row: Dictionary = (row_v as Dictionary).duplicate() + if String(row.get("row_kind", "")) == "elite": + row["pool"] = db.combat.enemy.effective_pool(row_v) + export_enemy_rows.append(row) + var errs := _validate_rows(export_enemy_rows, skill_rows) if not errs.is_empty(): for e in errs: push_error("compendium_dump: %s" % e) quit(1) return var payload := { - "counts": {"enemy_rows": enemy_rows.size(), "skill_rows": skill_rows.size()}, - "enemy_rows": enemy_rows, + "counts": {"enemy_rows": export_enemy_rows.size(), "skill_rows": skill_rows.size()}, + "enemy_rows": export_enemy_rows, "skill_rows": skill_rows, } var out_path := String(opt.out) diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 02b76ac2..4c333a4e 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -179,6 +179,7 @@ func _suites() -> Array: {"path": "res://tests/unit/core/run_time_fatal_test.gd", "expect": 1, "negative": true}, # 卡② wr5/wr6/wr7 运行期违约通道(fatal 封印传导/未知标签/未播种) {"path": "res://tests/unit/data_config/rng_service_test.gd", "expect": 0}, {"path": "res://tests/unit/data_config/table_structure_guard_test.gd", "expect": 0}, # 表结构守卫宿主(adventure 配额与同调目录/enemy 带键行序/AC 单轨与掷点参数/variation 门控互锁/difficulty 修饰/reward 九型/meta 行族) + {"path": "res://tests/unit/data_config/elite_row_test.gd", "expect": 0}, # 精英行校验/有效池合并/继承不复制/合并池纪律/暂不接线(精英专属池批) {"path": "res://tests/unit/data_config/rng_stream_boot_test.gd", "expect": 0}, # 启动初始化与统一 RNG 流宿主(data/tables 加载链/统一流黄金向量 m=42/derive_at 恒等/open_stream 取号序/new_adventure 计数器重置——批 B 自 smoke_all 迁入;源 :70 事件日志落盘条按裁决一转办跨系统闭环宿主) {"path": "res://tests/unit/core/treasure_items_test.gd", "expect": 0}, # 秘宝结构冻结断言面 {"path": "res://tests/unit/core/node_map_generator_test.gd", "expect": 0}, # 节点图生成器产出合格/可复现/岔路取舍/恰一店/归属锁定/战斗保底/出口结构/RNG 纯流面(卡① V1·V3·V5–V7·V9·V10) 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 de318317..a0cbe34a 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 @@ -527,7 +527,7 @@ func _smoke_skill_pool_data() -> void: and is_equal_approx(float(venom.rider_mult), 0.75)) var venom_sole_ok := true for row in _db.combat.enemy.rows: - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): if String(entry.skill_id) == "venom_burst" and String(row.enemy_id) != "drider": venom_sole_ok = false Kit.check("毒爆独占性(仅蛛化卓尔精灵池引用——Boss 专属池独占技纪律)", venom_sole_ok) @@ -538,7 +538,7 @@ func _smoke_skill_pool_data() -> void: and int(snare.buff_duration) == 2 and int(snare.cooldown_beats) == 4) var snare_sole_ok := true for row in _db.combat.enemy.rows: - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): if String(entry.skill_id) == "web_snare" and String(row.enemy_id) != "drider": snare_sole_ok = false Kit.check("蛛网束缚独占性(仅蛛化卓尔精灵池引用——实装面收窄,其余敌种不动)", snare_sole_ok) @@ -557,7 +557,7 @@ func _smoke_skill_pool_data() -> void: var spit_sole_ok := true # 毒吐只挂巨蜘蛛族且首遇带不投(Q6——教学位池零新招) var bash_sole_ok := true # 盾击只挂灰矮人族三行 for row in _db.combat.enemy.rows: - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): var sid := String(entry.skill_id) if sid == "venom_spit" and not (String(row.enemy_id) == "giant_spider" and int(row.level_index) == 2 \ and int(row.depth_band) > spider_lead_band): @@ -608,6 +608,8 @@ func _smoke_skill_pool_data() -> void: "3_boss_11_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, "3_boss_12_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, "3_boss_13_duergar_king": {"attack": 6, "heavy": 2, "earth_pulse": 2, "rockslide": 2}, + # 精英专属池批:首只精英=地精精英(有效池=基础池 goblin 9/1/1+附加技 heavy 2——expect_pools 比对读 effective_pool) + "1_elite_0_goblin_elite": {"attack": 9, "lunge": 1, "desperate_guard": 1, "heavy": 2}, } var pools_ok := true for row in _db.combat.enemy.rows: @@ -616,15 +618,15 @@ func _smoke_skill_pool_data() -> void: pools_ok = false continue var got := {} - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): got[String(entry.skill_id)] = int(entry.weight) if got != expect_pools[rk]: pools_ok = false - Kit.check("二十九行十种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok) + Kit.check("三十行十一种池模板 = 建议池行值((L,row_kind,depth_band,enemy_id) 四元键逐行精确比对;L1 地精 9/1/1〔触发条件批+孤注守势 w1〕 · 熊地精 6/2/2 · 酋长 6/2/2;L2 熊地精 6/1/2 · 蜘蛛 6/2/1+毒吐2〔首遇带不投——迭代 24 N2〕 · 重击手 5/2/2 · 蛛化卓尔精灵 6/2/2/2〔Boss〕;L3 灰矮人 6/2/2+盾击2 · 钩爪恐魔 6/2/1/2〔触发条件批+趁隙偷袭 w2〕 · 重装手 5/2/2+盾击2 · 石君王 6/2/2〔earth_pulse〕——全部 provisional;三 Boss 池尾 +1 门控条目〔quake_smash/venom_curb/rockslide 权重 2——半血解锁收编技能行 trigger 闩型——迭代14④ T5/触发条件批迁移〕;混编行集同带多行各一池合法——首批投放 (2,battle,5) 双行〔迭代 18 卡⑤:重击手+巢群〕;迭代 35 覆盖批八行为顶档池克隆,逐行同检)", pools_ok) var basic_count_ok := true for row in _db.combat.enemy.rows: var n := 0 - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id)) if not srow.is_empty() and String(srow.type) == "basic": n += 1 @@ -637,7 +639,7 @@ func _smoke_skill_pool_data() -> void: for row in _db.combat.enemy.rows: var n_buff := 0 var n_debuff := 0 - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): var srow: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id)) if srow.is_empty(): continue @@ -652,7 +654,7 @@ func _smoke_skill_pool_data() -> void: var uncond_ok := true for row in _db.combat.enemy.rows: var n_uncond := 0 - for entry in row.pool: + for entry in _db.combat.enemy.effective_pool(row): var srow_u: Dictionary = _db.combat.skill.row_by_id(String(entry.skill_id)) if not srow_u.is_empty() and String(srow_u.type) != "basic" and not srow_u.has("trigger"): n_uncond += 1 diff --git a/game/tests/unit/core/level_shop_economy_test.gd b/game/tests/unit/core/level_shop_economy_test.gd index 6d7eb724..dd1ade26 100644 --- a/game/tests/unit/core/level_shop_economy_test.gd +++ b/game/tests/unit/core/level_shop_economy_test.gd @@ -711,6 +711,7 @@ func _smoke_level_graph() -> void: # ⑫ 混编行集运行面正反断言:注入带键 (1,battle,5)/(1,battle,6) = # L1 节点图与全部既有扫描面之外的新带键(带命中精确取注入行集——新 L 会被 ceil 封顶到 L3 不可用), # 断言后立即还原表,后续断言面零污染。R3 fail-fast 正反验证 + R6 行序运行面验证。 + var base_n: int = _db.combat.enemy.rows.size() # 注入前基线行数(生产表行数不写死——精英行随批入表) var mix_row_a := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "goblin", "display_name": "地精", "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]} var mix_row_b := {"level_index": 1, "row_kind": "battle", "depth_band": 5, "enemy_id": "bugbear", "display_name": "熊地精", "unit_count": 2, "hp": 20, "atk": 3, "pool": [{"skill_id": "attack", "weight": 9}]} _db.combat.enemy.rows.append(mix_row_a) @@ -729,7 +730,7 @@ func _smoke_level_graph() -> void: _db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1) _db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1) _db.combat.enemy.rows.remove_at(_db.combat.enemy.rows.size() - 1) - Kit.check("混编断言段注入行全量还原(表回 29 行——L1 混编批后的基线行数;首批混编投放基线 21 行+覆盖 8 行)", int(_db.combat.enemy.rows.size()) == 29) + Kit.check("混编断言段注入行全量还原(表回注入前基线行数 %d——相对基数断言,生产表行数不写死)" % base_n, int(_db.combat.enemy.rows.size()) == base_n) # ⑬ 真图类型可见性(信息迷雾 v2.1):L2 入口停点纯核心面读数——候选 ⊆ 可见集 / # 终点恒可见 / 入口 BFS 层距 > N 的非豁免节点入雾。N 取数据表(代码零硬编码)。 diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index 670dc200..058bbc43 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -256,16 +256,16 @@ func _test_rowset_discipline() -> void: if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("unit_count"): dom_hit = true Kit.check("单行数量域 [1,3] 域闸不回归(UNIT_COUNT_MAX 语义 = 单种数)", dom_hit) - # 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据) + # 负向:row_kind 封闭枚举(非法值拒——键面换代后新增判据;elite 随精英专属池批转合法,非法样本换 miniboss) var t_kind = EnemyTableScript.new() - t_kind.rows.assign([mk_row.call(1, "elite", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"), + t_kind.rows.assign([mk_row.call(1, "miniboss", 5, "goblin"), mk_row.call(1, "battle", 0, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")]) errs = t_kind.validate() var kind_hit := false for e in errs: if String(e.get("category", "")) == "domain" and String(e.get("message", "")).contains("row_kind"): kind_hit = true - Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss})〔fv7〕", kind_hit) + Kit.check("非法 row_kind 被拒(封闭枚举 {battle, boss, elite})〔fv7〕", kind_hit) # 负向:anchor 两半——缺 battle band 0 锚行、缺 Boss 行集,各自报 anchor var t_no_anchor = EnemyTableScript.new() t_no_anchor.rows.assign([mk_row.call(1, "battle", 5, "goblin"), mk_row.call(1, "boss", 9, "ogre_chief")]) diff --git a/game/tests/unit/data_config/elite_row_test.gd b/game/tests/unit/data_config/elite_row_test.gd new file mode 100644 index 00000000..b915df38 --- /dev/null +++ b/game/tests/unit/data_config/elite_row_test.gd @@ -0,0 +1,305 @@ +# tests/unit/data_config/elite_row_test.gd — 精英行单元套(精英专属池批) +# 断言面 = 精英行加载期校验(字段互锁/引用校验/错误类别)·有效池合并(基础池取深度带最小行 +# +附加技)·继承不复制(改基础池传到精英、改写返回不回灌)·合并池过池构成纪律(跨表件)· +# 暂不接线(寻址行类门只放 battle/boss,elite 行静置合法)·生产表 goblin_elite 行(随 T3 落表追加)。 +# 断言从公开入口进出:EnemyTable.validate()/effective_pool()/validate_skill_pool_links()/ +# resolve_enemy_rowset()/ConfigDB.load_tables()。夹具一律 rows.assign([...])(定型数组直赋会触发 +# 脚本错误静默中断——config_db_test 同款坑注)。 +# 运行:godot --headless --path game --script res://tests/unit/data_config/elite_row_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const EnemyTableScript := preload("res://data/schemas/enemy_table.gd") +const ValidatorScript := preload("res://core/config_db_cross_validator.gd") +const SkillTableScript := preload("res://data/schemas/skill_table.gd") +const StatusTableScript := preload("res://data/schemas/status_effect_table.gd") +const ResolverScript := preload("res://core/config_db_resolver.gd") + +var _diags: Array[String] = [] # T2.4 夹具 resolver 诊断捕获 + + +func _init() -> void: + _test_elite_validation_rejects() + _test_effective_pool_merge() + _test_base_pool_change_propagates() + _test_merged_pool_composition() + _test_elite_unreachable_by_encounter() + _test_production_goblin_elite() + quit(Kit.finish("elite_row_test", "PASS — 精英行校验/有效池/继承不复制/合并池纪律/暂不接线/生产表落行 全过")) + + +# 行构造助手(照 config_db_test mk_row 写全字段;elite 行形态=去掉 pool、加 base_ref/pool_extra) +func _mk_row(l: int, kind: String, band: int, eid: String) -> Dictionary: + return {"level_index": l, "row_kind": kind, "depth_band": band, "enemy_id": eid, "display_name": eid + "名", + "unit_count": 1, "hp": 10, "atk": 2, "pool": [{"skill_id": "attack", "weight": 9}]} + + +func _mk_elite_row(l: int, band: int, eid: String, base_ref: String, extra: Array) -> Dictionary: + return {"level_index": l, "row_kind": "elite", "depth_band": band, "enemy_id": eid, "display_name": eid + "名", + "unit_count": 1, "hp": 20, "atk": 2, "base_ref": base_ref, "pool_extra": extra} + + +# 合法夹具(三行——battle 锚/boss/elite 引用同关 battle 敌种) +func _legal_rows() -> Array: + var goblin := _mk_row(1, "battle", 0, "goblin") + goblin["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] + return [goblin, _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}])] + + +# category 计数(错误列表按类别筛) +func _count_cat(errs: Array, cat: String) -> int: + var n := 0 + for e in errs: + if String((e as Dictionary).get("category", "")) == cat: + n += 1 + return n + + +# ---- R1:elite 行类合法与字段互锁、引用校验(合法零错+十一违例各自「含」预期类别)---- +func _test_elite_validation_rejects() -> void: +# oracle: design/gdd/systems/战斗循环/敌人系统.md:325 elite 行字段互锁与基础引用违例加载期返回错误 + var t_ok = EnemyTableScript.new() + t_ok.rows.assign(_legal_rows()) + var errs: Array = t_ok.validate() + Kit.check("合法夹具 validate 零错(battle 锚+boss+elite 引用在位——实得 %s)" % str(errs), errs.is_empty()) + # ① elite 删 base_ref → cross_field + var rows1 := _legal_rows() + rows1[2].erase("base_ref") + Kit.check("① elite 删 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows1), "cross_field") >= 1) + # ② base_ref 与自身同名 → cross_field + var rows2 := _legal_rows() + rows2[2]["base_ref"] = "goblin_elite" + Kit.check("② base_ref 与 enemy_id 同名报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows2), "cross_field") >= 1) + # ③ pool_extra 为空数组 → cross_field + var rows3 := _legal_rows() + rows3[2]["pool_extra"] = [] + Kit.check("③ pool_extra 空数组报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows3), "cross_field") >= 1) + # ④ elite 删 pool_extra 键 → cross_field + var rows4 := _legal_rows() + rows4[2].erase("pool_extra") + Kit.check("④ elite 删 pool_extra 键报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows4), "cross_field") >= 1) + # ⑤ elite 加 pool → cross_field + var rows5 := _legal_rows() + rows5[2]["pool"] = [{"skill_id": "attack", "weight": 9}] + Kit.check("⑤ elite 带 pool 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows5), "cross_field") >= 1) + # ⑥ battle 行加 base_ref → cross_field + var rows6 := _legal_rows() + rows6[0]["base_ref"] = "ogre_chief" + Kit.check("⑥ battle 行带 base_ref 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows6), "cross_field") >= 1) + # ⑦ battle 行加 pool_extra → cross_field + var rows7 := _legal_rows() + rows7[0]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}] + Kit.check("⑦ battle 行带 pool_extra 报 cross_field〔精英批 V1〕", _count_cat(_validate_rows(rows7), "cross_field") >= 1) + # ⑧ base_ref="hobgoblin"(同关无此 battle 敌种)→ dangling_base + var rows8 := _legal_rows() + rows8[2]["base_ref"] = "hobgoblin" + Kit.check("⑧ base_ref 悬空报 dangling_base〔精英批 V1〕", _count_cat(_validate_rows(rows8), "dangling_base") >= 1) + # ⑨ 再加 elite2 行、base_ref 改指它 → dangling_base 且 message 含「elite」 + var rows9 := _legal_rows() + rows9.append(_mk_elite_row(1, 0, "goblin_elite2", "goblin", [{"skill_id": "heavy", "weight": 2}])) + rows9[2]["base_ref"] = "goblin_elite2" + var errs9: Array = _validate_rows(rows9) + var has_elite_base := false + for e in errs9: + if String((e as Dictionary).get("category", "")) == "dangling_base" \ + and String((e as Dictionary).get("message", "")).contains("elite"): + has_elite_base = true + Kit.check("⑨ base_ref 指向 elite 行报 dangling_base 且 message 含 elite〔精英批 V1〕", has_elite_base) + # ⑩ pool_extra 含基础池已有 lunge → dup_key + var rows10 := _legal_rows() + rows10[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 2}, {"skill_id": "lunge", "weight": 1}] + Kit.check("⑩ 附加技与基础池重复报 dup_key〔精英批 V1〕", _count_cat(_validate_rows(rows10), "dup_key") >= 1) + # ⑪ pool_extra 条目权重 0 → domain 且 message 含 pool_extra + var rows11 := _legal_rows() + rows11[2]["pool_extra"] = [{"skill_id": "heavy", "weight": 0}] + var errs11: Array = _validate_rows(rows11) + var has_extra_domain := false + for e in errs11: + if String((e as Dictionary).get("category", "")) == "domain" \ + and String((e as Dictionary).get("message", "")).contains("pool_extra"): + has_extra_domain = true + Kit.check("⑪ pool_extra 权重越域报 domain 且定位含 pool_extra〔精英批 V1〕", has_extra_domain) + + +func _validate_rows(rows: Array) -> Array: + var t = EnemyTableScript.new() + t.rows.assign(rows) + return t.validate() + + +# ---- R2:有效池=基础池+附加技,基础池取深度带最小行(深带行在前防取首行假绿)---- +func _test_effective_pool_merge() -> void: +# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 精英有效池=同关战斗行类基础敌种深度带最小行的池+附加技 + var rows := [ + _mk_row(1, "battle", 3, "goblin"), # 深带行在前(取首行的错误实现必红) + _mk_row(1, "battle", 0, "goblin"), + _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]), + ] + rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}] + rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] + var t = EnemyTableScript.new() + t.rows.assign(rows) + var got := {} + for e in t.effective_pool(rows[3]): + got[String(e.get("skill_id", ""))] = int(e.get("weight", 0)) + Kit.check("精英有效池=浅带基础池+附加技({attack9,lunge1,heavy2}——非深带行池)〔精英批 V2〕", + got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 3) + var got_b0 := {} + for e in t.effective_pool(rows[1]): + got_b0[String(e.get("skill_id", ""))] = int(e.get("weight", 0)) + Kit.check("battle 行有效池=自身 pool(读法等价面——{attack9,lunge1})〔精英批 V2〕", + got_b0.get("attack", 0) == 9 and got_b0.get("lunge", 0) == 1 and got_b0.size() == 2) + + +# ---- R3:继承不复制——基础池改表传到精英、改写返回数组不回灌 ---- +func _test_base_pool_change_propagates() -> void: +# oracle: design/gdd/systems/战斗循环/敌人系统.md:149 继承不复制——基础池调参自动传到精英 + var rows := [ + _mk_row(1, "battle", 3, "goblin"), + _mk_row(1, "battle", 0, "goblin"), + _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]), + ] + rows[0]["pool"] = [{"skill_id": "attack", "weight": 5}, {"skill_id": "charge", "weight": 2}] + rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] + var t = EnemyTableScript.new() + t.rows.assign(rows) + var first: Array = t.effective_pool(rows[3]) + for e in first: + if String((e as Dictionary).get("skill_id", "")) == "attack": + e["weight"] = 99 # 改写返回数组条目——表内行不得被回灌 + rows[1]["pool"][0]["weight"] = 7 # 基础行(depth_band==0)attack 权重改 7 + var got := {} + for e in t.effective_pool(rows[3]): + got[String(e.get("skill_id", ""))] = int(e.get("weight", 0)) + Kit.check("基础池改权重 7 传到精英有效池(按需现算零缓存)〔精英批 V3〕", got.get("attack", 0) == 7) + Kit.check("改写返回数组不回灌表内行(基础行 attack 仍=7)〔精英批 V3〕", + int(rows[1]["pool"][0]["weight"]) == 7) + + +# ---- R4:合并池过池构成纪律(跨表件校验对象=合并后的池)---- +func _test_merged_pool_composition() -> void: +# oracle: design/gdd/systems/战斗循环/敌人系统.md:150 池构成纪律校验对象是合并后的池 + var skills := SkillTableScript.new() + skills.rows.assign([ + {"skill_id": "attack", "type": "basic"}, + {"skill_id": "lunge", "type": "damage"}, + {"skill_id": "heavy", "type": "damage"}, + {"skill_id": "harden", "type": "buff"}, + {"skill_id": "charge", "type": "buff"}, + ]) + var status := StatusTableScript.new() + status.rows.assign([]) # 空状态表——buff 无 buff_ref 可解析时 cast_side 不报(cross_validator :179-180) + # 合规精英:基础 attack/lunge+附加 heavy → 恰一普攻/增益≤1/无条件特殊≥1 全合规 + var enemy_ok = EnemyTableScript.new() + enemy_ok.rows.assign([ + _mk_row(1, "battle", 0, "goblin"), + _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]), + ]) + enemy_ok.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] + enemy_ok.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池(纪律对全行生效——本组只数精英违规) + var errs_ok: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_ok, status) + Kit.check("合规精英合并池 pool_composition=0(实得 %s)" % str(errs_ok), _count_cat(errs_ok, "pool_composition") == 0) + # 附加第二普攻:基础 attack+附加 attack → basic=2 → pool_composition=1 且 message 含 goblin_elite + var enemy_dup_basic = EnemyTableScript.new() + enemy_dup_basic.rows.assign([ + _mk_row(1, "battle", 0, "goblin"), + _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "attack", "weight": 2}]), + ]) + enemy_dup_basic.rows[0]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] + enemy_dup_basic.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池 + var errs_dup: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_basic, status) + Kit.check("附加第二普攻报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕", + _count_cat(errs_dup, "pool_composition") == 1 and _msgs(errs_dup).any(func(m: String) -> bool: return m.contains("goblin_elite"))) + # 附加第二增益:基础 attack/lunge/harden+附加 charge → buff=2 → pool_composition=1 + var enemy_dup_buff = EnemyTableScript.new() + enemy_dup_buff.rows.assign([ + _mk_row(1, "battle", 0, "goblin"), + _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "charge", "weight": 2}]), + ]) + enemy_dup_buff.rows[0]["pool"] = [ + {"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}, {"skill_id": "harden", "weight": 2}] + enemy_dup_buff.rows[1]["pool"] = [{"skill_id": "attack", "weight": 9}, {"skill_id": "lunge", "weight": 1}] # boss 行自带合规池 + var errs_buff: Array = ValidatorScript.new().validate_skill_pool_links(skills, enemy_dup_buff, status) + Kit.check("合并后增益超 1 报 pool_composition=1 且 message 含 goblin_elite〔精英批 V4〕", + _count_cat(errs_buff, "pool_composition") == 1 and _msgs(errs_buff).any(func(m: String) -> bool: return m.contains("goblin_elite"))) + + +func _msgs(errs: Array) -> Array[String]: + var out: Array[String] = [] + for e in errs: + out.append(String((e as Dictionary).get("message", ""))) + return out + + +# ---- R5:暂不接线——表里有 elite 行时寻址拿不到它(行类门改动特有判据)---- +func _test_elite_unreachable_by_encounter() -> void: +# oracle: design/gdd/systems/战斗循环/敌人系统.md:151 精英行接线前不会被任何节点读到 + _diags.clear() + var resolver = ResolverScript.new( + func(level, category, message) -> void: + _diags.append("%s/%s/%s" % [str(level), str(category), str(message)]), + func() -> Array: return [], + func() -> Array: return [ + _mk_row(1, "battle", 0, "goblin"), + _mk_row(1, "boss", 9, "ogre_chief"), + _mk_elite_row(1, 0, "goblin_elite", "goblin", [{"skill_id": "heavy", "weight": 2}]), + ], + func(): return null, + ) + # ① 请求 elite → 空数组且诊断含 fatal 与 request_oob(只改枚举不改行类门时此请求会返回 goblin_elite——判据绑行类门改动) + var hit: Array = resolver.resolve_enemy_rowset(1, 0, "elite") + var has_fatal_oob := false + for d in _diags: + if d.contains("fatal") and d.contains("request_oob"): + has_fatal_oob = true + Kit.check("请求 elite 返回空且诊断含 fatal/request_oob〔精英批 V5〕", hit.is_empty() and has_fatal_oob) + # ② 同夹具 battle/boss 查询返回行中 elite 计数=0(非空前置:合计行数>0) + var battle_rows: Array = resolver.resolve_enemy_rowset(1, 0, "battle") + var boss_rows: Array = resolver.resolve_enemy_rowset(1, 9, "boss") + var elite_n := 0 + for r in battle_rows + boss_rows: + if String((r as Dictionary).get("row_kind", "")) == "elite": + elite_n += 1 + Kit.check("battle/boss 查询合计>0 且零 elite 行(非空前置在判据内)〔精英批 V5〕", + battle_rows.size() + boss_rows.size() > 0 and elite_n == 0) + + +# ---- R6:生产表地精精英落表(真实加载零错+行值逐位+有效池字典)---- +func _test_production_goblin_elite() -> void: +# oracle: design/gdd/systems/战斗循环/敌人系统.md:147 精英=敌人表行类为 elite 的独立一行 + var db = load("res://core/config_db.gd").new() + var ok: bool = db.load_tables("res://data/tables", true) + Kit.check("生产表 ConfigDB.load_tables 装载(is_ready=%s)〔精英批 V6〕" % str(db.is_ready), ok and db.is_ready) + Kit.check("生产表加载 error 级诊断计数=0(实得 %d)〔精英批 V6〕" % db.diagnostics_count("error"), + db.diagnostics_count("error") == 0) + var elite_rows: Array = [] + for row in db.combat.enemy.rows: + if String(row.get("enemy_id", "")) == "goblin_elite": + elite_rows.append(row) + Kit.check("goblin_elite 行恰 1 条(实得 %d)〔精英批 V6〕" % elite_rows.size(), elite_rows.size() == 1) + if elite_rows.is_empty(): + return + var r: Dictionary = elite_rows[0] + var checks := { + "level_index": 1, "row_kind": "elite", "depth_band": 0, + "display_name": "精英·地精", "hp": 20, "atk": 2, "atk_min": 1, "atk_max": 3, + "armor_class": 2, "armor_type": "light", "unit_count": 1, "base_ref": "goblin", + } + var bad: Array[String] = [] + for k in checks: + if r.get(k) != checks[k]: + bad.append("%s=%s≠%s" % [k, str(r.get(k)), str(checks[k])]) + Kit.check("行值 12 字段逐位相等(失配项 %s)〔精英批 V6〕" % [bad if not bad.is_empty() else "无"], bad.is_empty()) + Kit.check("无 unit_count_max 键、无 pool 键(精英行形态面)〔精英批 V6〕", + not r.has("unit_count_max") and not r.has("pool")) + var got := {} + for e in db.combat.enemy.effective_pool(r): + got[String(e.get("skill_id", ""))] = int(e.get("weight", 0)) + Kit.check("有效池字典={attack9,lunge1,desperate_guard1,heavy2}(实得 %s)〔精英批 V6〕" % str(got), + got.get("attack", 0) == 9 and got.get("lunge", 0) == 1 and got.get("desperate_guard", 0) == 1 and got.get("heavy", 0) == 2 and got.size() == 4) 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 91c09323..f7247ec6 100644 --- a/game/tests/unit/data_config/table_structure_guard_test.gd +++ b/game/tests/unit/data_config/table_structure_guard_test.gd @@ -1,7 +1,7 @@ # tests/unit/data_config/table_structure_guard_test.gd — 表结构守卫单元测试宿主 # 断言面 = data/tables 各表的结构锚与守卫,七套: # adventure 冒险表(档位配额/件值对/同调目录 12 件/装配→战斗贯通) -# enemy 敌人表(29 行带键行序/混编行集/三 Boss 行) +# enemy 敌人表(30 行带键行序/混编行集/三 Boss 行/精英行) # AC 单轨与掷点参数(status_effect/reward/settlement 三处无双轨/免伤墙护栏/敌 AC 带宽) # variation 地形变异(行数/候选池门控/跨字段互锁负样本) # difficulty 难度表(空表零行为/修饰套乘/越域 clamp) @@ -208,18 +208,18 @@ func _smoke_adventure_table() -> void: v18_ok and locked_n == 12) -# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:29 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单)---- +# ---- 敌人表深度带键结构(迭代 34 建·L1 混编批换代:30 行键序=覆盖表序·(1,3) 混编行集·Boss 行集带键清单·精英行)---- func _smoke_enemy_bands() -> void: # oracle: design/gdd/systems/冒险循环/难度曲线与Boss.md:316 难度与敌人带校验(敌人带组) - print("--- 敌人表深度带键结构(29 行键序 · (1,3) 混编 · Boss 带键覆盖表)---") - # ① 29 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前) + print("--- 敌人表深度带键结构(30 行键序 · (1,3) 混编 · Boss 带键覆盖表 · 精英行)---") + # ① 30 行计数 + 键序 = 覆盖表序(文件行序不变——行序即契约;(1,3) 两行相邻、熊地精在前;精英行随精英专属池批) # 迭代 35 生成图深度扩面覆盖批随动:战斗带顶扩到各关最大生成深度(L1→6/L2→9/L3→13)、 # Boss 行按终点深度分带(L1 6/L2 8·9/L3 11·12·13),模板时代带键 4/9/9 全部在册不动 var expect_keys: Array[String] = [ "1|battle|0|goblin", "1|battle|1|goblin", "1|battle|2|bugbear", "1|battle|3|bugbear", "1|battle|3|goblin", "1|battle|6|bugbear", - "1|boss|4|ogre_chief", "1|boss|6|ogre_chief", + "1|boss|4|ogre_chief", "1|boss|6|ogre_chief", "1|elite|0|goblin_elite", "2|battle|0|bugbear", "2|battle|1|bugbear", "2|battle|4|giant_spider", "2|battle|5|bugbear", "2|battle|5|giant_spider", "2|battle|6|giant_spider", "2|battle|7|giant_spider", "2|battle|9|giant_spider", @@ -232,8 +232,8 @@ func _smoke_enemy_bands() -> void: for row in _db.combat.enemy.rows: got_keys.append("%d|%s|%d|%s" % [int(row.get("level_index", -1)), String(row.get("row_kind", "")), int(row.get("depth_band", -1)), String(row.get("enemy_id", ""))]) - Kit.check("敌人表 29 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前)〔L1混编 v1〕", - _db.combat.enemy.rows.size() == 29 and got_keys == expect_keys) + Kit.check("敌人表 30 行且键序 = 覆盖表序((1,3) 两行相邻、熊地精在前;精英行接 boss 行后)〔L1混编 v1〕", + _db.combat.enemy.rows.size() == 30 and got_keys == expect_keys) # ② (1,3) 混编构成与行序+静态最坏骰和+行值整行复用(R1-R4) var rowset13: Array[Dictionary] = _db.combat.enemy.resolve_rowset(1, 3, "battle") var ids13: Array[String] = [] @@ -386,6 +386,8 @@ func _smoke_ac_swap_guard() -> void: and l12_pool == {"attack": 6, "charge": 2, "heavy": 2}) var l1_totals := {} for row in _db.combat.enemy.rows: + if String(row.get("row_kind", "")) == "elite": + continue # 曲线护栏管遭遇曲线——elite 不进遭遇(敌人系统.md:151),不跳过则 band0 失真 if int(row.get("level_index", -1)) != 1: continue var cnt_lo: int = int(row.get("unit_count", 1)) diff --git a/reports/iter54-反哺点-2026-11-07.md b/reports/iter54-反哺点-2026-11-07.md index 13387792..363fa90c 100644 --- a/reports/iter54-反哺点-2026-11-07.md +++ b/reports/iter54-反哺点-2026-11-07.md @@ -13,4 +13,8 @@ (未列项核对:camp.md §顶栏 与 art-bible §6.2 —— α0.80/栏高 48/色值零改值,按卡 2 §6「未改值不列」不列;HUD框架 §提示与弹层·其三「四处共用同一件同一序」——实装后句码一致,无发现不列;architecture.md :168 D1 HUD 视图单元接口——CampHudBand 仍为信号外发+setter 只读注入形态、新出口为 UiKit 工厂面(工厂单点先例在案),未失真不列。) -## 卡 3|精英专属池——(随卡 3 推进补齐) +## 卡 3|精英专属池——一条 + +1. **`design/gdd/03-内容规模.md` 第 66 行「精英节点与词缀系统——只预留了字段位」**:精英怪本体已落敌人表精英行(行类 elite+池继承合并附加)——「只预留字段位」的表述落后于实况;建议改成「精英怪本体已落敌人表(行类+池继承附加),精英节点与投放仍未接线」。依据=本批实现(enemy.tres (1,elite,0,goblin_elite) 行+卡 3 对照表 R1–R6 全绿);由文档批落文。 + +(未列项核对:敌人系统.md v1.9——实现与文档零出入(V6 生产表 12 字段逐位+有效池四键精确);数据配置.md 加载期错误类别表——dangling_base 属「悬空引用」既有类别的实例,表述不变,不列。) diff --git a/reports/iter54-实现报告-2026-11-07.md b/reports/iter54-实现报告-2026-11-07.md index e733570b..4feaab37 100644 --- a/reports/iter54-实现报告-2026-11-07.md +++ b/reports/iter54-实现报告-2026-11-07.md @@ -101,6 +101,46 @@ 9. **T1.5 `--quick` 闸壳步以探针直跑代证**:改完即测铁规频率口径(轮内不跑闸壳全套含 --quick)下,T1.4 探针直跑即该件最窄确认(同一判据同源读数),未另起 --quick 闸壳跑批。 10. **T3.0 读数实测 48.0**(卡面 L3 第 5 条警戒项未触发)——顶格落座前提成立,R5⑤ 判据照卡落装。 -## 卡 3|精英专属池——状态:待开工 +## 卡 3|精英专属池——状态:已完成 -(随卡 3 任务卡推进补齐。) +### 各任务卡状态 + +| 任务卡 | 状态 | 摘要 | +|---|---|---| +| T1 敌人表 schema | 完成 | ROW_KINDS 增 elite+ENCOUNTER_ROW_KINDS 行类门常量+_min_band_row+effective_pool+_validate_pool_entries 提取(pool 文案逐字不变)+_validate_elite_fields/_validate_elite_refs(dangling_base 新类别)+_extra_overlap_errs;新套件 elite_row_test 入册;config_db_test fv7 样本换代〔T1+T2 合并提交〕 | +| T2 跨表纪律+寻址门 | 完成 | cross_validator :194 改读 effective_pool(合并池过纪律);resolver :41 行类门改读 ENCOUNTER_ROW_KINDS(文案保持 {battle, boss}) | +| T3 地精精英落表+存量同步 | 完成 | enemy.tres 新行 (1,elite,0,goblin_elite)(卡面 §2 表值逐字段)+尾批注 provisional 来源行;round_flow 七处读法换代+expect_pools 增键+文案换代;guard 行数 30+键序插行+l1_totals skip elite;shop 还原断言改相对基数〔提交 e538e2cb〕 | +| T4 图鉴导出 | 完成 | compendium_dump 导出行数组 elite 副本写 effective_pool(表内行零改写);gen.py 词表三处〔提交 f4865ad8〕 | +| T5 产出物落位 | 完成 | 本报告卡 3 段+反哺点+能力行名实核对 | + +### 实际对照表(R1–R7,补「结果」格) + +| 行 | 验收测试 | 结果 | +|---|---|---| +| R1 elite 行类合法与字段互锁、引用校验 | elite_row_test._test_elite_validation_rejects〔精英批 V1〕 | 绿——合法夹具零错+十一违例各「含」预期类别(cross_field×7/dangling_base×2/dup_key×1/domain+pool_extra 定位×1);config_db_test fv7 样本换代后直跑绿 | +| R2 有效池=基础池+附加技、基础池取深度带最小行 | _test_effective_pool_merge〔精英批 V2〕 | 绿——深带行在前夹具下 {attack9,lunge1,heavy2}(取首行错误实现必红);battle 行读法等价面 {attack9,lunge1} | +| R3 继承不复制 | _test_base_pool_change_propagates〔精英批 V3〕 | 绿——基础池权重改 7 传到精英;改写返回数组条目后表内行不变 | +| R4 合并池过池构成纪律 | _test_merged_pool_composition〔精英批 V4〕 | 绿——合规精英计数 0;附加第二普攻/第二增益两夹具各计 1 且 message 含 goblin_elite | +| R5 暂不接线:寻址拿不到 elite | _test_elite_unreachable_by_encounter〔精英批 V5〕 | 绿——请求 elite 返回空+诊断含 fatal/request_oob;同夹具 battle/boss 查询合计>0 且零 elite 行 | +| R6 地精精英落表且全表加载零错 | _test_production_goblin_elite〔精英批 V6〕 | 绿——error 级诊断计数 0;行恰 1 条、12 字段逐位、无 unit_count_max/pool 键;有效池 {attack9,lunge1,desperate_guard1,heavy2} | +| R7 存量生产表冒烟随新行同步 | round_flow/guard/shop/roll_multiplier/trigger_cond 五件直跑 | 绿——五件 exit=0 零 SCRIPT ERROR;既有断言原判据照旧(精确键集/行数精确等/键序逐位/注入全量还原),仅扩期望集与读法 | + +### 卡 3 批末读数(T3.8 里程碑出口) + +`bash tools/run_all_gates.sh --code`=**【验收通过】闸况与基线一致,无新增红**(冒烟/单元与集成/系统/场景探针/渲染闸/静态扫描六闸全绿)——卡 3 全改动面(schema+core+enemy.tres+存量测试+图鉴工具)全档回归在案。 + +### 实机目检总清单(卡 3) + +**零项**——理由:精英暂不接线(敌人系统.md:151),无玩家可见面(卡 3 §8 测试方式两分法申报在案)。 + +### 遗留读数义务(卡 3) + +- 精英 EV 重锚与数值终值 → 难度曲线与Boss(Alpha 阶段零读数;本批零 MC——卡 3 §5 申报)。 +- 开战快照改读有效池(敌人系统.md:325 半句)→ 待办清单「战斗节点遭遇编排方式」条目承接(本批无入口可验)。 + +### 实现决定记录(卡 3 新增条目) + +11. **T2.3 夹具的 ogre_chief 行补合规池**:卡面 T2.3 夹具规格未点 boss 行池值——_mk_row 默认池(仅普攻)自身违反「至少一行无条件特殊技」纪律,pool_composition 误红。boss 行补 attack+lunge(自身合规),断言只数精英违规——夹具 mechanics 决策(白名单「夹具助手函数的拆法」族),V4 判据面不变。 +12. **round_flow 七处转换的批次内第 4 处豁免核**:卡面申报「另两处遍历(:571-575、:791-794)有前置过滤」;实查另核卡面未列的 :844(resolve_rowset 结果池读取)——经行类门 resolver elite 行结构性不可达,同豁免逻辑,不改不改判据。全文件 row.pool 直取残留恰 1 处(:574)与卡面申报一致。 +13. **expect_pools 键与文案的完成形态**:卡面 T3.4 定「:623 文案改『三十行』」;实改「三十行十一种池模板」——池模板计数同句失真(精英合并池为第 11 种),一并换代。键插入位置=字典尾(顺序无关,判据面=精确键集比对)。 +14. **S13 基线零新键确认**:enemy_table.gd/config_db_cross_validator.gd/config_db_resolver.gd 三件静态扫描零新增键(卡 3 §13 预算全数守住——validate() 局部变量 10/10 零新增、各私有函数分支在预算内),--code 跑闸静态扫描段绿为凭。 diff --git a/tools/compendium_gen.py b/tools/compendium_gen.py index c81b918e..c3fd017a 100644 --- a/tools/compendium_gen.py +++ b/tools/compendium_gen.py @@ -31,7 +31,7 @@ SKILL_REQUIRED = ("skill_id", "display_name", "type", "cooldown_beats") # 以下中文名均为表 schema 封闭枚举的展示词(enemy_table.gd / skill_table.gd),非玩法数值 LEVEL_CN = {1: "第一关", 2: "第二关", 3: "第三关"} -ROW_KIND_CN = {"battle": "战斗行", "boss": "Boss行"} +ROW_KIND_CN = {"battle": "战斗行", "boss": "Boss行", "elite": "精英行"} ATTACK_TYPE_CN = {"normal": "普通", "pierce": "穿刺", "magic": "魔法"} ARMOR_TYPE_CN = {"light": "轻甲", "ward": "魔法护甲", "heavy": "重甲"} SKILL_TYPE_CN = {"basic": "普攻", "damage": "伤害", "buff": "增益", "debuff": "妨害"} @@ -46,6 +46,7 @@ FIELD_CN = { "display_name": "显示名", "unit_count": "单位数", "unit_count_max": "单位数上限", "hp": "生命", "atk": "攻击基线", "atk_min": "攻击下限", "atk_max": "攻击上限", "attack_type": "攻击类型", "armor_type": "护甲类型", "armor_class": "护甲等级", + "base_ref": "基础敌种", "pool_extra": "附加技", "skill_id": "技能ID", "type": "类型", "damage_mult": "伤害倍率", "rider_mult": "必中附加分量", "cooldown_beats": "冷却拍数", "trigger": "触发条件", "buff_ref": "增益引用", "buff_duration": "增益持续", } @@ -119,7 +120,7 @@ def render_enemy_card(row: dict, skill_by_id: dict) -> str: f'{esc(badge)}') out.append('') for key in row: - if key == "pool": + if key in ("pool", "pool_extra"): continue out.append(f'' f'') From 5358880072cb7298a89d4ff96c30d033424a9e35 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Sat, 7 Nov 2026 03:16:59 +0800 Subject: [PATCH 4/4] =?UTF-8?q?iter54=20=E7=8B=AC=E7=AB=8B=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E5=AE=A1=E8=AE=A1=E6=8A=A5=E5=91=8A:=20=E6=9C=AA?= =?UTF-8?q?=E5=8F=82=E4=B8=8E=E5=AE=9E=E7=8E=B0=E5=AD=90=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E5=AE=A1=E8=AE=A1=E2=80=94=E2=80=94=E5=8D=A1?= =?UTF-8?q?1=20=E4=B8=AD=E6=A1=A3=20R1-R9=20=E9=80=90=E8=A1=8C=E9=80=9A?= =?UTF-8?q?=E8=BF=87(=E6=AD=BB=E6=8E=A5=E7=BC=9D=E5=8F=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=20grep=20=E5=AE=A1=E8=AE=A1=E8=87=AA=E8=B7=91?= =?UTF-8?q?=E5=8F=8C=E9=9B=B6+41=20=E5=A4=84=E5=85=A8=E7=BB=8F=20ProbeUiUt?= =?UTF-8?q?il=20=E6=97=A0=E7=AC=AC=E4=BA=8C=E4=BB=BD=E5=90=88=E6=88=90?= =?UTF-8?q?=E4=BB=B6+=E5=88=A0=E9=99=A4=E6=96=AD=E8=A8=80=200=20=E6=9D=A1)?= =?UTF-8?q?/=E5=8D=A12=20=E4=B8=AD=E6=A1=A3=20R1-R8=20=E9=80=90=E8=A1=8C?= =?UTF-8?q?=E9=80=9A=E8=BF=87(=E5=85=81=E8=AE=B8=E6=94=B9=E5=8A=A8=200=20?= =?UTF-8?q?=E6=9D=A1=E6=88=90=E7=AB=8B+to=5Fhtml=20=E5=BD=A2=E6=80=81?= =?UTF-8?q?=E5=90=88=E8=A7=84+=E5=87=BA=E5=8F=A3=E5=8D=95=E5=BD=A2?= =?UTF-8?q?=E5=8F=82+13=20=E5=A4=84=E7=A6=81=E6=94=B9=E9=9A=90=E8=97=8F?= =?UTF-8?q?=E7=82=B9=E5=9C=A8=E4=BD=8D)/=E5=8D=A13=20=E9=AB=98=E6=A1=A3?= =?UTF-8?q?=E5=85=A8=E9=87=8F(=E7=AA=84=E5=A4=8D=E8=B7=91=E8=87=AA?= =?UTF-8?q?=E8=B7=91=20elite=5Frow=5Ftest=2027/27=20PASS+config=5Fdb=5Ftes?= =?UTF-8?q?t=2029=20checks=20green+4=20=E6=9D=A1=E7=94=B3=E6=8A=A5?= =?UTF-8?q?=E6=94=B9=E5=8A=A8=E4=B8=8E=20diff=20=E9=80=90=E6=9D=A1?= =?UTF-8?q?=E7=9B=B8=E7=AC=A6+=E5=A5=91=E7=BA=A6=E5=9B=9B=E6=9D=A1?= =?UTF-8?q?=E5=85=A8=E8=BF=87+oracle=20=E6=8A=BD=E6=9F=A5=205=20=E6=9D=A1?= =?UTF-8?q?=E9=9D=9E=E9=95=9C=E5=83=8F)=E2=80=94=E2=80=94=E6=80=BB?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E9=80=9A=E8=BF=87;=20=E6=9C=AA=E9=98=BB?= =?UTF-8?q?=E5=A1=9E=E8=A7=82=E5=AF=9F=E4=B8=A4=E6=9D=A1=E5=9C=A8=E6=A1=88?= =?UTF-8?q?(=E5=8D=A12=20T2=20=E5=BC=95=E7=94=A8=E5=93=88=E5=B8=8C?= =?UTF-8?q?=E7=AC=94=E8=AF=AF/=E8=A6=86=E7=9B=96=E7=99=BB=E8=AE=B0?= =?UTF-8?q?=E4=BB=B6=20BaseButton-only=20=E7=BB=93=E6=9E=84=E5=B1=80?= =?UTF-8?q?=E9=99=90=E5=BD=92=20B5=20=E6=97=A2=E6=9C=89=E6=9D=A1=E7=9B=AE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- reports/iter54-验收审计-2026-11-07.md | 169 ++++++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 reports/iter54-验收审计-2026-11-07.md diff --git a/reports/iter54-验收审计-2026-11-07.md b/reports/iter54-验收审计-2026-11-07.md new file mode 100644 index 00000000..0b5de566 --- /dev/null +++ b/reports/iter54-验收审计-2026-11-07.md @@ -0,0 +1,169 @@ +# 迭代 54 独立验收审计报告(2026-11-07) + +> **审计者身份**:独立验收审计子代理——未参与本批实现的只读会话,全量凭据自取(diff 逐笔自读、grep 自跑、窄复跑自跑),未采信任何报告自述的「已全绿」。 +> **对象**:分支 `iter54`(工作区 `D:\code\g-plan2-iter54`),基底=master `a2e500a4`,三笔卡提交:`96123978`=卡 1(QA 缺陷收口批)、`4c424af3`=卡 2(营地交互修复批)、`f9c9caf5`=卡 3(精英专属池)。原史(逐 T 步提交与当轮跑批读数)在 `backup/iter54-20261107-030512` 与 `iter54-premerge-backup` 两分支在案。 +> **审计输入**:三张需求卡片(reports/需求卡片-QA缺陷收口批-2026-11-06.md/需求卡片-营地交互修复批-2026-11-05.md/需求卡片-精英专属池-2026-11-06.md)+实现报告+反哺点+三笔提交完整 diff。 +> **执行证据口径(申报在案)**:卡 1/卡 2 为中档审计,最窄复跑省略——执行证据=逐 T 步提交信息内的当轮跑批读数(`iter54-premerge-backup` 在案,T1–T7 每步读数齐全)+合回后远端 CI 现查(合回落地后按 process-conventions §1 第 3 条现查)。卡 3 为高档审计,窄复跑由审计者本机自跑(§3.5 原样读数)。 + +--- + +## 卡 1|QA harness 缺陷与覆盖遗漏收口批——判定:通过 + +### 1.0 申报验收档与实际执行深度 + +卡面申报验收档=中。实际执行:对照表 R1–R9 全量逐行核对+测试削弱检查(diff 删除行全扫)+内部字段直查受控例外核对逐条+死接缝删除判据(双模式 grep)审计者自跑。窄复跑按中档口径省略(证据口径见头部申报)。 + +### 1.1 对照表逐行核对(R1–R9) + +| 行 | 核对结论 | 证据 | +|---|---|---| +| R1 终局结算框关闭动线零引擎报错 | 通过 | close() 与 _play_celebrate() 两处均改 `has_meta` 前置判断(battle_end_overlay.gd,diff 确认 :191–198/:329–336 两处,语义=有则必杀、无则跳过);验收形态按卡面自陈=grep 读数(「该缺陷唯一提交面=报错打印本体」),当轮读数〔T1 提交 57f3cc75〕=fx_probe/screen_flow_probe 各 exit=0、SCRIPT ERROR 0、celebrate_tw 0 | +| R2 停手钮真输入命中与落效 | 通过 | `_stoptest_tick`+`_stoptest_assert_hit` 在案(ui_click_probe.gd:1172–1236),断言①停手键命中登记、②相位离开 Active、120 帧超时红、终局竞态静默弃测守卫,尾缀〔QA收口 V2〕grep 命中 2 处;驱动=`_click_stop()`(`ap_bar._btn_stop` 真钮 push_input);当轮读数〔T2 提交 816aaae5〕=ui_click_probe exit=0、问题=0、停手键命中 1 次 | +| R3 journey 停手+方法口清零 | 通过(候选键一项按申报例外记) | journey_player 停手行为(每战至多一次,Active+停手钮使能+本轮已挖 ≥1+盘面有挖)+8 处方法口转换逐处核对在 diff(:380→node_overlay._btn 真点、:441→candidate_rect(0) 矩形中心、:449/:451/:452/:454/:455/:457→curio 真钮/bcard 矩形中心);头注「方法口已清零」换代在案。当轮读数〔T3 提交 67f16cc2〕=fatal 0、停手键命中 6 次、停点键在场、屏到达 16 类含战斗/终局、exit=1 为已知缺陷 A1 基线态(卡 1 §5 明示不收件,卡 2 承接)。「候选键在场」未产出——登记件登记通道=BaseButton pressed+营地自绘物件两源,地图画布与 BCard 非 BaseButton(结构性不可产出),实现报告决定记录 5 申报为观测面局限;效力承载面=屏到达 16 类+段间推进(当轮在跑批执行)。审计认定:判据字面一项偏差已申报且有替代效力证据,不构成缺漏 | +| R4 停点链真输入化(16 处) | 通过 | 逐处清点 diff:候选 2(:426/:536→push_click_at(candidate_rect))+节点继续 2(:448/:656→push_click(node_overlay._btn))+三选一 10(:473/:475/:476/:478/:479/:481/:649/:651/:652/:654→bcard 矩形中心/真钮)+弃图刷新链 2(:972→`_btn_refresh`、:975→`_confirm_row` 内 Button 子件)=16,全部经 ProbeUiUtil;既有落点断言删除行全扫=0 条。当轮读数〔T4 提交 2067461f〕=exit=0、问题=0、覆盖 43 命中/30 控件较改前 39/28 不回退、新增停点链键。弃图确认链四种子未触发(is_refresh_ready RAP≥8 门槛,改前同不可达)——申报在案,效力由 journey 真点链承载 | +| R5 到达型候选+停点(7 探针 11 处) | 通过 | 7 件 diff 逐件核对:fx_shot_probe:47/layout_probe:32/shot_probe:147/ui_probe:83/t17_defer_probe:39/ui_tune_probe:143(原 :109 行号就地校正)→ push_click_at(candidate_rect(0).get_center());ui_struct_probe:275(原 :257)→ push_click(node_overlay._btn);shot_probe:9 头注同步。当轮读数〔T5 提交 46493f73〕=headless 五件 exit=0+SCRIPT ERROR 0,窗口化两件 exit=0 | +| R6 调试浮窗面(4 处) | 通过 | ui_tune_probe:③应用→find_button 真点、④重置→find_button 真点、⑤模拟→`_sim_btn` 真点(折叠区滚轮滚入通道=test-conventions §2 现役条文,12 档上限+失败 problem 上报正面效力自检)、⑧应用→前置重开+真点=4 处全转真输入。当轮读数〔T5〕=ui_tune PASS 全链路(含 N=10 模拟输出与种子三分断言) | +| R7 系统测试宿主候选真输入化 | 通过 | system_test.gd:440(原 :433)→ push_click_at(candidate_rect(0).get_center()),`const ProbeUiUtil := preload("res://probe/probe_ui_util.gd")` 复用单点;当轮读数〔T6 提交 97d6b0a5〕=69 检查 0 失败三闸过(V3c 阶段真实选路提交在跑批中执行) | +| R8 三选一覆盖层单元真输入化(5 处) | 通过 | reward_overlay_test.gd 刷新/首卡/次卡/确认/继续 5 处真输入(bcard 矩形中心+真钮),断言语义逐位保持(diff 确认仅驱动行与消息文案换代,判定表达式零改);经 preload 复用 ProbeUiUtil。当轮读数〔T6〕=82 套件 all green、exit=0、0 FAIL | +| R9 死接缝同批清理(5 文件 11 方法) | 通过 | diff 确认删除:ap_bar_view 2+curio_pick_overlay 4+node_overlay 1+debug_overlay 3+level_map_screen 1=11。**审计者自跑双模式 grep**:定义清 `func press_(candidate\|continue\|card\|confirm\|refresh\|apply\|reset\|simulate)` 与调用清 `\.press_(…)`(--include=*.gd,全仓)双双零命中 | + +尾缀 grep 口径说明:〔QA收口 V2〕在码 2 处(T2 新断言的唯一在码尾缀);V1(grep 读数)、V3(journey 跑批读数)、V9(grep 判据)按卡面对照表自陈为读数型判据、无在码断言载体;V4–V8 验收载体=既有断言保持+当轮直跑读数(卡面未为转换行立新尾缀)。与卡面 §11 白名单「尾缀编号分配」一致,无缺漏。 + +### 1.2 测试削弱检查 + +- 三笔提交 diff 删除行全扫(`^-` 行筛 `_problems.append`/`Kit.check`/`check(`):卡 1 删除断言行=**0 条**。 +- 允许删除面(只验证被删 press_* 死接缝的护栏):实际无此类护栏被删——死接缝方法体内无断言。 +- 驱动行替换未触判定表达式;reward_overlay_test 仅消息文案「press_refresh 未外发」→「真点刷新未外发」换代,判定 `refreshed.size() == 1` 原样。 +- ui_click_probe 主循环冻结条件由 `_locktest_state != 1` 扩为 `_locktest_state != 1 and _stoptest_state != 1`——新增冻结窗,非弱化。 +- 判据不放宽面核对:无阈值改动、无断言删除、无跳过逻辑新增。 + +### 1.3 例外核对(内部字段直查受控例外逐条) + +- 产品码私取新增=0(A2 为视图内部容错;candidate_rect 为卡面 §13 申报的唯一新公开面,命中面与 `_candidate_at` 同源——审计比对 level_map_screen.gd:452 与 :478 的半宽常量 `NODE_HALF + Vector2(HIT_PAD, HIT_PAD)` 逐字节一致)。 +- 探针/测试面私有直读逐条:`ap_bar._btn_stop`/`_btn_refresh`/`_confirm_row`、`node_overlay._btn`、curio `_bcards`/`_confirm_btn`/`_continue_btn`、`debug_overlay._sim_btn`、登记件 `_recorder._node_keys`/`_hits`(决定记录 6 申报)——全部属探针族现役惯例(ui_click_probe 既有 `ap_bar._btn_stop` 同型先例),封装扫描件射程不含探针(presentation_encapsulation_sweep_test 只套五个 flow 件)。 +- tests 面合成输入件检查:game/tests 全目录 grep `InputEvent`=**零命中**;system_test 与 reward_overlay_test 均经 preload 复用 ProbeUiUtil(禁第二份合成输入件契约成立)。probe/sim 面持 push_input 的文件(ui_input_probe、screen_flow_probe 等)全部为改前既有件,本批零新增。 +- 改动面越界检查:卡 1 提交触面 17 文件=卡面 §7 结构图逐项+产出物 2 件,零越申报面。 + +--- + +## 卡 2|营地交互修复批——判定:通过 + +### 2.0 申报验收档与实际执行深度 + +卡面申报验收档=中。实际执行:对照表 R1–R8 全量逐行核对+测试削弱检查+允许改动 0 条既有断言核对+判据形态核对+UiKit 出口形参核对。窄复跑按中档口径省略(证据口径见头部申报)。 + +### 2.1 对照表逐行核对(R1–R8) + +| 行 | 核对结论 | 证据 | +|---|---|---| +| R1 大门无断点按「是」后开始层不可见 | 通过 | `_start_adventure()` 首句加 `ui.screens.start_overlay.visible = false`(main.gd:293,在 `ui.flow.close` 之前——契约 1 落点);`_start_adventure_from_menu()` 冗余句删除(:194 注释换代)。探针 R1 断言〔营地交互批 V1〕在案(ui_click_probe 大门段),真实入口=大门命中区真点→R4 推进钮真点→同链断言。当轮读数〔T1 提交 bf8d30c1〕=探针 exit=0、问题=0(修复前该句恒红判据零命中) | +| R2 开局落节点图选路停点 | 通过 | R2 断言〔营地交互批 V2〕(level_screen.visible)与既有 is_adventure 断言同批在案;await 1 帧→3 帧帧口径落实(T1 提交信息记「停点成形最小帧数实测=3 帧内成形」)。判据绑定面按卡面 R2 自陈=开局终态在位断言,特有提交面由 R1 承载——两行同批缺一即红结构保持 | +| R3 两/三钮形态位次=推进→次→维持 | 通过 | confirm_dialog.gd 三句 `add_child` 次序镜像为 确认→次→取消,对象创建、三句 connect、成员声明、open()/open_three() 签名零改动(diff 确认,契约 3);新套 confirm_dialog_test.gd=7 断言(双钮 3+三钮 4),判据=公开子节点序+Button 公开 text,零私有直读,双钮形态 child(2) 非 child(1) 硬知识与非空前置(子节点数=3)在案;入册 run_all.gd(+1 行)。当轮读数〔T2 提交 af127b15〕=直跑 exit=0 七断言全 PASS | +| R4 左位真点即推进 | 通过 | 探针 R4〔营地交互批 V3〕:真点前 `_confirm_consumer == "gate_new"` 前置读(探针既有直读先例)、按钮行 child(0) 全局矩形中心 push_click_at(非行矩形左位——卡面轮次 4 硬伤修正的落法)、真点后 is_adventure 翻转+consumer 清零。T2.3「前置重开」→「原位 R4」重构=实现报告决定记录 8 申报(主证绑定翻转才有判别力),既有确认断言原样保留。当轮读数〔T2〕=R4 在跑批执行且通过 | +| R5 顶栏栏体几何与徽章落座 | 通过 | `_smoke_camp_hud_band()` 接进 `_split_tail()` await 链(system_test.gd:47,非 _initialize——卡面 T3.4 硬伤修正落法);五钉:①`to_html(true) == "6e4526cc"`(禁浮点分量直等判据形态成立,全文件无 `.a == 0.80` 型断言)②铺满(栏体矩形对顶栏矩形等值+高 48)③IGNORE④栏体为第一子件⑤徽章顶格(y 对齐栏顶)+右位(end.x=栏右缘−12),几何容差 0.51。T3.0 前置读数=徽章 min 高实测 48.0(L3 第 5 条未触发,决定记录 10)。当轮读数〔T3 提交 8f2eab7c〕=76 检查 0 失败 | +| R6 栏体对落位带底色对比 ≥3:1 | 通过 | `_test_hud_band_contrast_over_bg()`:规格侧算式——测试内写死 `#6E4526`/α0.80(Color(0x6e/255.0, …) 字面构造),**未把视图件 color 喂进算式**(oracle 镜像禁令成立);带高取 `camp_scene_layout_table.gd` 数据出口;双层全像素扫描+扫描面非空前置;WCAG ≥3.0。当轮读数〔T3〕=实测 4.29:1,与卡面复核推算一致 | +| R7 回归·件 1 未动三条动线 | 通过 | 既有大门两分支段与篝火段断言零改写(diff 确认无删除行);三处未改动隐藏点核在位:`_resume_from_menu()`(main.gd:246)、boot 默认(:137)、其余 11 处自管点全部原样(grep 全列与卡面 13 处清单一致)。当轮读数〔T1〕=既有断言原样通过 | +| R8 回归·件 2 路由与件数断言 | 通过 | 探针次钮隐藏断言(ui_click_probe.gd:739,〔营地图批 V3〕原文原样)与 system_test 三钮计数断言(:193–201,V2d 三钮在位+渲染面可见钮计数==3)均未被本批触碰(两笔 diff 删除行全扫=0)。位次改动后二者仍绿=契约 3 未被违。当轮读数=探针问题=0、系统测试 76/0 | + +### 2.2 测试削弱检查 + +- diff 删除断言行=**0 条**(三笔提交中卡 2 段全扫)。 +- camp_scene_layout_test 入口重构(单 quit 迁至 `_run_all` 统一收尾):断言集零增删,早退路径从 quit 改 return 后仍会被 `_run_all` 收尾 quit 接住,无静默跳过——非削弱。 +- 允许改动 0 条既有断言:成立。 + +### 2.3 例外与申报面核对 + +- 契约 1:开局隐藏点唯一(`_start_adventure()`),续玩唯一(`_resume_from_menu()`),禁改的 13 处自管点逐一 grep 在位,`camp_shop_presenter.gd:85` 语义句未动(契约 2)。 +- 契约 4:`camp_hud_band_surface(host)` 形参只有 host——色值、α、铺满、IGNORE、挂载、返回句柄六事全在体内(ui_kit.gd:666–676 审读);无 color/pass_input 形参(开了即违——未开);盘上三处同形铺面(ui_assembler.gd overlay_bg、ui_kit `_modal_dim`、page_shell 底板)零触碰(不在 diff 文件清单)。 +- 契约 5:IGNORE 在出口内写死;camp_hud_band.gd 内右对齐容器自身 IGNORE 同批落(漏一处即顶排点不动的双点纪律成立);本文件内 `ColorRect.new()` grep=零命中。 +- S13 基线修订:ui_kit max-public-methods 23→24——卡 2 §13 预申报的例外(「本条即申报与请求」+U0 批卡),diff 仅此一行;非越权改基线。 +- 改动面越界检查:提交触面=卡面 §7 结构图 9 件+run_all 入册+gdstyle_baseline+产出物 2 件,零越申报面;design/ 与两份根流程清单零触碰(契约 6)。 +- 能力行名实核对:README:158/:159 两行与实际落地(confirm_dialog_test 两函数、`_smoke_camp_hud_band`、`_test_hud_band_contrast_over_bg`)逐字相符。 + +--- + +## 卡 3|精英专属池——判定:通过(高档全量) + +### 3.0 申报验收档与实际执行深度 + +卡面申报验收档=高。实际执行:①对照表 R1–R7 逐行核对;②判据绑定面抽查;③测试削弱检查(含 4 条申报测试改动逐条 diff 核对);④直查例外核对+契约核四条;⑤窄复跑抽查——审计者自跑两件;⑥oracle 出处核对抽查 5 条。 + +### 3.1 对照表逐行核对(R1–R7) + +| 行 | 核对结论 | 证据 | +|---|---|---| +| R1 elite 行类合法与字段互锁、引用校验 | 通过 | elite_row_test `_test_elite_validation_rejects`=合法夹具零错+11 个违例夹具(①删 base_ref ②同名 ③pool_extra 空 ④删键 ⑤带 pool ⑥battle 带 base_ref ⑦battle 带 pool_extra ⑧悬空 ⑨指向 elite ⑩重叠 ⑪权重 0)各「含」预期类别,与卡面 T1.11 逐条对应;`_validate_elite_fields`/`_validate_elite_refs`/`dangling_base` 实装与 T1.7/T1.9 一致。config_db_test fv7 样本 miniboss 换代在 diff(申报第 1 条) | +| R2 有效池=基础池+附加技、基础池取深度带最小行 | 通过 | `_test_effective_pool_merge`:夹具深带行(band 3, attack5/charge2)在前、浅带行(band 0, attack9/lunge1)在后——取首行的错误实现必红;期望 {attack9,lunge1,heavy2}+size 3;battle 行读法等价面 {attack9,lunge1}。实装 `_min_band_row`(遍历取 band 最小)与 `effective_pool`(基础池条目+pool_extra 条目,均 duplicate)与 T1.3/T1.4 一致 | +| R3 继承不复制 | 通过 | `_test_base_pool_change_propagates`:基础行 attack 改 7 后再查=7(按需现算);改写首次返回数组条目为 99 后表内行仍=7(duplicate 隔离)。enemy_table.gd 类变量 grep=仍仅 `rows`+`_resolver` 两个,**零新增类变量**(契约 1) | +| R4 合并池过池构成纪律 | 通过 | `_test_merged_pool_composition`:合规精英 pool_composition=0;附加第二普攻/第二增益两夹具各=1 且 message 含 goblin_elite。boss 行补合规池=决定记录 11 申报(夹具 mechanics,`_mk_row` 默认单普攻池自身违纪律会误红);跨表件 `:194` 改读 `enemy_table.effective_pool(row)` 单行 diff(T2.1) | +| R5 暂不接线 | 通过 | `_test_elite_unreachable_by_encounter`:夹具 resolver 敌行回调返回含 elite 行的三行行集;①`resolve_enemy_rowset(1, 0, "elite")` 返回空+诊断含 fatal/request_oob——判据绑行类门改动(只改枚举不改为门时该请求会返回 goblin_elite);②battle/boss 查询合计>0 且零 elite 行(非空前置内嵌判据)。resolver :41 改读 `ENCOUNTER_ROW_KINDS`、文案保持「row_kind 域 {battle, boss}」逐字(T2.2);该常量全仓 grep 仅 resolver 一处消费(卡面 §12 单点申报成立);`unit_container.gd`/`battle_orchestrator.gd`/`level_runner.gd`/`level_table.gd` 零触碰(契约 2) | +| R6 地精精英落表且全表加载零错 | 通过 | `_test_production_goblin_elite`:生产表 load_tables(true) 后 error 级诊断=0;行恰 1 条;12 字段逐位断言表+无 unit_count_max/无 pool 键+有效池四键精确。enemy.tres diff:行插 (1,boss,6,ogre_chief) 之后、(2,battle,0,bugbear) 之前(T3.1),15 键中 12 判据字段与卡面 §2 表逐值一致(hp 20/atk 2/1/3/armor 2/light/unit 1/base_ref goblin/pool_extra heavy 2),attack_type normal 由 roll_multiplier_test 既有「全行 normal」断言覆盖 | +| R7 存量冒烟随新行同步、判据不放宽 | 通过 | 逐条核对见 3.3——行数 29→30 精确等、键序逐位插行、池模板精确键集比对、曲线护栏阈 ≤2.5 未动、注入还原改相对基数(加强);五件存量套件当轮读数〔T3 提交 75a4afe2〕=exit=0 零 SCRIPT ERROR | + +### 3.2 判据绑定面抽查 + +- V5 绑定改动特有面:判据①在「只做 T1.1 不做 T2.2」的反事实下必红(elite 请求会返回行而非空)——断言钉在行类门改动本身,非改动前后双绿的常真断言。审计按卡面反事实推演复核成立。 +- V2 绑定取带语义:夹具行序(深带在前)使「取首行」实现必红——钉在 `_min_band_row` 的取最小带逻辑,非常真。 +- V3 绑定零缓存:改表后再查+改写返回不回灌,两钉分别排除缓存实现与引用泄漏——绑契约 1。 +- V4 绑定合并池:单看基础行或单看附加技都不违规,唯合并后违规——钉在「纪律校验对象=合并池」。 + +### 3.3 测试改动申报 4 条逐条 diff 核对(无越面改判) + +| 申报条 | diff 实况 | 结论 | +|---|---|---| +| 1. config_db_test fv7 样本 elite→miniboss+文案 {battle, boss, elite} | 恰两处(样本行+断言消息),判定逻辑(domain+含 "row_kind")零改 | 相符 | +| 2. round_flow 七处 row.pool→effective_pool+expect_pools 增键+文案 | diff 恰 7 处读法换代(:530/:541/:560/:621/:627/:642/:657 区段)+expect_pools 增 `"1_elite_0_goblin_elite"` 键+文案「二十九行十种」→「三十行十一种」(十种→十一种为决定记录 13 申报的完成形态,池模板计数同句失真一并换代);期望字典判定 `got != expect_pools[rk]` 零改 | 相符 | +| 3. guard 行数 30+键序插行+l1_totals skip elite | 行数断言 `== 30` 精确等(未放宽为 ≥);expect_keys 在 `"1|boss|6|ogre_chief"` 后恰插 `"1|elite|0|goblin_elite"`;`:389` 循环首加 elite 跳过(两行);头注与 print 文案同步 | 相符 | +| 4. level_shop 还原断言改相对基数 | 注入前记 `base_n`,断言 `== base_n`,文案去写死数——卡面自陈「属加强」 | 相符(加强) | + +三笔提交删除断言行全扫:恰上述 4 条申报改判的旧行,无第 5 条。**无越面改判。** + +### 3.4 契约核(四条全过)与例外核对 + +- 契约 1 继承不复制:enemy_table.gd 类变量 grep=2 个(rows/_resolver),零新增;effective_pool 每次现算、返回条目 duplicate、零缓存零写回(V3 断言实证)。 +- 契约 3 算法唯一落点:合并算法 grep `pool_extra` 全仓仅 enemy_table.gd/elite_row_test/round_flow/compendium_dump 四处(后三处为消费/测试/导出面,非第二份算法);权重上限 9999 在 schemas 面仅 enemy_table.gd:171 一处(meta_tree 的 9999 为价格域既有件,非池权重)。 +- 契约 4 既有文案逐字不变:`_validate_row_pool` 四条报错文案经 field 参数化后对 field="pool" 渲染逐字节等值(「%s pool 条目非字典」「%s pool 条目 skill_id 空」「%s pool 内 skill_id 重复」「%s pool 权重越域 (0,9999]」——diff 左右对比确认);既有错误类别零改名,新增类别仅 dangling_base;`:88` row_kind 文案域换代=卡面 T1.1 申报项。 +- 直查例外:产品码零私取新增(新增公开面=effective_pool+ENCOUNTER_ROW_KINDS,均卡面 §12 申报);elite_row_test 全部经公开入口(validate/effective_pool/validate_skill_pool_links/resolve_enemy_rowset/load_tables)进出,零私有直读。 +- 残留读法豁免核:round_flow 残留 `row.pool` 恰 3 处(:574 种带过滤 row_kind=="battle"、:795 species 名单 ["goblin","bugbear","ogre_chief"] 精确等值不含 goblin_elite、:846 resolver battle 查询结构性不可达)——与卡面 §3.4+决定记录 12 一致,豁免成立。 +- 改动面越界检查:提交触面=卡面 §7 结构图 12 件+产出物,零越申报面;design/ 零触碰。 + +### 3.5 窄复跑读数(审计者本机自跑,2026-11-07) + +`Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://tests/unit/data_config/elite_row_test.gd`: + +- 退出码=**0**;`[PASS]` 计数=**27/27**(V1 合法夹具+①–⑪ 共 12、V2×2、V3×2、V4×3、V5×2、V6×6);`[FAIL]` 与 `SCRIPT ERROR` 扫描=零行。原样尾行: + `[elite_row_test] PASS — 精英行校验/有效池/继承不复制/合并池纪律/暂不接线/生产表落行 全过` + V6 关键 PASS 行原样:`生产表加载 error 级诊断计数=0(实得 0)〔精英批 V6〕`/`行值 12 字段逐位相等(失配项 无)〔精英批 V6〕`/`有效池字典={attack9,lunge1,desperate_guard1,heavy2}(实得 { "attack": 9, "lunge": 1, "desperate_guard": 1, "heavy": 2 })〔精英批 V6〕` +- 同目录 `config_db_test.gd` 抽跑:退出码=**0**,尾行 `[config_db_test] all green (29 checks)`(fv7 换代样本所在套件全绿)。 +- (退出时 ObjectDB 泄漏警告为引擎常规退出清场噪音,与本套件断言面无关,headless 直跑既有形态。) + +### 3.6 oracle 出处核对抽查(5 条,均非 oracle 镜像) + +| 断言 | oracle 注记指向 | 独立推导核对 | +|---|---|---| +| V1 十一违例 | 敌人系统.md:325 拒绝清单 | :325 逐句可独立推出 11 个违例面:缺基础引用/缺附加技/悬空/指向 elite/同名/附加重复/合并池违纪/elite 写池/非 elite 带两键——①–⑦⑨⑩ 与之一一对应;⑪ 权重 0 由 :283「weight ∈ (0, 9999]」推出。**可独立写出,通过** | +| V2 合并池 | 敌人系统.md:149 | :149「基础池取自同关、战斗行类、该基础敌种深度带最小的那一行的池模板」——期望值 {attack9,lunge1,heavy2}=band 0 行池+附加,规格句独立可推;夹具反事实(取首行必红)为审计加验。**通过** | +| V3 传到精英 | 敌人系统.md:149「继承不复制:基础池调参自动传到精英」 | 改权重 7 期望读 7 直由该句推出。**通过** | +| V4 合并池纪律 | 敌人系统.md:150「校验的对象是合并后的池」 | 「附加第二普攻→违规」唯在合并口径下成立(基础行单独合规、附加技单独合法),规格句独立可推。**通过** | +| V6 十二字段 | 敌人系统.md:147+卡 3 §2 表 | :147 出「精英=敌人表行类为 elite 的独立一行」的结构面;行值出处=卡面 §2(2026-11-06 用户点头草案 v0,GDD 明示不登记行值)——12 字段值(hp 20/atk 2/1/3/armor_class 2/armor_type light/unit_count 1/base_ref goblin/display_name「精英·地精」等)与卡 §2 表逐值一致,预期值独立于实现存在(需求合同面)。**通过,非镜像** | + +抽查结论:5 条抽查均能由注记指向的规格句(或卡面用户已裁行值表)独立写出预期值,未发现「预期值只在实现里有的」oracle 镜像。 + +### 3.7 报告引用瑕疵(不阻塞项) + +实现报告卡 3 T3 摘要引用提交 `e538e2cb`/`f4865ad8`、卡 1 各 T 摘要引用 57f3cc75 等 10 个哈希均于 `iter54-premerge-backup` 分支在案可解;唯卡 2 T2 摘要引用「提交 46287043 区段」全库(含 reflog 与 dangling)不可解——该步实际提交=af127b15(premerge-backup),已折叠进重塑提交 4c424af3。属报告引用笔误,改动本身已由本审计在 diff 与跑批读数双面核实,不影响判定。 + +--- + +## 总判定:通过(三卡全过) + +| 卡 | 申报档 | 判定 | 关键依据 | +|---|---|---|---| +| 卡 1 QA 缺陷收口批 | 中 | 通过 | R1–R9 全量核对通过;死接缝双模式 grep 审计自跑双零;削弱检查零删除断言;41 处转换全经 ProbeUiUtil 单点;候选键观测面局限已申报且效力有替代承载 | +| 卡 2 营地交互修复批 | 中 | 通过 | R1–R8 全量核对通过;允许改动 0 条既有断言成立(次钮隐藏/三钮计数原样且绿);to_html 判据形态合规;出口形参仅 host;S13 基线修订在预申报通道内 | +| 卡 3 精英专属池 | 高 | 通过 | R1–R7 全量核对通过;窄复跑自跑 27/27 PASS+config_db_test all green;4 条申报测试改动与 diff 逐条相符无越面;四条契约全过;oracle 抽查 5 条均非镜像 | + +- 测试削弱总核:三笔提交删除断言行合计恰卡 3 申报 4 条,其余零削弱;禁变异类测试与撤销类取证——三笔 diff 内未发现人为制造红或回退取证的形态。 +- 最窄复跑省略(中档卡)的执行证据口径=批内逐 T 提交读数(`iter54-premerge-backup` 每步在案)+合回后远端 CI 现查;卡 3 高档窄复跑由审计者自跑实证。 +- 遗留义务核对:真人目检两项(覆盖面板下钻/顶栏观感)已登记实现报告(登记即了结义务);「开战快照拿到合并后池」半句与精英 EV 重锚的移交去向(待办清单「战斗节点遭遇编排方式」/难度曲线与Boss)在卡 3 §5 与实现报告遗留义务节闭合;反哺点三条(architecture CampHudBand 行/体验验证清单一行/03-内容规模.md:66)落点明确待用户裁决。 +- 未阻塞观察两条(供后续批次参考,不构成本批缺漏):①上节 3.7 报告哈希引用笔误一处;②卡 1 R3/R4 覆盖登记件对非 BaseButton 控件(地图画布、bcard)无命中键为登记件结构性局限——若日后需要该面命中证据,归「控件级零命中缺口」B5 既有条目处置,非本批义务。
{esc(FIELD_CN.get(key, key))} {esc(key)}{attr_value(row, key)}