From 2a55f4e1b751a44005fdbedcaed95b0993673cfe Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 07:46:31 +0800 Subject: [PATCH 1/8] =?UTF-8?q?T1=20P0=E6=80=A5=E6=95=91:=20=E2=91=A0?= =?UTF-8?q?=E6=97=97=E7=BA=B9=E7=90=86=E6=A7=BD=E5=8F=8C=E5=86=99=E8=B8=A9?= =?UTF-8?q?=E8=B8=8F=E4=BF=AE=E5=A4=8D=EF=BC=88=E5=88=A0paint=E5=B0=BE?= =?UTF-8?q?=E9=83=A8T13=E9=81=97=E7=95=99u.tex=5Fflag=3Dnull=E2=80=94?= =?UTF-8?q?=E2=80=94=E4=B8=8EL683=E6=9D=A1=E4=BB=B6=E8=B5=8B=E5=80=BC?= =?UTF-8?q?=E6=9E=84=E6=88=90683=E2=86=92746=E6=89=A7=E8=A1=8C=E5=BA=8F?= =?UTF-8?q?=E8=A6=86=E7=9B=96=3D=E7=B4=A0=E6=9D=90=E5=9C=A8=E4=BD=8D?= =?UTF-8?q?=E6=8F=92=E6=97=97=E6=81=92=E4=B8=8D=E5=8F=AF=E8=A7=81=EF=BC=8C?= =?UTF-8?q?headless=E7=BC=BA=E7=9B=98=E5=AD=97=E5=BD=A2=E5=9B=9E=E9=80=80?= =?UTF-8?q?=E6=8E=A9=E7=9B=96=EF=BC=89+ui=5Finput=5Fprobe=20case=E2=91=A3b?= =?UTF-8?q?=E5=9B=9E=E5=BD=92=E6=96=AD=E8=A8=80=EF=BC=88=E6=9C=89=E7=9B=98?= =?UTF-8?q?=E6=96=AD=E8=A8=80=E7=BA=B9=E7=90=86=E5=9C=A8=E6=A7=BD/?= =?UTF-8?q?=E7=BC=BA=E7=9B=98=E6=96=AD=E8=A8=80=E5=AD=97=E5=BD=A2=E5=9B=9E?= =?UTF-8?q?=E9=80=80=EF=BC=89=E2=91=A1notify=5Fcombat=5Fend=E6=8B=86?= =?UTF-8?q?=E8=A1=8C=E5=A4=8D=E6=B4=BB=EF=BC=88=E5=8E=9F=E8=A2=ABtab?= =?UTF-8?q?=E4=BA=8B=E6=95=85=E5=B9=B6=E8=BF=9B=E8=A1=8C=E5=B0=BE=E6=B3=A8?= =?UTF-8?q?=E9=87=8A=E4=BB=8E=E4=B8=8D=E6=89=A7=E8=A1=8C=3D=E6=88=98?= =?UTF-8?q?=E5=90=8E=E7=9B=B8=E4=BD=8D=E4=B8=8D=E5=9B=9EIdle/RAP=E4=B8=8D?= =?UTF-8?q?=E6=B8=85=E9=9B=B6=EF=BC=8C=E6=AF=8F=E6=88=98=E6=96=B0=E5=BB=BA?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E6=8E=A9=E7=9B=96=EF=BC=89+smoke=5Fall=20=5F?= =?UTF-8?q?run=5Fbattle=E6=94=B6=E6=9D=9F=E7=9B=B8=E4=BD=8DIdle=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E6=96=AD=E8=A8=80=EF=BC=88628=E2=86=92636=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/battle_orchestrator.gd | 3 ++- game/presentation/views/board_view.gd | 4 +++- game/smoke/smoke_all.gd | 5 +++++ game/smoke/ui_input_probe.gd | 11 ++++++++++- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index d31065bc..464ef271 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -795,7 +795,8 @@ func _battle_over(result: String) -> void: _enemy_system.despawn_all() _status_effects.notify_battle_end() if aux_inventory != null: - aux_inventory.retire_overlays() # 迭代 7 T3——叠加层随战斗结束清空(库存不动——关卡域跨战斗) action_economy.notify_combat_end() # → Idle,RAP 清零 + aux_inventory.retire_overlays() # 迭代 7 T3——叠加层随战斗结束清空(库存不动——关卡域跨战斗) + action_economy.notify_combat_end() # → Idle,RAP 清零(2026-10-02 修复批:原被 tab 事故并进上一行行尾注释从不执行——每战新建实例掩盖了症状;P0-3) db.set_reload_gate(false) _reset_round_facts() battle_finished.emit({"result": result, "hero_hp": hero_hp_left, "gp_dropped": gp_dropped}) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index ec513c9f..18fc7b89 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -743,7 +743,9 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: u.proj_band = 0.0 u.tex_facade = null u.facade_strip = false - u.tex_flag = null + # 旗槽不在此重置(2026-10-02 修复批——T13 批的 paint 尾部 `u.tex_flag = null` 与 L683 的条件 + # 赋值构成双写踩踏:执行序 683→此处把旗纹理恒清 null=素材在位时插旗不可见;L683 的 else 分支 + # 已是全路径清残留,尾部重置系冗余且有害,删除) var south_n: Vector2i = c + Vector2i(0, 1) var north_n: Vector2i = c + Vector2i(0, -1) var south_in: bool = south_n.x >= 0 and south_n.y >= 0 and south_n.x < _board.width and south_n.y < _board.height diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 52fc2a40..a45b5e7e 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -1273,6 +1273,11 @@ func _run_battle(L: int, D: int) -> Dictionary: if out.result == "": out.result = "timeout" out.terminated = out.result == "victory" or out.result == "defeat" + if out.terminated: + # P0-3 回归断言(2026-10-02 修复批):notify_combat_end 曾被并进行尾注释从不执行—— + # 战斗收束后相位必须回 Idle(RAP 清零),任何「相位卡 Waiting」回归在此被抓 + check(action_economy.phase == "Idle", + "战斗收束后行动经济相位回 Idle(notify_combat_end 生效,实测=%s)" % String(action_economy.phase)) return out diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index c766672a..e7615c2a 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -208,6 +208,15 @@ func _ready() -> void: await get_tree().process_frame if not ms.has_flag(flag_t): _problems.append("case④ 核心旗态未落位") + # ---- case④b 旗纹理槽回归断言(P0-1 修复批 2026-10-02):T13 批曾在 paint 尾部无条件 + # `u.tex_flag = null` 覆盖 T17 批的条件赋值=素材在位时插旗不可见(headless 缺盘走字形回退 + # 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ---- + var flag_unit = bv._cells.get(flag_t) + if bv._flag_tex != null: + if flag_unit == null or flag_unit.tex_flag != bv._flag_tex: + _problems.append("case④b 旗纹理槽未挂封印旗纹理(tex_flag=%s)" % ["无单元" if flag_unit == null else str(flag_unit.tex_flag)]) + elif flag_unit == null or flag_unit.glyph != "旗": + _problems.append("case④b 缺盘字形回退缺失(glyph=%s)" % ["无单元" if flag_unit == null else String(flag_unit.glyph)]) # ---- case⑤ 双击 chord:禁入格双左击(核心拒挖零副作用)→ chord_attempted ---- _push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT) _push_click(_pos(bv, forbid_t), MOUSE_BUTTON_LEFT) @@ -318,7 +327,7 @@ func _ready() -> void: else: _problems.append("case⑭ 无未开格可验(板面异常)") if _problems.is_empty(): - print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标 十四断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增)") + print("[ui_input_probe] PASS — 悬停金框/禁入信号/路径预览/flag_place/chord/dig/英雄落位/影子构成/朝向/缩格重排/缺位回退/变异角标 十五断言全绿(真实 push_input 路径——⑫图上命中⑬画布拟合 2026-09-28 增,⑭变异角标 2026-09-29 增,④b 旗纹理槽回归 2026-10-02 增)") get_tree().quit(0) else: for p in _problems: From 59f0c70ca3303053e469109b0f0064e1249862c0 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 07:53:36 +0800 Subject: [PATCH 2/8] =?UTF-8?q?T2=20=E6=8E=A2=E9=92=88=E6=80=A5=E6=95=91:?= =?UTF-8?q?=20ui=5Fclick=5Fprobe=E4=B8=BB=E5=BE=AA=E7=8E=AF=E7=BC=A9?= =?UTF-8?q?=E8=BF=9B=E8=BF=98=E5=8E=9F=EF=BC=88c44fb1b=E5=8A=A0bonus?= =?UTF-8?q?=E5=88=86=E6=94=AF=E6=97=B6=E6=8A=8Awhile=E4=BD=93=E6=95=B4?= =?UTF-8?q?=E4=BD=93=E5=8E=BB=E7=BC=A9=E8=BF=9B=3Dcontinue=E5=87=BA?= =?UTF-8?q?=E5=BE=AA=E7=8E=AF=E8=A7=A3=E6=9E=90=E9=94=99=E8=AF=AF=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E4=B8=8D=E5=8F=AF=E5=8A=A0=E8=BD=BD=EF=BC=8C=E6=94=B9?= =?UTF-8?q?=E5=90=8E=E6=97=A0=E4=BA=BA=E5=AE=9E=E8=B7=91=EF=BC=89+bonus?= =?UTF-8?q?=E5=AD=90=E6=80=81=E5=88=86=E6=B4=BE=E8=A1=A5=E5=85=A8=EF=BC=88?= =?UTF-8?q?c44fb1b=E5=88=9D=E7=89=88=E5=8F=AA=E5=A4=84=E7=90=86=E9=80=89?= =?UTF-8?q?=E5=8F=96=E6=80=81=E2=80=94=E2=80=94=E5=85=91=E4=BB=98=E5=90=8E?= =?UTF-8?q?=E7=BB=93=E6=9E=9C=E6=80=81visible=E6=81=92=E7=9C=9Fpress=5Fcon?= =?UTF-8?q?firm=E7=A9=BA=E8=BD=AC=E5=88=B7=E6=8B=92=E7=BB=9D=EF=BC=8C?= =?UTF-8?q?=E8=A1=A5=E7=BB=93=E6=9E=9C=E6=80=81=E6=8C=89=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?/=E6=9B=BF=E6=8D=A2=E6=80=81=E6=94=BE=E5=BC=83=E6=96=B0?= =?UTF-8?q?=E4=BB=B6=E5=88=86=E6=B4=BE=EF=BC=89+action=5Fbudget=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E5=8C=96=E5=B8=A6=E5=8F=A3=E5=BE=84=E6=B3=A8=EF=BC=9B?= =?UTF-8?q?=E5=AE=9E=E8=B7=91=E8=AF=81=E6=8D=AE=3D=E9=80=89=E8=B7=AF?= =?UTF-8?q?=E5=8F=A3=E7=82=B9=E5=87=BB7=E6=AC=A1=EF=BC=88=E5=8E=9F?= =?UTF-8?q?=E6=81=920=E4=B8=8D=E5=8F=AF=E8=BE=BE=EF=BC=89+=E5=BE=AA?= =?UTF-8?q?=E7=8E=AF=E9=80=80=E5=87=BAdone=3Dtrue=E9=97=AE=E9=A2=98=3D0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/smoke/ui_click_probe.gd | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index ea9cacd7..3a7d9584 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -186,21 +186,30 @@ func _ready() -> void: _problems.append("首格未可点(cells=0)") print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [ first_cell_ms, t_total, t_ui_ready]) - var action_budget := 20000 + const ACTION_BUDGET_FRAMES := 20000 # 主循环帧预算(坏态挂死兜底上限——常态通关远低于此;有挂帧证据才收紧,勿让坏态等满预算才报) + var action_budget := ACTION_BUDGET_FRAMES + # 2026-10-02 修复批(P0-2):c44fb1b 加 bonus 分支时把本 while 循环体整体去缩进——continue 落在 + # 循环外成解析错误、探针不可加载且改后无人实跑。此处还原循环体缩进,bonus 分支保留为体内兄弟分支。 while not _done and action_budget > 0: action_budget -= 1 await get_tree().process_frame - if main.node_overlay.visible: - main.node_overlay.press_continue() - _flags_placed.clear() - continue - if main.bonus_overlay.visible: - # 迭代 10 三选一停点(BonusPickOverlay——奖励节点/通关兑付共用面;T17 硬锁批适配—— - # 本探针此前未适配该层,走到即卡死):真实信号路径点选首件+确认。 - main.bonus_overlay.press_card(0) - main.bonus_overlay.press_confirm() - _flags_placed.clear() - continue + if main.node_overlay.visible: + main.node_overlay.press_continue() + _flags_placed.clear() + continue + if main.bonus_overlay.visible: + # 迭代 10 三选一停点(BonusPickOverlay——奖励节点/通关兑付共用面):按子态分派真实按钮路径。 + # 选取态=点选首件+确认;结果态=按「继续」收面;替换子态=放弃新件(-1)。c44fb1b 初版只处理 + # 选取态——兑付后结果态 visible 恒真,press_confirm 空转刷「无待选停点」拒绝(T2 修复批补分派)。 + if main.bonus_overlay._continue_btn.visible: + main.bonus_overlay.press_continue() + elif main.bonus_overlay._replace_box.visible: + main.bonus_overlay.replace_chosen.emit(-1) + else: + main.bonus_overlay.press_card(0) + main.bonus_overlay.press_confirm() + _flags_placed.clear() + continue if main.level_screen.visible: # 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造) main.level_screen.press_candidate(_route_clicks % 2) From d1460327a478dbe5f026faf563de19f96e23bc01 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 08:46:12 +0800 Subject: [PATCH 3/8] =?UTF-8?q?T5=20core/data=E7=BB=93=E6=9E=84=E6=AE=B5:?= =?UTF-8?q?=20config=5Fdb=E6=8B=86R1=E8=A7=A3=E6=9E=90=E5=99=A8+=E8=B7=A8?= =?UTF-8?q?=E8=A1=A8=E6=A0=A1=E9=AA=8C=E5=99=A8=E4=B8=A4=E7=B1=BB=EF=BC=88?= =?UTF-8?q?=E9=97=A8=E9=9D=A238=E6=B6=88=E8=B4=B9=E6=96=B9=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E9=9B=B6=E5=8F=98=E5=8C=96=EF=BC=89+=E4=B8=89?= =?UTF-8?q?=E9=80=89=E4=B8=80=E5=8F=8C=E6=B8=A0=E9=81=93=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E5=8C=96=EF=BC=88=5Fapply=5Fbonus=5Fpick=E5=85=B1=E4=BA=AB/?= =?UTF-8?q?=E5=AE=88=E5=8D=AB=E6=94=B6AuxItems.can=5Faccept/=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E5=AD=90=E4=BA=A4=E4=BA=92=E4=BF=9D=E5=AE=88=E5=88=86?= =?UTF-8?q?=E8=BD=A8=E6=B3=A8=E8=AE=B0=EF=BC=89+rest=E5=9B=9E=E8=A1=80?= =?UTF-8?q?=E5=8D=95=E7=82=B9+=E8=A1=8C=E4=B8=BB=E5=BA=8F=E9=94=AE?= =?UTF-8?q?=E5=8D=95=E7=82=B9+=E9=AD=94=E6=B3=95=E6=95=B0=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8C=96=EF=BC=88bonus=5Fcandidate=5Fcount=E9=BB=98?= =?UTF-8?q?=E8=AE=A43/=E5=87=BA=E7=94=9F=E5=8C=BA9/=E6=95=B0=E9=87=8F?= =?UTF-8?q?=E5=9F=9F=E5=B8=B8=E9=87=8F=E2=80=94=E2=80=94=E5=80=BC=E9=9B=B6?= =?UTF-8?q?=E6=94=B9=E5=8A=A8=EF=BC=89+level=5Frunner=20Phase=20enum?= =?UTF-8?q?=EF=BC=88=E5=A4=96=E9=83=A8=E8=AF=BB=E9=9D=A2=E9=9B=B6=E5=8F=98?= =?UTF-8?q?=E5=8C=96=EF=BC=89+=E8=A1=A8=E8=AE=BF=E9=97=AE=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=EF=BC=88aux=E8=B5=B0=E8=AE=BF=E9=97=AE=E5=99=A8/?= =?UTF-8?q?=E8=A1=8C=E9=9B=86=E7=9B=B4=E6=89=AB=E4=B8=89=E5=A4=84=E6=94=B6?= =?UTF-8?q?=E5=8F=A3/=E5=88=A0clamped=5Fpool=E5=A3=B3=E4=B8=8E=E6=AD=BB?= =?UTF-8?q?=E5=8F=98=E9=87=8Ferow/=5Fminesweeper=5Fref=E5=8C=85=E8=A3=85?= =?UTF-8?q?=EF=BC=89+tune=5Fsim=E5=8F=8D=E5=90=91=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E8=A7=A3=E9=99=A4=EF=BC=88=E6=9E=84=E9=80=A0=E6=B3=A8=E5=85=A5?= =?UTF-8?q?=E7=BC=BA=E7=9C=81null=E5=85=9C=E5=BA=95=EF=BC=89+=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E6=B3=A8=E6=89=B9=E9=87=8F=EF=BC=9B=E9=97=B8=3D?= =?UTF-8?q?=E5=86=B2=E7=83=9F636/0+run=5Fall=2015=E5=A5=97+=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=85=A8=E7=BB=BF+MC=E5=AF=B9=E6=8B=8D200=E5=B1=80?= =?UTF-8?q?=E9=80=90=E4=BD=8D=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/core/adventure_runner.gd | 129 +++++------ game/core/aux_items.gd | 18 +- game/core/battle_orchestrator.gd | 22 +- game/core/board_data.gd | 10 + game/core/board_generator.gd | 8 +- game/core/config_db.gd | 235 +++------------------ game/core/config_db_cross_validator.gd | 146 +++++++++++++ game/core/config_db_cross_validator.gd.uid | 1 + game/core/config_db_resolver.gd | 108 ++++++++++ game/core/config_db_resolver.gd.uid | 1 + game/core/damage_controller.gd | 6 +- game/core/level_runner.gd | 79 ++++--- game/core/meta_progression.gd | 2 +- game/core/minesweeper_rules.gd | 5 +- game/core/reward_economy.gd | 4 +- game/core/unit_container.gd | 3 +- game/data/schemas/adventure_table.gd | 6 + game/data/schemas/board_gen_table.gd | 4 +- game/data/schemas/enemy_table.gd | 13 +- game/data/schemas/level_table.gd | 8 + game/presentation/tune_sim_controller.gd | 25 ++- 21 files changed, 491 insertions(+), 342 deletions(-) create mode 100644 game/core/config_db_cross_validator.gd create mode 100644 game/core/config_db_cross_validator.gd.uid create mode 100644 game/core/config_db_resolver.gd create mode 100644 game/core/config_db_resolver.gd.uid diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 09bc6b89..ed3411ec 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -53,8 +53,8 @@ signal round_started(round_index: int) signal board_changed() signal wallet_changed(event: String, gp: int, extra: Dictionary) -var db -var rng +var db: ConfigDb +var rng: RngServiceCore var state := "idle" # idle / running / settling(冒险结构.md 三态) var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享) var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 统一冒险加算区(商店购买与三选一属性件同一本账;四轴 +ap——迭代 10 奖励重构) @@ -107,7 +107,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: keystone = 0 summary = {} _reset_bonus_state() - levels_total = db.level.rows.size() + levels_total = db.level_count() # 表访问收口(2026-10-02——行集直扫退役) level_index = 1 _pending_next_level = false _base_override = _normalize_override(params.get("hero_override", {})) @@ -148,7 +148,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: purchased = (bp.get("shop_purchased", {}) as Dictionary).duplicate(true) keystone = int(bp.get("keystone", 0)) summary = {} - levels_total = db.level.rows.size() + levels_total = db.level_count() # 表访问收口(2026-10-02——行集直扫退役) level_index = int(bp.get("level_index", 1)) _pending_next_level = false _reset_bonus_state() @@ -221,10 +221,10 @@ func runner() -> LevelRunner: return _runner -# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶;与 LevelRunner.rest_heal_amount -# 同式,同一公式两个落点是临时态,注释指认,统一归并归后续批) +# 接口③治疗封顶回报执行体(恢复点节点兑付——按比例回血并封顶)。算术宿主 = LevelRunner.rest_heal +#(2026-10-02 修复批——同式双落点字面量退役,与 LevelRunner.rest_heal_amount 同一单点) func apply_rest(pct: int, hp: int, max_hp: int) -> int: - return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) + return LevelRunner.rest_heal(pct, hp, max_hp) # 大奖励确认 → 进下一关(仅兑付后受理——零自动推进的冒险级停点;三选一未兑付拒绝推进) @@ -255,6 +255,19 @@ func pick_bonus(id: String) -> bool: return false var value: int = int(db.adventure.bonus_value(id)) var hp_before := int(ledger.get("hero_hp", 0)) + if id == "aux_item": + return _pick_aux_item(value) # 道具件分派通关渠道(持有容器 = 跨关携带——与节点渠道分轨) + if not _apply_bonus_pick(id, value): + push_error("[冒险结构] pick_bonus 拒绝——未知件: %s" % id) + return false + _finish_bonus_pick(id, value, hp_before) + return true + + +# 三选一件兑付共享体(2026-10-02 修复批——通关/奖励节点双渠道 match 双胞胎收口): +# 属性件写加算区 + 件获取计数、AP 件写第四轴、GP 件走钱包。道具件不入此体——两渠道持有容器 +# 语义不同(跨关携带 vs 关卡域库存),分派回调用方渠道处理;未知件返回 false 由调用方按渠道名报错。 +func _apply_bonus_pick(id: String, value: int) -> bool: match id: "attack": bonus["atk"] = int(bonus.get("atk", 0)) + value @@ -271,37 +284,24 @@ func pick_bonus(id: String) -> bool: "gp": if _runner != null: _runner.grant_bonus_gp(value) - "aux_item": - return _pick_aux_item(value) _: - push_error("[冒险结构] pick_bonus 拒绝——未知件: %s" % id) return false - _finish_bonus_pick(id, value, hp_before) return true # 道具件兑付(同调位约束):有位直接入跨关携带;满位转替换子交互(incoming 暂存、等 resolve_aux_replace)。 # 掷定前置(追加批二):具体道具随候选抽定(_last_draw_aux / _bonus_pending.aux_detail)——本函数消费预掷值, # 断点续玩/无预掷值时回退当场掷定(流序不同但消费数不变,确定性由调用序保证)。 +# 持有容器 = 跨关携带 _bonus_aux_carry(与节点渠道的关卡域库存分轨——容器差异保守不合,注记在案)。 func _pick_aux_item(count: int) -> bool: - var pool: Array = _bonus_aux_pool() - if pool.is_empty(): + var picked: Array = _draw_pending_aux(_bonus_pending, count, true) + if picked.is_empty(): push_error("[冒险结构] pick_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)") return false var hp_before := int(ledger.get("hero_hp", 0)) - var picked: Array[String] = [] - var detail: Array = (_bonus_pending.get("aux_detail", []) as Array).duplicate() - if detail.is_empty(): - detail = _last_draw_aux.duplicate() - for i in maxi(1, count): - if i < detail.size(): - picked.append(String(detail[i])) - else: - picked.append(_roll_bonus_aux_item(pool)) - var slots := maxi(0, int(db.adventure.transitional_sync_slots)) - if _bonus_aux_carry.size() + picked.size() <= slots: + if AuxItems.can_accept(picked.size(), _bonus_aux_carry.size(), db): # 同调位守卫单点(2026-10-02) for iid in picked: - _bonus_aux_carry.append(iid) + _bonus_aux_carry.append(String(iid)) _finish_bonus_pick("aux_item", picked.size(), hp_before) return true # 满位替换子交互:现有持有件 + 新件 N+1 选 N(丢旧收新 / 放弃新件两路,终态恒 ≤ 同调位) @@ -312,8 +312,9 @@ func _pick_aux_item(count: int) -> bool: # 替换子交互受理(discard_index = -1 放弃新件不获取;否则丢该件腾位收新)——终态恒 ≤ 同调位 +# 持有容器 = 跨关携带(序号寻址);与节点渠道(库存句柄寻址)的丢弃/授予/收尾三步保守不合。 func resolve_aux_replace(discard_index: int) -> bool: - if _bonus_pending.is_empty() or String(_bonus_pending.get("chosen", "")) != "aux_item": + if not _replace_in_transit(_bonus_pending): push_error("[冒险结构] resolve_aux_replace 拒绝——无替换在途") return false var incoming: Array = _bonus_pending.get("incoming", []) as Array @@ -368,55 +369,33 @@ func apply_node_bonus(id: String) -> bool: push_error("[冒险结构] apply_node_bonus 拒绝——件不在候选: %s" % id) return false var value: int = int(db.adventure.bonus_value(id)) - match id: - "attack": - bonus["atk"] = int(bonus.get("atk", 0)) + value - _bonus_taken["attack"] = int(_bonus_taken.get("attack", 0)) + 1 - "max_hp": - bonus["max_hp"] = int(bonus.get("max_hp", 0)) + value - _bonus_taken["max_hp"] = int(_bonus_taken.get("max_hp", 0)) + 1 - "armor": - bonus["armor_class"] = int(bonus.get("armor_class", 0)) + value - _bonus_taken["armor"] = int(_bonus_taken.get("armor", 0)) + 1 - "ap": - bonus["ap"] = int(bonus.get("ap", 0)) + value - _bonus_taken["ap"] = int(_bonus_taken.get("ap", 0)) + 1 - "gp": - if _runner != null: - _runner.grant_bonus_gp(value) - "aux_item": - return _apply_node_aux(value) - _: - push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s" % id) - return false + if id == "aux_item": + return _apply_node_aux(value) # 道具件分派节点渠道(持有容器 = 关卡域库存——与通关渠道分轨) + if not _apply_bonus_pick(id, value): + push_error("[冒险结构] apply_node_bonus 拒绝——未知件: %s" % id) + return false _finish_node_bonus(id, value) return true # 节点渠道道具件(同调位约束):持有=关卡域库存+跨关携带,有位直接入本关库存(当关可用); -# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨)。 +# 满位转替换子交互(丢弃目标=库存件 handle——与通关渠道的 carry 序号分轨,容器语义保守不合)。 # 掷定前置:消费 _node_bonus_pending.aux_detail 预掷值(缺省回退当场掷定)。 func _apply_node_aux(count: int) -> bool: - var pool: Array = _bonus_aux_pool() - if pool.is_empty(): + var picked: Array = _draw_pending_aux(_node_bonus_pending, count, false) + if picked.is_empty(): push_error("[冒险结构] apply_node_bonus(aux_item) 拒绝——解锁道具池空集(白板/门控态不入牌库的防御兜底)") return false - var picked: Array[String] = [] - var detail: Array = (_node_bonus_pending.get("aux_detail", []) as Array).duplicate() - for i in maxi(1, count): - if i < detail.size(): - picked.append(String(detail[i])) - else: - picked.append(_roll_bonus_aux_item(pool)) - var slots := maxi(0, int(db.adventure.transitional_sync_slots)) var records: Array = [] if _runner != null and _runner.aux_inventory != null: records = _runner.aux_inventory.holding_records() var holding := records.size() + _bonus_aux_carry.size() - if holding + picked.size() <= slots: + if AuxItems.can_accept(picked.size(), holding, db): # 同调位守卫单点(2026-10-02) if _runner != null: for iid in picked: - _runner.grant_aux_item(iid, "reward_node") + var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node") + if rec.is_empty(): + push_warning("[冒险结构] 奖励节点道具授予被拒(防御兜底留痕): %s" % String(iid)) _finish_node_bonus("aux_item", picked.size()) return true _node_bonus_pending["chosen"] = "aux_item" @@ -426,7 +405,7 @@ func _apply_node_aux(count: int) -> bool: func _resolve_node_replace(discard_handle: int) -> bool: - if _node_bonus_pending.is_empty() or String(_node_bonus_pending.get("chosen", "")) != "aux_item": + if not _replace_in_transit(_node_bonus_pending): push_error("[冒险结构] resolve_node_replace 拒绝——无替换在途") return false var incoming: Array = _node_bonus_pending.get("incoming", []) as Array @@ -441,6 +420,32 @@ func _resolve_node_replace(discard_handle: int) -> bool: return true +# 替换在途守卫(2026-10-02 修复批——双渠道替换受理的共用前置谓词:停点态非空且 chosen=aux_item) +func _replace_in_transit(pending: Dictionary) -> bool: + return not pending.is_empty() and String(pending.get("chosen", "")) == "aux_item" + + +# 双渠道道具件掷定共享体(2026-10-02 修复批——_pick_aux_item/_apply_node_aux 复制债收口一步): +# 解锁道具池空集守卫 + N 件掷定(先消费停点预掷 aux_detail,不足部分当场经 reward_pick 封闭流补掷—— +# 取数序与拆分前逐位一致)。返回空数组 = 池空集(调用方按渠道名报错)。 +# 保守不合处:通关渠道允许回退 _last_draw_aux(allow_detail_fallback)、节点渠道不回退——掷定回退 +# 语义渠道各异,参数化保留差异;持有容器与兑付收尾仍归调用方渠道。 +func _draw_pending_aux(pending: Dictionary, count: int, allow_detail_fallback: bool) -> Array: + var pool: Array = _bonus_aux_pool() + if pool.is_empty(): + return [] + var picked: Array = [] + var detail: Array = (pending.get("aux_detail", []) as Array).duplicate() + if detail.is_empty() and allow_detail_fallback: + detail = _last_draw_aux.duplicate() + for i in maxi(1, count): + if i < detail.size(): + picked.append(String(detail[i])) + else: + picked.append(_roll_bonus_aux_item(pool)) + return picked + + # 替换受理分发(单入口——节点停点在途优先按库存句柄,否则按通关停点的携带序号) func resolve_bonus_replace(discard: int) -> bool: if not _node_bonus_pending.is_empty() and _node_bonus_pending.has("incoming"): @@ -479,7 +484,7 @@ func _draw_bonus_candidates() -> Array: keyed.append({"id": id, "key": pow(rg.randf(), 1.0 / w)}) keyed.sort_custom(func(a, b): return float(a["key"]) > float(b["key"])) var out: Array = [] - for i in mini(3, keyed.size()): + for i in mini(int(db.adventure.bonus_candidate_count), keyed.size()): # 候选数读表(2026-10-02 魔法数结构化——默认 3 = 现行行为) var cid := String(keyed[i]["id"]) out.append(cid) if cid == "aux_item": diff --git a/game/core/aux_items.gd b/game/core/aux_items.gd index dc204754..4323862c 100644 --- a/game/core/aux_items.gd +++ b/game/core/aux_items.gd @@ -73,6 +73,15 @@ func holding_count() -> int: return n +# 同调位守卫谓词(2026-10-02 修复批——四处手抄收口单点:战斗结算授予路由 / 关卡授予 / 三选一双渠道): +# holding = 调用方渠道的当前实际持有数(库存渠道传 holding_count()、通关渠道传跨关携带数、 +# 节点渠道传库存+携带合计);读数源 = db.adventure.transitional_sync_slots(负值钳 0 口径同前)。 +# 返回再入 n 件是否仍在位内。挂 static:谓词纯算术不依赖实例态——通关渠道(跨关携带)没有 +# AuxItems 实例可用,实例方法形态不成立;db 参数不标类型(同 _db 桩表注入纪律)。 +static func can_accept(n: int, holding: int, db) -> bool: + return holding + n <= maxi(0, int(db.adventure.transitional_sync_slots)) + + # 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) func holding_records() -> Array: var out: Array = [] @@ -105,10 +114,13 @@ func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool: func _item_row(item_id: String) -> Dictionary: - if _db != null and _db.aux_items != null: - return _db.aux_items.row_by_id(item_id) - return {} + if _db == null: + return {} + if _db.has_method("aux_item_row"): + return _db.aux_item_row(item_id) # 表访问收口(2026-10-02——正式面走 ConfigDB 访问器) + return _db.aux_items.row_by_id(item_id) # 桩表面(单测 StubDB 无访问器——直读保留,随测试面演进退役) +# 不标具体类型原因:单元测试注入 StubDB 桩表(aux_items_test.gd)——typed 赋值会运行期拒收,保守保持 null 注 var _db = null # ConfigDB(行定义读取——_init 注入;测试可注入桩表) diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index 464ef271..c1a4abf1 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -50,8 +50,8 @@ var action_economy: ActionEconomy var _enemy_system: UnitContainer var _status_effects: StatusContainer var _reward_economy: RewardEconomy -var aux_inventory = null # AuxItems(迭代 7 T3——LevelRunner 注入的关卡域库存引用; - # 每战新建本类,库存不归本类所有——注入面) +var aux_inventory: AuxItems # 迭代 7 T3——LevelRunner 注入的关卡域库存引用; + # 每战新建本类,库存不归本类所有——注入面 var _damage: DamageController # 伤害控制器(伤害管线单据化批 2026-09-24——单据唯一兑付管线) var encounter: Dictionary[String, Variant] = {} @@ -126,7 +126,6 @@ func begin_battle(enc: Dictionary) -> bool: _hero_atk, int(enc.get("hero_max_hp", -1)), rolled_atk): _fatal("敌人系统 spawn 被拒") return false - var erow: Dictionary = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) if not _install_new_board(): return false _reset_round_facts() # 首板 install 的初始展开事件不入轮 facts(resource 恒 false 的双保险) @@ -375,7 +374,8 @@ func _grant_aux(snap: Dictionary, polarity: String, source_class: String, cell: # 同调位持有上限(迭代 10 奖励重构批过渡承载——transitional_sync_slots,provisional 1): # 战斗内渠道遇满位沉没不入(战斗域零交互纪律——替换交互归停点渠道;正主 = 同调位谓词表, # 全渠道统一收口随魔法道具同调实施批)。逐行归因纪律:沉没行照发(「获得」的零兑付形态)。 - if aux_inventory.holding_count() + 1 > maxi(0, int(db.adventure.transitional_sync_slots)): + # 谓词单点 = AuxItems.can_accept(2026-10-02 修复批——四处手抄收口)。 + if not AuxItems.can_accept(1, aux_inventory.holding_count(), db): _emit_row("effect", source_class, cell, ri, "同调位已满,%s 沉没" % String(db.aux_item_row(iid).get("display_name", iid)), 0, {"effect_id": "", "aux_sink": iid, "sink_reason": "sync_slots_full"}, "") return false @@ -396,7 +396,7 @@ func use_aux(handle: int, target: Vector2i) -> Dictionary: return {"ok": false, "reason": "库存未接线"} if action_economy.phase != action_economy.PHASE_ACTIVE: return {"ok": false, "reason": "非挖掘期(使用窗口=Active)"} - var board = _minesweeper_ref() + var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_aux 直引公有成员) var v: Dictionary = aux_inventory.validate_use(handle, target, Vector2(board.width, board.height), func(c: Vector2i): return not board.is_revealed(c)) @@ -419,10 +419,6 @@ func set_aux_unlocked(ids: Dictionary) -> void: _reward_economy.set_aux_unlocked(ids) -func _minesweeper_ref() -> Variant: - return minesweeper - - func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "") -> bool: # 迭代 7 T3——aux 投放行授予路由(魔法道具 GDD:授予路由唯一调用方=战斗结算):正路由(挖开/标中) # → 道具入关卡域库存(来源可归因);负路由(雷格/标错旗摇中 aux 身份——雷安同池)→ 零兑付 @@ -672,18 +668,14 @@ func _combat_segment(ri: int) -> void: # ---- 算术读数(伤害管线单据化批 2026-09-24 迁移注记)---- # 掷点/折扣/克制点数/池乘区/攻击行文本已全部迁入 伤害控制器(damage_controller.gd 管线单点); -# 本类仅保留两处薄委托——_defender_ac(快照面消费)/ _clamped_pool(冒烟白盒断言直呼面 smoke_all 沿用)。 +# 本类仅保留一处薄委托——_defender_ac(快照面消费)。_clamped_pool 委托壳已随 2026-10-02 修复批删除 +# (冒烟白盒断言消费面改直引 settlement._damage.clamped_pool——并行卡同批改 smoke_all)。 # 防守方护甲等级读数——实现归 伤害控制器 defender_ac(本委托供结算快照/呈现快照) func _defender_ac(unit: Dictionary) -> int: return _damage.defender_ac(unit) -# 池内加算 → clamp——实现归 伤害控制器 clamped_pool(本委托供冒烟白盒断言 smoke_all 直呼兼容) -func _clamped_pool(pool: String) -> int: - return _damage.clamped_pool(pool) - - func _pool_effective(effect_id: String) -> int: var side := "hero" var row: Dictionary = db.status_effect_row(effect_id) diff --git a/game/core/board_data.gd b/game/core/board_data.gd index 354919de..7548cb3f 100644 --- a/game/core/board_data.gd +++ b/game/core/board_data.gd @@ -6,6 +6,10 @@ class_name BoardData extends RefCounted +# 出生 3×3 区格数(2026-10-02 修复批 T2 魔法数结构化——W×H−9 布雷域基的双落点收口单点: +# 棋盘生成 _attempt/_finalize 与 BoardGenTable 布雷数下限校验共用;与 spawn_cells() 的 3×3 几何同源) +const SPAWN_AREA_CELLS := 9 + var width: int = 0 var height: int = 0 var spawn_center := Vector2i.ZERO @@ -24,6 +28,12 @@ func in_bounds(c: Vector2i) -> bool: return c.x >= 0 and c.y >= 0 and c.x < width and c.y < height +# 行主序排序键(2026-10-02 修复批——排雷玩法 旗判定批与 地图战斗资源 资格规范化两处手抄收口单点): +# y×1000+x 字典序 = 行主序;板宽域 [8,64] 远小于 1000,x 不跨行、键值不碰撞。 +static func row_major_key(c: Vector2i) -> int: + return c.y * 1000 + c.x + + func index_of(c: Vector2i) -> int: return c.y * width + c.x diff --git a/game/core/board_generator.gd b/game/core/board_generator.gd index b8e9e676..7c370070 100644 --- a/game/core/board_generator.gd +++ b/game/core/board_generator.gd @@ -13,7 +13,8 @@ const METRIC_VERSION := 1 # ==================== 生成入口 ==================== -# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件) +# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。 +# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表(2026-10-02 注) func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: var row: Dictionary = db.resolve_board_gen(L, D) if row.is_empty(): @@ -80,6 +81,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar b.level_index = params.L b.node_depth = params.D # ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删" + # (−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS——2026-10-02 修复批) var spawn := {} for c in b.spawn_cells(): spawn[c] = true @@ -87,7 +89,7 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar for c in b.all_cells(): if not spawn.has(c): domain.append(c) - var n_mines: int = int(round(params.density * float(params.w * params.h - 9))) + var n_mines: int = int(round(params.density * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) # 部分 Fisher-Yates(确定性,消费 board_gen 流) for i in range(0, mini(n_mines, domain.size())): var j := rng_stream.randi_range(i, domain.size() - 1) @@ -314,7 +316,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int: func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db) -> void: var m := measure_inferability(b) # 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命 - var n_target: int = int(round(float(params.density) * float(params.w * params.h - 9))) + var n_target: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) if b.mines.size() > n_target: _fatal("产物断言违例:实际雷数 %d > 目标雷数 N=%d(先布后删只减不增)" % [b.mines.size(), n_target]) var attempt_facts := b.metrics # 保留尝试期事实(ring_removed) diff --git a/game/core/config_db.gd b/game/core/config_db.gd index 4509dab1..0dafbd58 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -48,10 +48,15 @@ var adventure: AdventureTable var is_ready := false var _diagnostics: Array[Dictionary] = [] # {severity, category, message} -var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表 +var _resolver: ConfigDbResolver # R1 难度行解析协作类(2026-10-02 修复批 T5 拆分——门面公有签名零变化) +var _validator := ConfigDbCrossValidator.new() # 跨表校验协作类(无状态——声明期即建) var _reload_gate := false # 数据配置 EC8——战斗结算 唯一调用方 +func _init() -> void: + _resolver = ConfigDbResolver.new(self) # 协作类持门面引用(诊断/致命通道回呼) + + func push_diag(severity: String, category: String, message: String) -> void: _diagnostics.append({"severity": severity, "category": category, "message": message}) if severity == "warning": @@ -73,7 +78,7 @@ func _fatal(category: String, message: String) -> void: func reset_diagnostics() -> void: _diagnostics.clear() - _warn_dedup.clear() + _resolver.reset_warn_dedup() # R1 告警去重注册表(协作类持钥——清空时点与拆分前一致) func diagnostics() -> Array: @@ -95,7 +100,7 @@ func _ready() -> void: func load_tables(root: String, collect_only: bool) -> bool: is_ready = false _diagnostics.clear() - _warn_dedup.clear() + _resolver.reset_warn_dedup() var loaded := {} for key in TABLE_MANIFEST.keys(): var path := root.path_join(TABLE_MANIFEST[key]) @@ -134,222 +139,31 @@ func load_tables(root: String, collect_only: bool) -> bool: return not has_errors +# 跨表校验(悬空引用 / 池护栏——全部加载期致命)本体自 2026-10-02 修复批迁出: +# ConfigDbCrossValidator(core/config_db_cross_validator.gd)承载,门面公有签名零变化。 func _cross_validate(loaded: Dictionary) -> void: - var st: StatusEffectTable = loaded.get("status_effects") - var rt: RewardTable = loaded.get("reward") - var st_tab: SettlementTable = loaded.get("settlement") - if st == null or rt == null or st_tab == null: - return # 单表校验已报错,不连锁 - var effect_ids := {} - for row in st.rows: - effect_ids[row.get("effect_id", "")] = true - # ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。 - # GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键 - # (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。 - for row in rt.rows: - if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE: - continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面 - var eff: String = row.get("effect_id", "") - if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff): - push_diag("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff]) - # ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id, - # 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役 - # 后仍投放(连带错误),同拦。 - var aux_live := {} - var aux_tab = loaded.get("aux_items") - if aux_tab != null: - for arow in aux_tab.active_rows(): - aux_live[String(arow.get("item_id", ""))] = true - for row in rt.rows: - if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE: - continue - var aux_eff: String = String(row.get("effect_id", "")) - if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验 - continue - if not aux_live.has(aux_eff): - push_diag("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"]) - # ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter} - # = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减 - # ①c F2 投放维度覆盖校验随战斗掉落渠道退役(2026-10-01 用户拍板甲——挖格不掉道具,魔法道具 - # 唯一来路=三选一共享池〔节点/通关〕):旧校验=「掉落池五维度各 ≥1 正权重活行」,载体已随 - # reward.tres aux 投放四行删除而消失;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可 - # 静态校验。静态覆盖保证的重立(如按维度钉共享池抽签面)归同调实施批/渠道设计批。 - # ② SettlementTable.effect_pool_map 悬空 - for eff in st_tab.effect_pool_map.keys(): - if not effect_ids.has(eff): - push_diag("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff) - # ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键) - var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"] - for s in required_sources: - if not st_tab.source_label_map.has(s): - push_diag("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) - # ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— - # 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦) - var lv: LevelTable = loaded.get("level") - var bg: BoardGenTable = loaded.get("board_gen") - var en: EnemyTable = loaded.get("enemy") - if lv != null and bg != null and en != null: - var bg_keys := {} - for row in bg.rows: - bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - var en_keys := {} - for row in en.rows: - en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true - for lrow in lv.rows: - var li: int = int(lrow.level_index) - # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) - var node_lists: Array[Array] = [] - var tpls: Array = lrow.get("templates", []) - if tpls.is_empty(): - node_lists.append(lrow.get("nodes", [])) - else: - for tpl in tpls: - node_lists.append(tpl.get("nodes", [])) - for nodes_arr in node_lists: - for node in nodes_arr: - var t: String = String(node.get("type", "")) - if t != "battle" and t != "boss": - continue - var key := "%d_%d" % [li, int(node.get("node_depth", -1))] - if not bg_keys.has(key): - push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) - if not en_keys.has(key): - push_diag("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) - # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) - var skt = loaded.get("skill") - if skt != null and en != null: - for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")): - push_diag("error", e.category, e.message) + _validator.cross_validate(loaded, push_diag) -# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用): -# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 + -# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。 -# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表, -# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。 +# 池模板 × 技能行跨表校验(冒烟直验独立入口——签名与消费面零变化,实现归协作类) func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array: - var errs: Array[Dictionary] = [] - var skill_types := {} - for row in skill_table.rows: - skill_types[String(row.get("skill_id", ""))] = String(row.get("type", "")) - # 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接 - for row in skill_table.rows: - var st: String = String(row.get("type", "")) - if st != "buff" and st != "debuff": - continue - var want_side: String = "enemy" if st == "buff" else "hero" - var eff: String = String(row.get("buff_ref", "")) - var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {} - if not srow.is_empty() and String(srow.get("target_side", "")) != want_side: - errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]}) - for row in enemy_table.rows: - var eid: String = String(row.get("enemy_id", "")) - var basic_n := 0 - var buff_n := 0 - var debuff_n := 0 - for entry in row.get("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]}) - continue - match skill_types[sid]: - "basic": - basic_n += 1 - "buff": - buff_n += 1 - "debuff": - debuff_n += 1 - if basic_n != 1: - errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]}) - if buff_n > 1: - errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]}) - if debuff_n > 1: - errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]}) - return errs + return _validator.validate_skill_pool_links(skill_table, enemy_table, status_table) # ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)---- - +# 解析本体自 2026-10-02 修复批迁出:ConfigDbResolver(core/config_db_resolver.gd)承载—— +# 含 _resolve_progress/_ceil_key 与 R1 告警去重注册表;门面同名方法委托,越域 Fatal 语义逐位一致。 func resolve_board_gen(L: int, D: int) -> Dictionary: - return _resolve_progress(board_gen.rows, L, D, "board_gen") + return _resolver.resolve_board_gen(L, D) func resolve_enemy_rowset(L: int, D: int) -> Dictionary: - return _resolve_progress(enemy.rows, L, D, "enemy") + return _resolver.resolve_enemy_rowset(L, D) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: - if difficulty == null: - return {} - for row in difficulty.rows: - if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D: - return { - "hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0), - "atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0), - } - return {} - - -func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary: - if L < 1 or D < 0: - _fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) - return {} - var level_keys: Array[int] = [] - for row in rows: - var lv := int(row.get("level_index", 0)) - if not level_keys.has(lv): - level_keys.append(lv) - if level_keys.is_empty(): - _fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id) - return {} - level_keys.sort() - var l_star := _ceil_key(level_keys, L) - var l_clamped := l_star < 0 - if l_clamped: - l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值) - var depth_keys: Array[int] = [] - for row in rows: - if int(row.get("level_index", 0)) == l_star: - var dv := int(row.get("node_depth", 0)) - if not depth_keys.has(dv): - depth_keys.append(dv) - depth_keys.sort() - var d_star := _ceil_key(depth_keys, D) - var d_clamped := d_star < 0 - if d_clamped: - d_star = int(depth_keys[-1]) - var missing := "" - if l_clamped and d_clamped: - missing = "both_clamp" - elif l_clamped: - missing = "level_clamp" - elif d_clamped: - missing = "depth_clamp" - elif l_star != L and d_star != D: - missing = "both_ceil" - elif l_star != L: - missing = "level_ceil" - elif d_star != D: - missing = "depth_ceil" - if missing != "": - var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star] - if not _warn_dedup.has(dedup_key): - _warn_dedup[dedup_key] = true - var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" - push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) - for row in rows: - if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: - return row - return {} - - -# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal -func _ceil_key(sorted_keys: Array, request: int) -> int: - for k in sorted_keys: - if int(k) >= request: - return int(k) - return -1 + return _resolver.resolve_difficulty_modifiers(L, D) # ---- 地形变异 variant_pool_gate:候选池门控(∅ board_types = 通配;2026-09-19 用户裁定字段名对齐 @@ -373,6 +187,19 @@ func status_effect_row(effect_id: String) -> Dictionary: return status_effects.row_by_id(effect_id) +# ---- 关卡行集访问器(2026-10-02 修复批 T5 表访问收口——level_runner/meta_progression/adventure_runner +# 直扫 db.level.rows 退役;消费面一律走本访问器,行集解析单点归 LevelTable)---- + +# 关卡行单点读取(键 = level_index,缺行返回空字典——消费方按既有空行防御路径处理) +func level_row(level_index: int) -> Dictionary: + return level.row_by_level_index(level_index) if level != null else {} + + +# 序列总关数(AdventureRunner levels_total / MetaProgression 通关累加域的取数口径) +func level_count() -> int: + return level.rows.size() if level != null else 0 + + # ---- GP+商店房实施批(2026-09-21):ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)---- # ── 魔法道具访问器(迭代 7 T1——消费面一律走访问器,禁直读行集)── # 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径 diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd new file mode 100644 index 00000000..a0758ae8 --- /dev/null +++ b/game/core/config_db_cross_validator.gd @@ -0,0 +1,146 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Date: 2026-10-02(code-quality 修复批 T5——ConfigDb 拆分:跨表校验协作类,自 config_db.gd 迁出) +# 职责单一化(Rule 6 校验双通道的跨表一侧):_cross_validate 全域(悬空引用/池护栏/节点精确行/ +# 技能池构成)+ validate_skill_pool_links(冒烟直验独立入口)。ConfigDb autoload 门面保持—— +# load_tables 调用点与冒烟直验经门面同名方法,公有签名零变化。 +# 诊断通道注入:跨表校验的行级错误经 push_diag: Callable 外发(门面传 Callable(self, "push_diag"))—— +# 本类零 ConfigDb 依赖(无状态纯校验器,装载序上先于成员赋值执行的前提保持)。 +# 迁移注记:validate_skill_pool_links 的 status_table 显式入参纪律照抄(装载通道传 loaded 表, +# 冒烟直验传门面 status_effects;null 时 side 守卫跳过)——返回错误列表不进诊断通道,消费面归调用方。 +# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚) +class_name ConfigDbCrossValidator +extends RefCounted + + +func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: + var st: StatusEffectTable = loaded.get("status_effects") + var rt: RewardTable = loaded.get("reward") + var st_tab: SettlementTable = loaded.get("settlement") + if st == null or rt == null or st_tab == null: + return # 单表校验已报错,不连锁 + var effect_ids := {} + for row in st.rows: + effect_ids[row.get("effect_id", "")] = true + # ① RewardTable effect_id 悬空(percent 行引用 StatusEffectTable——悬空加载致命)。 + # GP+商店房实施批(2026-09-21)例外:gp_gain/gp_loss = GP 战斗内缓冲(escrow)通道的路由键 + # (战斗结算 _apply_gp_row 兑付——不入增益状态容器),词表封闭按字面豁免悬空引用校验。 + for row in rt.rows: + if String(row.get("cell_type", "")) == RewardTable.AUX_ITEM_TYPE: + continue # aux 行 effect_id=item_id——归 ①b 双表校验(reward_dangling_aux),不入增益词表面 + var eff: String = row.get("effect_id", "") + if eff != "" and eff != "gp_gain" and eff != "gp_loss" and not effect_ids.has(eff): + push_diag.call("error", "dangling_effect", "RewardTable %s 引用不存在的 effect_id: %s" % [row.get("reward_id"), eff]) + # ①b RewardTable aux 投放行双表校验(迭代 7 T1——reward_dangling_aux):aux 行 effect_id = item_id, + # 须命中 AuxItemTable 活行(非 retired 非 deferred——罗盘退役/失义件不投放)。retired 行被引用=退役 + # 后仍投放(连带错误),同拦。 + var aux_live := {} + var aux_tab = loaded.get("aux_items") + if aux_tab != null: + for arow in aux_tab.active_rows(): + aux_live[String(arow.get("item_id", ""))] = true + for row in rt.rows: + if String(row.get("cell_type", "")) != RewardTable.AUX_ITEM_TYPE: + continue + var aux_eff: String = String(row.get("effect_id", "")) + if aux_eff == "": # 负占位行(雷格极性字典兜底,零兑付)——不入投放词表校验 + continue + if not aux_live.has(aux_eff): + push_diag.call("error", "reward_dangling_aux", "RewardTable %s aux 行引用无效道具 %s(retired/deferred/不存在): %s" % [row.get("reward_id"), aux_eff, "双表校验"]) + # ①c F2 投放维度覆盖(魔法道具 GDD——五维度各 ≥1 掉落权重为正的活行):豁免面 {plan, info, counter} + # = 罗盘退役(plan,2026-09-29 拍板①乙)+显影件 Alpha 重定(info/counter 悬置)——豁免随 Alpha 重定批消减 + # ①c F2 投放维度覆盖校验随战斗掉落渠道退役(2026-10-01 用户拍板甲——挖格不掉道具,魔法道具 + # 唯一来路=三选一共享池〔节点/通关〕):旧校验=「掉落池五维度各 ≥1 正权重活行」,载体已随 + # reward.tres aux 投放四行删除而消失;共享池为「抽到道具位再动态掷定具体道具」,维度覆盖不可 + # 静态校验。静态覆盖保证的重立(如按维度钉共享池抽签面)归同调实施批/渠道设计批。 + # ② SettlementTable.effect_pool_map 悬空 + for eff in st_tab.effect_pool_map.keys(): + if not effect_ids.has(eff): + push_diag.call("error", "poolmap_dangling", "SettlementTable.effect_pool_map 悬空 effect_id: %s" % eff) + # ③ source_label_map 键集封闭性(键 = 增益状态 source_class ∪ {enemy_action, hero_attack, flag_miss}——2026-09-15 格面资源批删 item 键) + var required_sources := ["safe_cell", "flag_hit", "mine_hit", "clear_reward", "hero_attack", "enemy_action", "flag_miss"] + for s in required_sources: + if not st_tab.source_label_map.has(s): + push_diag.call("error", "segorder_violation", "source_label_map 缺必需键: %s" % s) + # ④ LevelTable 节点悬空引用(battle/boss 的 (L,node_depth) 须在 board_gen 与 enemy 两表有精确行—— + # 线性推进器按精确深度行跑,floor 兜底静默降难度属调参事故,加载期直接拦) + var lv: LevelTable = loaded.get("level") + var bg: BoardGenTable = loaded.get("board_gen") + var en: EnemyTable = loaded.get("enemy") + if lv != null and bg != null and en != null: + var bg_keys := {} + for row in bg.rows: + bg_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + var en_keys := {} + for row in en.rows: + en_keys["%d_%d" % [int(row.level_index), int(row.node_depth)]] = true + for lrow in lv.rows: + var li: int = int(lrow.level_index) + # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) + var node_lists: Array[Array] = [] + var tpls: Array = lrow.get("templates", []) + if tpls.is_empty(): + node_lists.append(lrow.get("nodes", [])) + else: + for tpl in tpls: + node_lists.append(tpl.get("nodes", [])) + for nodes_arr in node_lists: + for node in nodes_arr: + var t: String = String(node.get("type", "")) + if t != "battle" and t != "boss": + continue + var key := "%d_%d" % [li, int(node.get("node_depth", -1))] + if not bg_keys.has(key): + push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 board_gen 无精确行" % [li, t, key]) + if not en_keys.has(key): + push_diag.call("error", "level_node_dangling", "level %d %s 节点 (%s) 在 enemy 无精确行" % [li, t, key]) + # ⑤ 技能池引用完整性(敌人技能池批——敌人表池 × 技能表;独立入口 validate_skill_pool_links 供冒烟直验) + var skt = loaded.get("skill") + if skt != null and en != null: + for e in validate_skill_pool_links(skt, en, loaded.get("status_effects")): + push_diag.call("error", e.category, e.message) + + +# 池模板 × 技能行跨表校验(加载期致命;返回错误列表——ConfigDB 诊断通道与冒烟直验共用): +# skill_id 悬空致命 + 池构成纪律(每池恰一行普攻、增益技 ≤1、妨害技 ≤1——proposal §2.1 结构保证 + +# proposal-status-vocabulary §1.2)+ 施放类 side 守卫(增益引用 enemy 侧 / 妨害引用 hero 侧状态行)。 +# status_table 显式入参:装载序上本校验先于成员赋值执行(self.status_effects 尚为 Nil)——装载通道传 loaded 表, +# 冒烟直验传 _db.status_effects;传 null 时 side 守卫跳过(其他校验不受影响)。 +func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, status_table: StatusEffectTable = null) -> Array: + var errs: Array[Dictionary] = [] + var skill_types := {} + for row in skill_table.rows: + skill_types[String(row.get("skill_id", ""))] = String(row.get("type", "")) + # 施放类 side 守卫(新效应词汇批 2026-09-19):buff → 敌方侧状态 / debuff → 主角侧状态,防反向挂接 + for row in skill_table.rows: + var st: String = String(row.get("type", "")) + if st != "buff" and st != "debuff": + continue + var want_side: String = "enemy" if st == "buff" else "hero" + var eff: String = String(row.get("buff_ref", "")) + var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {} + if not srow.is_empty() and String(srow.get("target_side", "")) != want_side: + errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]}) + for row in enemy_table.rows: + var eid: String = String(row.get("enemy_id", "")) + var basic_n := 0 + var buff_n := 0 + var debuff_n := 0 + for entry in row.get("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]}) + continue + match skill_types[sid]: + "basic": + basic_n += 1 + "buff": + buff_n += 1 + "debuff": + debuff_n += 1 + if basic_n != 1: + errs.append({"category": "pool_composition", "message": "EnemyTable %s 池普攻行数 ≠ 1 (%d)——普攻冷却恒 0 = 空池不可能的结构前提" % [eid, basic_n]}) + if buff_n > 1: + errs.append({"category": "pool_composition", "message": "EnemyTable %s 池增益技 > 1 (%d)——每池增益 ≤1 纪律(纠偏三)" % [eid, buff_n]}) + if debuff_n > 1: + errs.append({"category": "pool_composition", "message": "EnemyTable %s 池妨害技 > 1 (%d)——每池妨害 ≤1 纪律(新效应词汇批 2026-09-19)" % [eid, debuff_n]}) + return errs diff --git a/game/core/config_db_cross_validator.gd.uid b/game/core/config_db_cross_validator.gd.uid new file mode 100644 index 00000000..ce288fba --- /dev/null +++ b/game/core/config_db_cross_validator.gd.uid @@ -0,0 +1 @@ +uid://d4dfucylnemk6 diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd new file mode 100644 index 00000000..159af6c3 --- /dev/null +++ b/game/core/config_db_resolver.gd @@ -0,0 +1,108 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# Date: 2026-10-02(code-quality 修复批 T5——ConfigDb 拆分:R1 难度行解析协作类,自 config_db.gd 迁出) +# 职责单一化(R1 解析域):resolve_board_gen / resolve_enemy_rowset / resolve_difficulty_modifiers +# 及其私有算术(_resolve_progress / _ceil_key)与 R1 专属的告警去重注册表。ConfigDb autoload 门面 +# 保持——全部消费方(战斗结算/敌人系统/棋盘生成/呈现层/冒烟)经门面同名方法取数,公有签名零变化。 +# 诊断通道归门面:push_diag / _fatal 经 _db 引用回呼(协作类同仓内部通道——不另设公共出口, +# 行为与拆分前逐位一致,含越域 Fatal 退出语义)。 +# 迁移注记:_warn_dedup 去重注册表随解析逻辑同迁(唯一消费方 = _resolve_progress)—— +# 门面 load_tables / reset_diagnostics 经 reset_warn_dedup() 同步清空,清空时点零变化。 +# 实现 GDD:design/archive/gdd-2026-09-14/d2-data-config.md(系统 ID D2·数据配置——registry 追溯锚) +class_name ConfigDbResolver +extends RefCounted + +var _db: ConfigDb # 门面引用(表读取 + 诊断/致命通道——_init 注入) +var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表(自 ConfigDb 迁入) + + +func _init(db_ref: ConfigDb) -> void: + _db = db_ref + + +# 去重注册表清空(门面 load_tables / reset_diagnostics 调用——与拆分前清空时点逐位一致) +func reset_warn_dedup() -> void: + _warn_dedup.clear() + + +# ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)---- + +func resolve_board_gen(L: int, D: int) -> Dictionary: + return _resolve_progress(_db.board_gen.rows, L, D, "board_gen") + + +func resolve_enemy_rowset(L: int, D: int) -> Dictionary: + return _resolve_progress(_db.enemy.rows, L, D, "enemy") + + +# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) +func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: + if _db.difficulty == null: + return {} + for row in _db.difficulty.rows: + if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D: + return { + "hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0), + "atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0), + } + return {} + + +func _resolve_progress(rows: Array, L: int, D: int, table_id: String) -> Dictionary: + if L < 1 or D < 0: + _db._fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) + return {} + var level_keys: Array[int] = [] + for row in rows: + var lv := int(row.get("level_index", 0)) + if not level_keys.has(lv): + level_keys.append(lv) + if level_keys.is_empty(): + _db._fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id) + return {} + level_keys.sort() + var l_star := _ceil_key(level_keys, L) + var l_clamped := l_star < 0 + if l_clamped: + l_star = int(level_keys[-1]) # 超出最大行 → 取最大行封顶(2026-09-18 追加拍板:可取范围内向上取整,超出用最大值) + var depth_keys: Array[int] = [] + for row in rows: + if int(row.get("level_index", 0)) == l_star: + var dv := int(row.get("node_depth", 0)) + if not depth_keys.has(dv): + depth_keys.append(dv) + depth_keys.sort() + var d_star := _ceil_key(depth_keys, D) + var d_clamped := d_star < 0 + if d_clamped: + d_star = int(depth_keys[-1]) + var missing := "" + if l_clamped and d_clamped: + missing = "both_clamp" + elif l_clamped: + missing = "level_clamp" + elif d_clamped: + missing = "depth_clamp" + elif l_star != L and d_star != D: + missing = "both_ceil" + elif l_star != L: + missing = "level_ceil" + elif d_star != D: + missing = "depth_ceil" + if missing != "": + var dedup_key := "%s|%s|%d|%d" % [table_id, missing, l_star, d_star] + if not _warn_dedup.has(dedup_key): + _warn_dedup[dedup_key] = true + var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" + _db.push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) + for row in rows: + if int(row.get("level_index", 0)) == l_star and int(row.get("node_depth", 0)) == d_star: + return row + return {} + + +# 取 sorted_keys 中第一个 >= request 的键;无(请求高于最大行)返回 -1 → 调用方 Fatal +func _ceil_key(sorted_keys: Array, request: int) -> int: + for k in sorted_keys: + if int(k) >= request: + return int(k) + return -1 diff --git a/game/core/config_db_resolver.gd.uid b/game/core/config_db_resolver.gd.uid new file mode 100644 index 00000000..0f76496e --- /dev/null +++ b/game/core/config_db_resolver.gd.uid @@ -0,0 +1 @@ +uid://bv7guty0wowk diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index 7899f7ba..556eea04 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -49,9 +49,9 @@ const TARGET_HERO := "hero" const TARGET_ALL_ALIVE := "all_alive_enemies" var db: ConfigDb -var rng -var units # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用) -var effects # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面) +var rng: RngServiceCore # 注入成员类型注(2026-10-02 修复批——单测经真实 RngService 构造,标注安全) +var units: UnitContainer # UnitContainer——目标解析/伤害交付/存活查询(句柄即 Dictionary 引用) +var effects: StatusContainer # StatusContainer——池值/AC 型效果读取(增益状态 只读消费面) var emit_row_cb: Callable # 行协议注入口:func(type, source_class, origin_cell, ri, display, value, extra, highlight) var policy := POLICY_SEGMENT # 结账时机开关(2026-09-24 用户拍板第二步拨 B=攒批段界统一结;回切 A 改回 POLICY_IMMEDIATE 即还原迁移前行为) diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index b7714906..9c4713d4 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -61,16 +61,19 @@ var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区 var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零) var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零) # 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口 -var _settlement = null # 当前战斗编排器(关卡结束置 null) -var aux_inventory = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续, +var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) +var aux_inventory: AuxItems = null # 魔法道具 关卡域库存(迭代 7 T3——AuxItems;跨战斗延续, var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放) # 关卡末清空+快照计数入 level_finished 载荷) var hero_atk_pct := 0 # 测试面板覆盖值(切片专用) var hero_hp_pct := 0 -var _running := false -var level_result := "" # "" / "victory" / "defeat" -var _awaiting_choice := false # 选路态(兑付确认后、pick 前为零自动推进的可见停点) +# ---- 关卡相位轴(2026-10-02 修复批——布尔旗收拢:_running/_awaiting_choice 两旗 → 显式 Phase enum; +# 对齐 行动经济 Phase 先例(action_economy.gd:内部 enum + 守卫换算,外部读面契约零变化)—— +# level_result:String 与 awaiting_choice() 被 AdventureRunner/呈现层消费的语义逐字不变)---- +enum Phase { IDLE, RUNNING, AWAITING_CHOICE, ENDED } +var _phase: int = Phase.IDLE +var level_result := "" # "" / "victory" / "defeat"(外部契约面——语义零变化) var _candidates: Array[Dictionary] = [] # 当前选路候选(节点引用数组) var visited: Array[Dictionary] = [] # 已兑付/到达节点序列(呈现层图条数据源;含当前节点) var _graph_mode := false # 图形态缓存(行集任一节点带 next;单链 = false) @@ -81,6 +84,11 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: rng = rng_ref +# 进行中谓词(原 _running 语义——RUNNING 或 AWAITING_CHOICE 两相位合计;内部守卫专用) +func _is_running() -> bool: + return _phase == Phase.RUNNING or _phase == Phase.AWAITING_CHOICE + + # ---- 测试面板覆盖(模拟天赋树属性提升——仅测试用,正式侧归 元进度;经 encounter 传入战斗)---- func set_hero_override(atk_pct: int, hp_pct: int) -> void: @@ -135,7 +143,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = return false node_index = -1 level_result = "" - _awaiting_choice = false + _phase = Phase.IDLE _candidates = [] visited = [] _graph_mode = false @@ -152,11 +160,11 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = hero_hp = effective_max_hp() rng.new_adventure(seed_val) # 关卡起点 = 冒险起点(切片口径;跨关接续归 冒险结构) level_row.assign(_resolve_template_row(row, template_pick)) # 池解析在播种后(node_map 抽签须已播种——EC7) - _running = true + _phase = Phase.RUNNING var entry := _entry_node() if entry.is_empty(): push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)") - _running = false + _phase = Phase.ENDED return false return _enter_node(entry) @@ -189,7 +197,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti level_row.assign(_resolve_template_row(row, template_pick)) node_index = -1 level_result = "" - _awaiting_choice = false + _phase = Phase.IDLE _candidates = [] visited = [] _graph_mode = false @@ -197,7 +205,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti if node.has("next"): _graph_mode = true break - _running = true + _phase = Phase.RUNNING # 断点续玩路径(迭代 8 卡①——defer_entry 之外的第三种停点入口):直接重建到停点所在节点, # 零 RNG 消耗(模板已随 template_pick 强制、节点状态纯重建不 _enter_node——当场战斗从头打)。 # resume_state = {"node_id": int(-1 = 入口停点), "visited_ids": Array[int]};空字典 = 既有路径逐位不变。 @@ -207,10 +215,10 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti var entry := _entry_node() if entry.is_empty(): push_error("[关卡节点地图] 入口节点缺失(无入边唯一节点——图合法性校验应已拦截)") - _running = false + _phase = Phase.ENDED return false if defer_entry: - _awaiting_choice = true # 入口停点(node_index 保持 -1——进节点前无 current_node) + _phase = Phase.AWAITING_CHOICE # 入口停点(node_index 保持 -1——进节点前无 current_node) _candidates = [entry] return true return _enter_node(entry) @@ -219,7 +227,7 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti # 入口停点呈现(停点前移批 2026-09-23——defer_entry=true 的第二步):发出入口候选供呈现层弹 # 选路层;非停点态(缺省自动进入路径)拒绝。零 RNG 消耗——呈现与行进取号解耦。 func present_entry_choice() -> bool: - if not _running or not _awaiting_choice or _candidates.is_empty() or node_index != -1: + if _phase != Phase.AWAITING_CHOICE or _candidates.is_empty() or node_index != -1: push_error("[关卡节点地图] present_entry_choice 拒绝——非入口停点态(defer_entry 未开或已 pick)") return false choices_presented.emit(_candidates) @@ -237,9 +245,9 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool: var entry := _entry_node() if entry.is_empty(): push_error("[关卡节点地图] 断点恢复失败——入口节点缺失") - _running = false + _phase = Phase.ENDED return false - _awaiting_choice = true + _phase = Phase.AWAITING_CHOICE _candidates = [entry] return true var stop: Dictionary = {} @@ -249,7 +257,7 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool: break if stop.is_empty(): push_error("[关卡节点地图] 断点恢复失败——停点节点编号 %d 不在本关图" % node_id) - _running = false + _phase = Phase.ENDED return false node_index = nodes().find(stop) visited = [] @@ -258,11 +266,11 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool: if _node_id(node) == int(vid): visited.append(node) break - _awaiting_choice = true + _phase = Phase.AWAITING_CHOICE _candidates = _out_nodes(stop) if _candidates.is_empty(): push_error("[关卡节点地图] 断点恢复失败——停点节点 %d 无出边(非合法停点)" % node_id) - _running = false + _phase = Phase.ENDED return false return true @@ -270,7 +278,7 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool: # 停点呈现(断点续玩路径——present_entry_choice 的中途节点版):信号顺序契约同入口 # (呈现层须先经 level_started 重取 runner 实例)。零 RNG 消耗。 func present_resumed_choice() -> bool: - if not _running or not _awaiting_choice or _candidates.is_empty() or node_index < 0: + if _phase != Phase.AWAITING_CHOICE or _candidates.is_empty() or node_index < 0: push_error("[关卡节点地图] present_resumed_choice 拒绝——非中途停点态") return false choices_presented.emit(_candidates) @@ -286,10 +294,7 @@ func restore_aux_items(items: Array) -> void: func _find_level_row(level_index: int) -> Dictionary: - for row in db.level.rows: - if int(row.level_index) == level_index: - return row - return {} + return db.level_row(level_index) # 表访问收口(2026-10-02——行集直扫归 LevelTable/ConfigDB 访问器) # ---- 模板池解析(关卡节点图生成 v0.4——M-3 迭代 5 T5)---- @@ -411,11 +416,11 @@ func visited_ids() -> Array: func awaiting_choice() -> bool: - return _awaiting_choice + return _phase == Phase.AWAITING_CHOICE func _enter_node(node: Dictionary) -> bool: - if not _running: + if not _is_running(): return false node_index = nodes().find(node) visited.append(node) @@ -490,7 +495,7 @@ func _on_battle_finished(payload: Dictionary) -> void: var hp_left := hero_hp if result == "victory" else 0 if not _external: close_wallet() # 单关模式:关卡收束即冒险收束(GP 清零);冒险模式归 AdventureRunner._settle - _running = false + _phase = Phase.ENDED _settlement = null var aux_cleared := 0 if aux_inventory != null: @@ -505,12 +510,17 @@ func _on_battle_finished(payload: Dictionary) -> void: _present_choices() -# 恢复数学单一路径(漂移③清偿——同一公式两个落点:本体内置默认 + AdventureRunner.apply_rest, -# 统一归并归后续批):floor(上限 × 比例 ÷ 100) 后封顶缺口;冒险结构 GDD 公式逐位一致 -func rest_heal_amount(pct: int, hp: int, max_hp: int) -> int: +# 恢复数学单一路径(漂移③清偿;2026-10-02 修复批——同式双落点收口:算术宿主 = 本静态方法, +# AdventureRunner.apply_rest 同式委托,字面量双写退役)。floor(上限 × 比例 ÷ 100) 后封顶缺口; +# 冒险结构 GDD 公式逐位一致 +static func rest_heal(pct: int, hp: int, max_hp: int) -> int: return mini(int(floor(float(max_hp) * float(pct) / 100.0)), max_hp - hp) +func rest_heal_amount(pct: int, hp: int, max_hp: int) -> int: + return rest_heal(pct, hp, max_hp) # 公有成文出口保持(start_level 内置恢复执行体经 Callable 引用) + + func _apply_rest() -> int: var pct: int = int(level_row.get("rest_heal_pct", 0)) # 接口③治疗封顶回报:单关模式 = 内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest @@ -528,7 +538,7 @@ func _present_choices() -> bool: if cands.is_empty(): push_error("[关卡节点地图] 当前节点无出边且非 Boss(图合法性校验应已拦截)") return false - _awaiting_choice = true + _phase = Phase.AWAITING_CHOICE _candidates = cands choices_presented.emit(cands) return true @@ -536,11 +546,11 @@ func _present_choices() -> bool: # 玩家选下一跳(候选外编号拒绝;Boss 恒无出边——选路只在中间节点发生) func pick_next_node(node_id: int) -> bool: - if not _awaiting_choice: + if _phase != Phase.AWAITING_CHOICE: return false for cand in _candidates: if _node_id(cand) == node_id: - _awaiting_choice = false + _phase = Phase.RUNNING _candidates = [] return _enter_node(cand) return false @@ -563,7 +573,7 @@ func set_aux_unlocked(ids: Dictionary) -> void: func grant_aux_item(item_id: String, source: String) -> Dictionary: if aux_inventory == null: aux_inventory = AuxItems.new(db) - if aux_inventory.holding_count() + 1 > maxi(0, int(db.adventure.transitional_sync_slots)): + if not AuxItems.can_accept(1, aux_inventory.holding_count(), db): # 同调位守卫单点(2026-10-02) push_warning("[关卡节点地图] 同调位已满,授予拒绝: %s(%s)" % [item_id, source]) return {} return aux_inventory.add_item(item_id, source, Vector2i(-1, -1), 0) @@ -642,7 +652,8 @@ func buy_product(product_id: String) -> bool: return false var price := current_price(prow) if gp < price: - return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝) + push_warning("[关卡节点地图] 购买拒绝——余额不足: %s(需 %d,钱包 %d)" % [product_id, price, gp]) + return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝;2026-10-02 补静默留痕) gp -= price var flat := int(prow.get("flat_value", 0)) bonus[attr] = int(bonus[attr]) + flat diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 7ecf70e2..4bf60bb4 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -74,7 +74,7 @@ func _workshop_progress(channel: String) -> int: match channel: "total_clears": var total := 0 - for i in db.level.rows.size(): + for i in db.level_count(): # 表访问收口(2026-10-02——行集直扫退役) total += save.clear_count(i + 1) return total _: diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 757111d5..6bb50563 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -416,9 +416,10 @@ func _do_stop() -> Dictionary: # ---- 轮末旗判定 + 棋盘路径 round_end 外发 ---- func _emit_flags_batch() -> void: - # 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性);被拔的不判;诚实重复上报(去重归 战斗结算) + # 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性——排序键单点 BoardData.row_major_key, + # 2026-10-02 修复批);被拔的不判;诚实重复上报(去重归 战斗结算) var cells: Array = _flags.keys() - cells.sort_custom(func(a, b): return a.y * 1000 + a.x < b.y * 1000 + b.x) + cells.sort_custom(func(a, b): return BoardData.row_major_key(a) < BoardData.row_major_key(b)) for c in cells: flag_judged.emit({"cell": c, "hit": _board.is_mine(c)}) diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index 7960ee12..fae67b5c 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -41,7 +41,7 @@ func roll_board(eligible: Array) -> bool: if eligible.is_empty(): _fatal("roll_board 空资格清单(E1 兜底 fail-fast——排雷玩法 不变式④前置)") return false - # 行主序规范化 + 去重(排序责任不外推给 战斗结算) + # 行主序规范化 + 去重(排序责任不外推给 战斗结算;排序键单点 BoardData.row_major_key——2026-10-02 修复批) var seen := {} var list: Array[Dictionary] = [] for e in eligible: @@ -51,7 +51,7 @@ func roll_board(eligible: Array) -> bool: continue seen[c] = true list.append({"cell": c}) - list.sort_custom(func(a, b): return a.cell.y * 1000 + a.cell.x < b.cell.y * 1000 + b.cell.x) + list.sort_custom(func(a, b): return BoardData.row_major_key(a.cell) < BoardData.row_major_key(b.cell)) _eligible = list _granted.clear() # 新批重置映射受理面(旧图快照消亡) _power_roll.clear() diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index 6173892c..8a05b211 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -62,8 +62,9 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_override: var hp_mult: float = float(mod.get("hp_mult", 1.0)) var atk_mult: float = float(mod.get("atk_mult", 1.0)) # unit_count_override ≥1 = 战斗结算 遭遇骰结果(敌人系统 零随机纪律——收到即确定值;-1 = 行值定值路径) + # 数量域常量单点 = EnemyTable.UNIT_COUNT_MIN/MAX(2026-10-02 修复批——与表校验同源) var count: int = unit_count_override if unit_count_override >= 1 else int(row.unit_count) - if count < 1 or count > 3: + if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX: push_warning("[敌人系统] 实例化数量越域: %d" % count) return false for i in range(count): diff --git a/game/data/schemas/adventure_table.gd b/game/data/schemas/adventure_table.gd index 9665eb98..a0c8ef64 100644 --- a/game/data/schemas/adventure_table.gd +++ b/game/data/schemas/adventure_table.gd @@ -18,6 +18,10 @@ const BONUS_CAP_UNLIMITED := -1 # —— 通关部分回血(固定型,非末关通关兑付)—— @export var clear_heal_pct: int = 20 # [0,100] knob——回复生效上限的百分比,封顶上限不回满 +# —— 三选一候选数(2026-10-02 修复批 T2 魔法数结构化:自 adventure_runner 抽签字面量 3 收口进表; +# 默认 3 = 现行行为零改动——加权洗牌不放回掷 N 的 N,改值须重扫三选一体验与牌库剔除节奏)—— +@export var bonus_candidate_count: int = 3 # [1,6] knob——候选数上界受 BONUS_IDS 词表封闭约束 + # —— 三选一件值(兑付面)—— @export var bonus_attack_flat: int = 1 # [0,999] knob——攻击轴累加(覆盖值通道) @export var bonus_max_hp_flat: int = 5 # [0,999] knob——生命上限轴累加(不连带当前生命) @@ -93,6 +97,8 @@ func validate() -> Array: errs.append({"category": "domain", "message": "bonus_cap(%s) 越域 [0,99]: %d" % [id, c]}) if bonus_aux_item_count < 1 or bonus_aux_item_count > 9: errs.append({"category": "domain", "message": "bonus_aux_item_count 越域 [1,9]: %d" % bonus_aux_item_count}) + if bonus_candidate_count < 1 or bonus_candidate_count > BONUS_IDS.size(): + errs.append({"category": "domain", "message": "bonus_candidate_count 越域 [1,%d]: %d" % [BONUS_IDS.size(), bonus_candidate_count]}) if transitional_sync_slots < 0 or transitional_sync_slots > 9: errs.append({"category": "domain", "message": "transitional_sync_slots 越域 [0,9]: %d" % transitional_sync_slots}) return errs diff --git a/game/data/schemas/board_gen_table.gd b/game/data/schemas/board_gen_table.gd index ad5f4b40..eef3ff8f 100644 --- a/game/data/schemas/board_gen_table.gd +++ b/game/data/schemas/board_gen_table.gd @@ -51,8 +51,8 @@ func validate() -> Array: var att: int = int(row.get("max_gen_attempts", 0)) if att < 1 or att > 32: errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]}) - # 布雷数下限(R3)——零雷板拒绝 - var n_mines := int(round(d * float(w * h - 9))) + # 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS——2026-10-02 修复批) + var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS))) if n_mines < 1: errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]}) # F2b min_safe 可行性 diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index 781493fc..d5b62e70 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -15,6 +15,11 @@ extends Resource # 2026-09-15 命名统一批:位置键 {none, a, b} → 语义键 {light, ward, heavy} = 轻甲 / 魔法护甲 / 重甲) const ARMOR_TYPES := ["light", "ward", "heavy"] +# 实例化数量域(2026-10-02 修复批 T2 魔法数结构化——数量域 [1,3] 双落点收口单点: +# 本表 validate 与 单位容器 spawn 守卫共用;遭遇骰结果与行值 unit_count/unit_count_max 同域) +const UNIT_COUNT_MIN := 1 +const UNIT_COUNT_MAX := 3 + func validate() -> Array: var errs: Array[Dictionary] = [] @@ -39,13 +44,13 @@ func validate() -> Array: if String(row.get("display_name", "")) == "": errs.append({"category": "domain", "message": "%s display_name 空" % eid}) var uc: int = int(row.get("unit_count", 0)) - if uc < 1 or uc > 3: - errs.append({"category": "domain", "message": "%s unit_count 越域 [1,3]" % eid}) + if uc < UNIT_COUNT_MIN or uc > UNIT_COUNT_MAX: + errs.append({"category": "domain", "message": "%s unit_count 越域 [%d,%d]" % [eid, UNIT_COUNT_MIN, UNIT_COUNT_MAX]}) # unit_count_max(可省略——遭遇随机化批,难度曲线与Boss GDD Core Rule 8):区间 [unit_count, max],缺省 = 定值 if row.has("unit_count_max"): var ucm: int = int(row.get("unit_count_max", uc)) - if ucm < uc or ucm > 3: - errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),3]" % [eid, ucm, uc]}) + if ucm < uc or ucm > UNIT_COUNT_MAX: + errs.append({"category": "cross_field", "message": "%s unit_count_max(%d) 越域 [unit_count(%d),%d]" % [eid, ucm, uc, UNIT_COUNT_MAX]}) var hp: int = int(row.get("hp", 0)) if hp < 1 or hp > 999: errs.append({"category": "domain", "message": "%s hp 越域 [1,999]" % eid}) diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index 64619156..7767a3cb 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -20,6 +20,14 @@ extends Resource const NODE_TYPES := ["battle", "reward", "rest", "boss", "shop"] # 商店房批 2026-09-21 四→五类(关卡节点地图 v1.1) +# 行单点读取(2026-10-02 修复批 T5 表访问收口——LevelRunner._find_level_row 直扫收口;键 = level_index) +func row_by_level_index(level_index: int) -> Dictionary: + for row in rows: + if int(row.level_index) == level_index: + return row + return {} + + func validate() -> Array: var errs: Array[Dictionary] = [] if rows.is_empty(): diff --git a/game/presentation/tune_sim_controller.gd b/game/presentation/tune_sim_controller.gd index 83267881..9df1f240 100644 --- a/game/presentation/tune_sim_controller.gd +++ b/game/presentation/tune_sim_controller.gd @@ -1,29 +1,41 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-12(code-review 修复批——自 main.gd 拆出,规范 1.1/1.2 模拟编排独立成类) -# 调整板批量模拟控制器:逐次驱动 LevelAutoplay(与 headless 扫描同口径)+ 进度/报告 setter 注入面板。 +# 调整板批量模拟控制器:逐次驱动 关卡自动策略(与 headless 扫描同口径)+ 进度/报告 setter 注入面板。 # 职责边界:零核心写——表基线复位由组合根在移交前完成(_apply_hero_override(0,0)), -# 加成施加 = LevelAutoplay 内部 runner.set_hero_override 单路径;报告文案装配归 HudPresenter。 +# 加成施加 = 自动策略内部 runner.set_hero_override 单路径;报告文案装配归 HudPresenter。 # 组合根只转发 simulate_requested 信号。await 依赖面板所在场景树(进度逐帧刷新)。 +# 反向依赖解除(2026-10-02 修复批 T6):presentation → smoke 的 preload 编译期依赖拆除—— +# 驱动实例由调用方构造注入(_driver,须供 run_level(...) 与 smoke/level_autoplay 同款签名); +# 构造参数缺省 null = 内部自建旧行为(运行时 load 兜底,同种子同口径零漂移)——main 侧接线归主控后续卡。 extends RefCounted const SEED_BASE := 4200 # 模拟种子基(与历史实现同源——同种子可回放) -const LevelAutoplay := preload("res://smoke/level_autoplay.gd") # 与 headless 扫描同口径的自动策略(P3:preload 编译期路径校验) +const PlaytestLogger := preload("res://presentation/playtest_logger.gd") # 试玩日志(同层依赖——运行时 load 收敛为 preload 常量) var db var panel # TunePanel setter 面:atk_pct/hp_pct/set_sim_running/set_sim_progress/set_sim_report var _base_hp := 0 # 报告中的"基础 HP"口径注记值 +var _driver = null # 关卡自动策略驱动(调用方注入;null = 旧行为运行时 load 兜底) -func _init(db_ref, panel_ref, base_hp: int) -> void: +func _init(db_ref, panel_ref, base_hp: int, driver = null) -> void: db = db_ref panel = panel_ref _base_hp = base_hp + _driver = driver + + +# 驱动解析:注入优先;缺省回退运行时 load(旧行为——编译期反向依赖已解除,运行时口径与拆分前逐位一致) +func _autoplay(): + if _driver != null: + return _driver + return load("res://smoke/level_autoplay.gd") # 与 headless 扫描同口径的自动策略(P3 兜底路径) func run(n: int) -> void: # 共享日志会话(2026-09-12 用户要求:模拟运行与真人试玩同日志合规——操作/轮次/战斗/通关全量 JSONL; # 一文件多局,run 行携带种子切分局次) - var logger = load("res://presentation/playtest_logger.gd").new() + var logger = PlaytestLogger.new() logger.ap_per_round = int(db.action_economy.ap_per_round) var log_path: String = logger.start_session(SEED_BASE) print("[tune_sim] 模拟日志: %s" % log_path) @@ -35,8 +47,9 @@ func run(n: int) -> void: var hp_boss: Array[float] = [] var rounds_all: Array[int] = [] var death_nodes := {} + var autoplay = _autoplay() for i in range(n): - var st = LevelAutoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) + var st = autoplay.run_level(db, SEED_BASE + i, 1, panel.atk_pct(), panel.hp_pct(), false, logger) dmg_all.append(st.hero_dmg) rounds_all.append(st.rounds) if st.result == "victory": From 0691a188fd191224fc9256cc35905a2a3bb336bb Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 08:46:37 +0800 Subject: [PATCH 4/8] =?UTF-8?q?T3=20=E5=91=88=E7=8E=B0=E5=B1=82=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E6=AE=B5:=20main.gd=201395=E2=86=921237=E4=B8=89?= =?UTF-8?q?=E4=BB=B6=E8=BF=81=E5=87=BA=EF=BC=88RowPacer=E6=8E=92=E6=92=AD?= =?UTF-8?q?=E6=B3=B5/AuxInteractController=E4=B8=A4=E6=AD=A5=E4=BA=A4?= =?UTF-8?q?=E4=BA=92=E5=90=AB-9,-9=E5=93=A8=E5=85=B5=E6=94=B9=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E6=97=A0=E7=9B=AE=E6=A0=87/GP=E5=BD=B1=E5=AD=90?= =?UTF-8?q?=E8=B4=A6=E6=9C=AC=E7=8B=AC=E7=AB=8B=E4=BB=B6=EF=BC=89+?= =?UTF-8?q?=E6=A8=A1=E6=80=81=E6=A0=88=E6=94=B6=E5=8F=A3=EF=BC=88settings/?= =?UTF-8?q?camp/tune=E6=94=B9=E8=B5=B0=5Fopen=5Fmodal+=5Finput=5Flocked?= =?UTF-8?q?=E6=94=B9ModalStack.has=5Fopen=E6=9F=A5=E8=AF=A2+=E4=B8=89?= =?UTF-8?q?=E4=BE=8B=E5=A4=96=EF=BC=89+HUD=E5=8D=95=E9=80=9A=E9=81=93?= =?UTF-8?q?=EF=BC=88=E6=94=B6=E5=B0=BE=E5=8F=8D=E9=A6=88/=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E8=A7=86=E8=A7=89=E8=BF=9Bpresenter=EF=BC=8Cplay=5Fro?= =?UTF-8?q?und=5Fsweep=E6=97=B6=E5=BA=8F=E6=95=8F=E6=84=9F=E7=95=99?= =?UTF-8?q?=E7=BB=84=E5=90=88=E6=A0=B9=E6=B3=A8=E8=AE=B0=EF=BC=89+?= =?UTF-8?q?=E9=93=BE=E5=BC=8F=E7=A9=BF=E9=80=8F=E6=B8=85=E9=99=A4=EF=BC=88?= =?UTF-8?q?variation=E5=9B=9E=E8=B0=83=E6=B3=A8=E5=85=A5/phase=E7=BB=8Fsna?= =?UTF-8?q?pshot=EF=BC=89+confirm=5Fdialog=E6=9E=84=E5=BB=BA=E5=BA=8F?= =?UTF-8?q?=E5=A4=8D=E4=BD=8D=E5=88=A0move=5Fchild=E5=88=9B=E5=8F=AF?= =?UTF-8?q?=E8=B4=B4+=E7=99=BB=E8=AE=B0=E5=BA=8F=E6=A0=91=E5=BA=8F?= =?UTF-8?q?=E6=BC=82=E7=A7=BB=E5=A4=8D=E4=BD=8D=EF=BC=9B=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E9=9D=A2=3Dmain.=5Frow=5Fqueue=E5=A7=94=E6=89=98/pace=5Fdelay?= =?UTF-8?q?=5Ffor/=5Fflush=5Frow=5Fqueue/=5Fadventure=5Fgp=5Fview=E5=86=99?= =?UTF-8?q?=E9=9D=A2=E5=85=A8=E6=95=B0=E6=A0=B8=E5=AF=B9=E8=BF=87=EF=BC=9B?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=8D=A1=E6=B3=A8=3Dmain=E5=88=B0900?= =?UTF-8?q?=E8=BF=98=E9=9C=80=E8=BF=81=E4=B8=89=E9=80=89=E4=B8=80=E7=BC=96?= =?UTF-8?q?=E6=8E=92=E4=B8=8Edebug=E6=8E=A5=E7=BA=BF=EF=BC=8806=E7=99=BB?= =?UTF-8?q?=E8=AE=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/aux_interact_controller.gd | 141 +++++++ .../aux_interact_controller.gd.uid | 1 + game/presentation/gp_shadow_ledger.gd | 31 ++ game/presentation/gp_shadow_ledger.gd.uid | 1 + game/presentation/hud_presenter.gd | 23 + game/presentation/main.gd | 396 ++++++------------ game/presentation/modal_stack.gd | 9 + game/presentation/row_pacer.gd | 144 +++++++ game/presentation/row_pacer.gd.uid | 1 + game/presentation/views/confirm_dialog.gd | 9 +- 10 files changed, 473 insertions(+), 283 deletions(-) create mode 100644 game/presentation/aux_interact_controller.gd create mode 100644 game/presentation/aux_interact_controller.gd.uid create mode 100644 game/presentation/gp_shadow_ledger.gd create mode 100644 game/presentation/gp_shadow_ledger.gd.uid create mode 100644 game/presentation/row_pacer.gd create mode 100644 game/presentation/row_pacer.gd.uid diff --git a/game/presentation/aux_interact_controller.gd b/game/presentation/aux_interact_controller.gd new file mode 100644 index 00000000..40580426 --- /dev/null +++ b/game/presentation/aux_interact_controller.gd @@ -0,0 +1,141 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 魔法道具两步交互控制器(迭代 7 T4;T3 拆件批 2026-10-02 自 main.gd 迁出——规范 1.1 单一职责): +# C 区槽位选中/取消(toggle)→ 棋盘点格使用目标拾取的两步交互状态机 + 库存/叠加层只读投影装配。 +# 数据源 = runner 级库存只读面(query_inventory/query_overlays)+ 表行投影(display_name/target/effect_kinds); +# 写调用唯一经 战斗结算.use_aux(core 侧四步编排——拒绝零消耗 toast 可辨认)。 +# 装配纪律:settlement/runner 随关卡逐关更替——经组合根现值回调注入(仿 identity_resolver 只读回调 +# 先例);呈现句柄(信息面板/toast/棋盘叠加层)由组合根注入;ADR-0001——信号连接不在此发生, +# info_panel.aux_slot_clicked 由组合根接至 on_slot_clicked。 +# 无目标使用显式化(T3 收口):AP 类(target=none)本无坐标可给——原调用面伪造 Vector2i(-9,-9) +# 哨兵坐标;现调用面一律 attempt_use(handle)(cell 缺省=null=显式「无目标」),core 契约的 +# Vector2i 形参收口于本类常量(aux_items.validate_use 对 ap 行忽略目标值,哨兵不出本类)。 +extends RefCounted + +const NO_TARGET := Vector2i(-9, -9) # core use_aux 形参占位(仅 target=none 行到达——validate_use 忽略该值) + +var db +var meta # 解锁门控数据源(工坊解锁×活行交集——冒险开始组装;组合根 _ready 建立后注入) +var info_panel: HeroInfoPanelView +var toast: ToastStack # 反馈面(board_zone.toast 注入) +var board_view: BoardView +var _settlement_get: Callable # () → 当前战斗结算现值(组合根 lambda) +var _runner_get: Callable # () → 当前关卡推进器现值(组合根 lambda) +var _use_selection := 0 # 选中句柄(迭代 7 T4——0=未选中;选中期棋盘点击=使用目标拾取) + + +func _init(db_ref, meta_ref, info_panel_ref: HeroInfoPanelView, toast_ref: ToastStack, + board_view_ref: BoardView, settlement_get: Callable, runner_get: Callable) -> void: + db = db_ref + meta = meta_ref + info_panel = info_panel_ref + toast = toast_ref + board_view = board_view_ref + _settlement_get = settlement_get + _runner_get = runner_get + + +# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 活行词表;罗盘退役/失义件不入集) +func unlocked_set() -> Dictionary: + var out := {} + if meta == null: + return out + for row in db.aux_active_rows(): + var iid := String(row.get("item_id", "")) + if meta.save.item_unlocked(iid): + out[iid] = true + return out + + +# 选中态读数(组合根棋盘点击劫持判据——0=未选中) +func active_selection() -> int: + return _use_selection + + +# 槽点击(第一步):AP 类(target=none)点击即用(无目标);空间类进/出选中态(toggle 取消)。 +func on_slot_clicked(slot_handle: int) -> void: + var settlement = _settlement_get.call() + var runner = _runner_get.call() + if settlement == null or runner == null or runner.aux_inventory == null: + return + if slot_handle == 0: + _use_selection = 0 + refresh_slots() + return + var rec: Dictionary = {} + for r in inventory_projection(): + if int(r.get("handle", 0)) == slot_handle: + rec = r + break + if rec.is_empty() or bool(rec.get("used", false)): + return + var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", ""))) + if String(arow.get("target", "none")) == "none": + attempt_use(slot_handle) # AP 类:显式无目标即用(不再伪造哨兵坐标) + return + if not bool(rec.get("usable", true)): + toast.show_toast("当前无可预读的未开格", "warn") + return + _use_selection = 0 if _use_selection == slot_handle else slot_handle # toggle 取消 + refresh_slots() + if _use_selection != 0: + toast.show_toast("%s:点击目标格使用(9 宫窗口),再点道具取消" % String(rec.get("name", "")), "info") + + +# 使用受理(第二步;cell=空间类目标格,null=无目标;core 侧 use_aux 四步编排——拒绝零消耗 toast 可辨认) +func attempt_use(handle: int, cell: Variant = null) -> void: + var settlement = _settlement_get.call() + if settlement == null: + return + var target: Vector2i = cell if cell != null else NO_TARGET + var v: Dictionary = settlement.use_aux(handle, target) + if not bool(v.ok): + toast.show_toast(String(v.get("reason", "无法使用")), "warn") + return + _use_selection = 0 + refresh_slots() + refresh_overlays() + var arow: Dictionary = db.aux_item_row(String(v.get("item_id", ""))) + toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info") + + +# 库存投影(C 区四槽取数面——字形=名首字;usable=板级零信息预判回调接排雷玩法只读面) +func inventory_projection() -> Array: + var runner = _runner_get.call() + if runner == null or runner.aux_inventory == null: + return [] + var settlement = _settlement_get.call() + var ms = settlement.minesweeper if settlement != null else null + var cb := Callable() + if ms != null: + cb = func(_c: Vector2i) -> bool: + for y in ms.current_board().height: + for x in ms.current_board().width: + if not ms.is_revealed(Vector2i(x, y)): + return true + return false + var out: Array = [] + for rec in runner.aux_inventory.query_inventory(cb): + var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", ""))) + var dn := String(arow.get("display_name", "")) + out.append({ + "handle": int(rec.get("handle", 0)), + "item_id": String(rec.get("item_id", "")), + "glyph": dn.left(1) if dn != "" else "?", + "name": dn, + "used": bool(rec.get("used", false)), + "usable": bool(rec.get("usable", true)), + }) + return out + + +func refresh_slots() -> void: + info_panel.set_aux_inventory(inventory_projection(), _use_selection) + + +# 叠加层投影注入(use 成功/三清后——棋盘标记与预读徽章重算) +func refresh_overlays() -> void: + var runner = _runner_get.call() + if runner == null or runner.aux_inventory == null or board_view == null: + return + var ov: Dictionary = runner.aux_inventory.query_overlays() + board_view.set_aux_overlays(ov.get("mine_reveal", []), ov.get("reward_preview", [])) diff --git a/game/presentation/aux_interact_controller.gd.uid b/game/presentation/aux_interact_controller.gd.uid new file mode 100644 index 00000000..b98ab061 --- /dev/null +++ b/game/presentation/aux_interact_controller.gd.uid @@ -0,0 +1 @@ +uid://cgqtlpkl0ypn0 diff --git a/game/presentation/gp_shadow_ledger.gd b/game/presentation/gp_shadow_ledger.gd new file mode 100644 index 00000000..d075d9bf --- /dev/null +++ b/game/presentation/gp_shadow_ledger.gd @@ -0,0 +1,31 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# GP 冒险钱包呈现侧影子账本(迭代 6 T7 卡⑩;T3 拆件批 2026-10-02 自 main.gd 迁出——规范 1.1): +# 冒险进行中余额影子 + 收束清零拍转存的最终合计,供冒险终局面板 GP 行取数。 +# 手工镜像边界:wallet_changed gain/buy 载荷 = 入账后总额(level_runner.gd:482/539),"clear" 到达即 +# 影子转存 final 并归零——纯呈现侧账本,零核心触碰(C1 呈现层方案);写入口唯一 = apply_event +#(组合根 _on_wallet_changed 投喂;探针合成注入经 set_view_total——组合根委托面转发)。 +extends RefCounted + +var _view := 0 # 冒险进行中余额影子(gain/buy 载荷直录) +var _final := 0 # 收束清零拍转存的最终合计("clear" 到达时自影子转存) + + +func apply_event(event: String, gp: int) -> void: + match event: + "gain", "buy": + _view = gp + "clear": + _final = _view + _view = 0 + + +func set_view_total(v: int) -> void: + _view = v # 探针合成注入面(ui_probe 影子账清零转存断言) + + +func view_total() -> int: + return _view + + +func final_total() -> int: + return _final diff --git a/game/presentation/gp_shadow_ledger.gd.uid b/game/presentation/gp_shadow_ledger.gd.uid new file mode 100644 index 00000000..496868fd --- /dev/null +++ b/game/presentation/gp_shadow_ledger.gd.uid @@ -0,0 +1 @@ +uid://uk77ei6hjhca diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index 4bc7f651..6b926a05 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -83,6 +83,29 @@ static func _refresh_economy(ap_bar: ApBarView, s: Dictionary, db, refresh_remai ap_bar.set_stop_enabled(bool(s.hero.get("alive", false)) and digging) +# ---- 战斗收尾反馈与结算视觉同步(T3 HUD 单通道批 2026-10-02——组合根直改视图收敛入此) ---- + +# battle_over 收尾反馈:死亡兜底灰化 → 主角态回 idle → RAP 清零脉冲(调试收尾路)→ 状态全清可见反馈。 +# 调试/冒险两路共用;reset_rap=false = 冒险收束(RAP 清零由战斗末拍已毕,收束拍不再重放脉冲)。 +static func battle_end_feedback(stage: BattleStageView, ap_bar: ApBarView, reset_rap: bool) -> void: + stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) + stage.settle_hero_idle() # 主角态回 idle(迭代 6 T3 三态收尾) + if reset_rap: + ap_bar.set_rap(0) # RAP 清零脉冲 + 微文案(载体 2026-09-26 迁 D 区 AP 条) + stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体迁 B 区站桩) + + +# 结算阶段视觉同步(round_end → 下一轮):D 区去饱和 + 禁挖光 + 明细带 settle/dig 换挡 + 轮次文案。 +# 光带演出(play_round_sweep)属时序敏感触发,留在组合根(上升沿检测随行)——本条只做态驱动 setter。 +static func sync_settlement_visuals(stage: BattleStageView, board_view: BoardView, + log_view: BattleLogView, settling: bool, phase_text: String) -> void: + board_view.set_desaturated(settling) + board_view.set_dig_glow_enabled(not settling) + stage.set_settle_focus(settling) + log_view.set_mode("settle" if settling else "dig") + stage.set_round_text(phase_text) + + # 战斗外(节点间隙/关卡结束)——HP 读 runner 携带值 static func _refresh_out_of_battle(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApBarView, runner, level_active: bool, db) -> void: diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 4b29cd45..fc156228 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -18,7 +18,16 @@ # 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归 # ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归 # debug_session.gd——本类不拼呈现字符串。 -# ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于驱动视图状态。 +# T3 拆件批(2026-10-02,规范 1.1 单一职责):结算行排播泵迁 row_pacer.gd(本类只入队/排空/读队列, +# 探针/测试委托面 _row_queue/pace_delay_for/_flush_row_queue 原位保留);魔法道具两步交互迁 +# aux_interact_controller.gd(本类只接线与转发意图,无目标使用显式化——哨兵坐标不出控制器); +# GP 冒险钱包影子账迁 gp_shadow_ledger.gd(本类留探针同名读写委托面)。模态开合全量走 +# _open_modal/_close_modal 单口,输入锁=modal_stack.has_open() 开态查询+三例外;battle 收尾反馈与 +# 结算视觉 setter 收敛 hud_presenter 单通道(时序敏感演出触发留本类并注释);settlement 相位读取改经 +# presentation_snapshot 成文面、变异表读取直读本根 db(settlement.db 即同一实例)——禁 +# settlement.action_economy / settlement.db 链式穿透。 +# ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于 +# 驱动视图状态;信号连接只在组合根(row_pacer/aux_interact_controller 零连接,只收注入)。 extends Control const UiAssembler := preload("res://presentation/ui_assembler.gd") @@ -26,6 +35,9 @@ const Narrator := preload("res://presentation/battle_narrator.gd") const ModalStack := preload("res://presentation/modal_stack.gd") const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") +const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批) +const AuxInteractController := preload("res://presentation/aux_interact_controller.gd") # 魔法道具两步交互(T3 拆件批) +const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批) const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律) @@ -67,16 +79,32 @@ var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/ var board_view: BoardView # 探针兼容别名(= board_zone.board_view) var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下) + +# T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui) +var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) +var _aux = null # 魔法道具两步交互(aux_interact_controller.gd——本类只接线与转发意图) +var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂) + +# GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况) +var _adventure_gp_view: int: + get: + return _gp_ledger.view_total() + set(value): + _gp_ledger.set_view_total(value) +var _adventure_gp_final: int: + get: + return _gp_ledger.final_total() + +# 排播队列只读委托面(探针/测试读 main._row_queue——RowPacer 迁移后保留的同名可读面,共享同一 Array 引用) +var _row_queue: Array: + get: + return _row_pacer.pending_rows() + # 模态 overlay(z 序 = 登记序;pattern 6 层级纪律) var legend_overlay: LegendOverlay var review_overlay: SettlementReviewOverlay var node_overlay: NodeOverlay var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现) -var _adventure_gp_view := 0 # GP 冒险钱包呈现侧影子(迭代 6 T7——终局面板 GP 行取数面, - # wallet_changed gain/buy 载荷=入账后总额 level_runner.gd:482/539) -var _adventure_gp_final := 0 # 收束清零拍暂存的最终合计("clear" 到达时自影子转存) -var _aux_use_selection := 0 # 魔法道具选中句柄(迭代 7 T4——0=未选中;选中期棋盘点击=使用目标拾取) -var _aux_unlocked := {} # aux 解锁门控集(迭代 7 T5——工坊解锁×活行交集,冒险开始组装) var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现) var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效) var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面) @@ -89,17 +117,6 @@ var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_r var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批) -# ---- 结算行排播队列(迭代 8 卡⑦——演出层排播:逻辑先行零回压,行按段分组阶梯延迟进明细带)---- -# 硬契约:核心层零 await 不破——本队列只做信号的"后置消费",结算顺序/结果/时序逻辑零改动(§3 契约 1); -# 减动效开=延迟归零走同帧直落快路(行呈现不跳过——§3 契约 5);模态开启前强制排空(明细回看/停点 -# 覆盖层所见明细带恒完整)。 -var _row_queue: Array = [] # 待排播行(step_row 载荷原样入队;__round_line 标记=轮首分隔行) -var _row_pace_wait := 0.0 # 距下一行排播的剩余秒(_process 消耗) -var _pace_prev_segment := "" # 上一排播行的段型(段间/段内延迟判据) -var _was_pumping := false # 排播活跃边沿(T17——末行消费帧触发结算视觉回挖掘态刷新) -const PACE_SEGMENT_GAP := 0.3 # 段间阶梯延迟(代拍板:白名单 0.2–0.4s 内取值,追认清单呈报) -const PACE_ROW_GAP := 0.22 # 段内间隔(三轮 #3 上调 0.06→0.22——逐条演出可感知,配合数字浮现/状态脉冲) - func _ready() -> void: db = get_node("/root/ConfigDB") @@ -141,9 +158,24 @@ func _build_ui() -> void: board_view = battle_ui.board_view # 探针兼容别名(= board_zone.board_view) ap_bar = battle_ui.ap_bar # 探针兼容别名(= board_zone.ap_bar) settlement_fx = battle_ui.settlement_fx + + # 结算行排播泵(T3 拆件批——迁 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) + _row_pacer = RowPacer.new() + _row_pacer.log_view = log_view + _row_pacer.stage = stage + _row_pacer.board_zone = board_zone + _row_pacer.settlement_fx = settlement_fx + _row_pacer.board_view = board_view + _row_pacer.refresh_request = _refresh_presentations + add_child(_row_pacer) + # 魔法道具两步交互(T3 拆件批——迁 aux_interact_controller.gd;settlement/runner 随关卡更替, + # 经现值回调注入——仿 identity_resolver 只读回调先例) + _aux = AuxInteractController.new(db, meta, info_panel, board_zone.toast, board_view, + func() -> Variant: return settlement, func() -> Variant: return runner) + log_view.review_requested.connect(_open_review_from_band) board_zone.action_requested.connect(_on_board_action) - info_panel.aux_slot_clicked.connect(_on_aux_slot_clicked) # 迭代 7 T4——两步交互第一步 + info_panel.aux_slot_clicked.connect(_aux.on_slot_clicked) # 迭代 7 T4——两步交互第一步 board_zone.stop_pressed.connect(_on_stop_pressed) board_zone.refresh_confirmed.connect(_on_refresh_confirmed) board_zone.legend_toggled.connect(_on_legend_toggled) @@ -159,7 +191,8 @@ func _build_ui() -> void: logger = PlaytestLogger.new() logger.ap_per_round = int(db.action_economy.ap_per_round) - # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 选路 < 终局 < 商店 < 调整板 < 开始) + # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 + # < 商店 < 营地 < 调整板 < 设置 < 确认弹窗) var overlays: Dictionary = UiAssembler.build_overlays(self, db, int(db.hero.atk), int(db.hero.max_hp)) legend_overlay = overlays.legend_overlay review_overlay = overlays.review_overlay @@ -190,10 +223,6 @@ func _build_ui() -> void: settings_overlay = SettingsOverlay.new(settings_store) # 迭代 8 卡④——最小设置面(音量/全屏/减动效) add_child(settings_overlay) settings_overlay.closed.connect(_on_settings_closed) - confirm_dialog = ConfirmDialog.new() # 二轮修改批 A——新冒险互斥确认弹窗 - add_child(confirm_dialog) - confirm_dialog.confirmed.connect(_on_confirm_new_adventure) - confirm_dialog.cancelled.connect(_on_confirm_cancelled) # 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进) bonus_overlay = BonusPickOverlay.new() add_child(bonus_overlay) @@ -201,10 +230,6 @@ func _build_ui() -> void: bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen) bonus_overlay.continue_pressed.connect(_on_bonus_continue) move_child(bonus_overlay, _end_overlay.get_index()) - _modals = ModalStack.new() - for m: Control in [legend_overlay, review_overlay, node_overlay, bonus_overlay, level_screen, _end_overlay, shop_overlay, camp_overlay, tune_panel, settings_overlay, confirm_dialog]: - _register_modal(m) - _tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp)) var start: Dictionary = UiAssembler.build_start_overlay(self, db) _start_overlay = start.overlay @@ -227,6 +252,20 @@ func _build_ui() -> void: var diag_btn := UiAssembler.build_diag_button(self) diag_btn.pressed.connect(_on_diagnose_pressed) + # 确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化):树序落在开始层/过场层/诊断钮之后 = 子级最末 + #(T3 收口批——「登记序 = 树序 = z 序」纪律复位,confirm_dialog.open 的 move_child 运行时自提 + # 创可贴随之删除;弹窗只在开始菜单域弹出,绘制序恒最上) + confirm_dialog = ConfirmDialog.new() + add_child(confirm_dialog) + confirm_dialog.confirmed.connect(_on_confirm_new_adventure) + confirm_dialog.cancelled.connect(_on_confirm_cancelled) + + # 模态登记(缺陷⑤纪律——登记序 = 树序:含 move_child 调位后的关卡画面/三选一终位) + _modals = ModalStack.new() + for m: Control in [legend_overlay, review_overlay, node_overlay, level_screen, bonus_overlay, _end_overlay, shop_overlay, camp_overlay, tune_panel, settings_overlay, confirm_dialog]: + _register_modal(m) + _tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp)) + func _fatal_screen(msg: String) -> void: var l := UiKit.label(msg, 20, true) @@ -260,10 +299,9 @@ func _on_modal_closed(m: Control) -> void: func _input_locked() -> bool: - # 模态/非战斗期棋盘输入锁(各 overlay 自身可见即锁定) - return legend_overlay.is_open() or review_overlay.is_open() or _end_overlay.is_open() \ - or node_overlay.visible or level_screen.visible or shop_overlay.visible or tune_panel.visible \ - or _start_overlay.visible or _inter_overlay.visible or confirm_dialog.visible or settlement == null + # 模态/非战斗期棋盘输入锁(T3 收口批——开态查询归 modal_stack.has_open(),登记册单一来源, + # 不再手工罗列各层;例外三件:开始层/过场层(非模态全屏态,不在登记册)与战斗缺位(棋盘输入无载体)) + return _modals.has_open() or _start_overlay.visible or _inter_overlay.visible or settlement == null # ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)==================== @@ -324,7 +362,7 @@ func _resume_from_menu() -> void: log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) adventure.set_hero_override(tune_panel.atk_pct(), tune_panel.hp_pct()) - if not adventure.resume_adventure(bp, {"hero_override": meta.hero_override() if meta != null else {}, "aux_unlocked": _aux_unlocked_set()}): + if not adventure.resume_adventure(bp, {"hero_override": meta.hero_override() if meta != null else {}, "aux_unlocked": _aux.unlocked_set()}): _fatal_screen("续玩恢复失败——见输出日志") return @@ -362,7 +400,7 @@ func _start_adventure() -> void: # 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签 var first_script: bool = not save_manager.is_first_adventure_done() - if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux_unlocked_set()}): + if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}, "first_script": first_script, "aux_unlocked": _aux.unlocked_set()}): _fatal_screen("冒险启动失败——见输出日志") return @@ -547,9 +585,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: settlement = null _exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 _sync_settlement_visuals() # → 全关 - stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体 2026-09-26 迁 B 区站桩) - stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) - stage.settle_hero_idle() # 主角态回 idle(迭代 6 T3 三态收尾) + HudPresenter.battle_end_feedback(stage, ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道(T3 收口批) # 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary[String, Variant] = {} @@ -663,14 +699,19 @@ func _scaled(base: int, pct: int) -> int: return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0))) -# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音) +# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; +# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) func _identity_lookup(c: Vector2i): - if settlement == null or settlement.minesweeper.current_board() == null or settlement.action_economy.phase != settlement.action_economy.PHASE_ACTIVE: + if settlement == null or settlement.minesweeper.current_board() == null: + return null + if String(settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: return null return settlement.query_reward_identity(c) -# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动) +# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动。 +# 变异表直读本根 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession +# 构造传参),免 settlement 内部链式穿透,T3 收口批) func _variant_mask_lookup(c: Vector2i) -> bool: if settlement == null: return false @@ -678,14 +719,14 @@ func _variant_mask_lookup(c: Vector2i) -> bool: if b == null: return false for vid in b.variant_ids(c): - var vrow: Dictionary = settlement.db.variation.row_by_id(String(vid)) + var vrow: Dictionary = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) == "hide_number": return true return false # 地形变异 角标解析(迭代 6 T6——伤害型变异未开格视觉标记;读取方按格子变异数据行动,规则本体零特判。 -# 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络) +# 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络。变异表直读本根 db——同上款收口) func _variant_mark_lookup(c: Vector2i) -> String: if settlement == null: return "" @@ -693,7 +734,7 @@ func _variant_mark_lookup(c: Vector2i) -> String: if b == null: return "" for vid in b.variant_ids(c): - var vrow: Dictionary = settlement.db.variation.row_by_id(String(vid)) + var vrow: Dictionary = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) == "flat_damage": return "flat_damage" return "" @@ -709,10 +750,7 @@ func _on_battle_finished(payload: Dictionary) -> void: # 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派) var result: String = String(payload.get("result", "")) _sync_settlement_visuals() # → 全关(battle_over 反馈面) - stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面) - stage.settle_hero_idle() # 主角态回 idle(迭代 6 T3 三态收尾) - ap_bar.set_rap(0) # RAP 清零脉冲 + 微文案(载体 2026-09-26 迁 D 区 AP 条) - stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体迁 B 区站桩) + HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道(T3 收口批) var decision: Dictionary = _debug.on_battle_finished(payload) match String(decision.outcome): "next_layer": @@ -751,126 +789,34 @@ func _wire_battle(settlement_ref) -> void: settlement_ref.board_changed.connect(_on_board_changed) +# ==================== 结算行排播(泵本体归 row_pacer.gd——本节仅入口与探针/测试委托面)==================== + func _on_step_row(row: Dictionary) -> void: # 卡⑦ 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放 if UiKit.motion_scale < 1.0: - _consume_row(row) + _row_pacer.consume_now(row) return - _row_queue.append(row) + _row_pacer.enqueue(row) -# 行消费(排播队列的出队步 / 减动效快路共用):明细带入带+演出链+呈现刷新——与卡⑦前排位逐位一致 -func _consume_row(row: Dictionary) -> void: - if row.has("__round_line"): - log_view.finish_settlement_round() # 上轮摘要行(明细带 pattern 3)随轮首分隔行落带 - log_view.append_bb("[color=#e0b860]── 第 %d 轮 ──[/color]" % int(row["__round_line"])) - return - log_view.append_row(row) - stage.on_step_row_fx(row) - _play_cross_zone_fx(row) # 跨区因果演出(结算演出链批):开格之因 → 站桩之果 - _play_settlement_mark(row) # 三轮 #3——逐条标出:Buff/Debuff 色环脉冲 → 伤害/治疗数字浮现 - # 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道 - if String(row.get("type", "")) == "highlight": - board_zone.fx.play_perfect_sweep() - elif String(row.get("highlight", "")) == "flag_hit": - board_zone.fx.play_flag_hit_projectile(true) - _refresh_presentations() - - -# 逐条结算标出(三轮 #3——用户裁决按结算顺序演出:先 Buff/Debuff 特效、再基础伤害数字; -# 打中谁标谁、主角被打标主角;道具/奖励行逐条冒出)。目标解析失败兜底棋盘中心。 -func _play_settlement_mark(row: Dictionary) -> void: - var mark_type := String(row.get("type", "")) - if mark_type != "effect" and mark_type != "damage" and mark_type != "heal": - return - var target := String(row.get("target", "")) - var center: Vector2 - if target == "hero": - center = stage.hero_standee_center() - else: - var v = stage.enemy_standee_center(target) if target != "" and target != "all_enemies" else null - center = v if v != null else board_view.get_global_rect().get_center() - match mark_type: - "effect": - var col: Color = Color("7cc47c") if int(row.get("value", 0)) > 0 else Color("c47c7c") - settlement_fx.play_status_pulse(center, col) - "damage": - settlement_fx.play_damage_number(center, "-%d" % int(row.get("value", 0)), - Color("d9402a"), bool(row.get("crit", false))) - "heal": - settlement_fx.play_damage_number(center, "+%d" % int(row.get("value", 0)), Color("7cc47c"), false) - - -func _process(delta: float) -> void: - # 排播泵(卡⑦——演出层帧驱动;队列空=零开销直返) - if _row_queue.is_empty(): - _row_pace_wait = 0.0 - _pace_prev_segment = "" - if _was_pumping: - # 演播完边沿:结算视觉期由排播队列承载(_is_settling 排播项),队列排空的翻转 - # 无核心信号驱动——此处主动刷回挖掘态(去饱和解除/明细带收起/mode=dig)。 - _was_pumping = false - _refresh_presentations.call_deferred() - return - _was_pumping = true - _row_pace_wait -= delta - if _row_pace_wait > 0.0: - return - var entry: Dictionary = _row_queue.pop_front() - _consume_row(entry) - var seg := String(entry.get("type", "")) - _row_pace_wait = 0.0 if UiKit.motion_scale < 1.0 else (PACE_SEGMENT_GAP if seg != _pace_prev_segment else PACE_ROW_GAP) - _pace_prev_segment = seg - - -# 段间延迟取数面(减动效双态断言用——开=0 归零,行不跳过;常态=段间/段内两档) +# 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面) func pace_delay_for(prev_seg: String, next_seg: String) -> float: - if UiKit.motion_scale < 1.0: - return 0.0 - return PACE_SEGMENT_GAP if prev_seg != next_seg else PACE_ROW_GAP + return _row_pacer.pace_delay_for(prev_seg, next_seg) -# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整) +# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读) func _flush_row_queue() -> void: - while not _row_queue.is_empty(): - _consume_row(_row_queue.pop_front()) - _row_pace_wait = 0.0 - _pace_prev_segment = "" - - -# 跨区因果演出(§Transitions 结算逐行兑现——动画密度终稿归战斗表现样张批): -# 伤害行 → ⚔ 弹道自棋盘飞向目标敌方站桩(crit 金档加重);治疗行 → ✚ 弹道飞向主角站桩; -# 命中反馈(闪白/血条递减/灰化)由站桩侧 on_step_row_fx/set_data 承接 -func _play_cross_zone_fx(row: Dictionary) -> void: - var from_v: Vector2 = board_view.get_global_rect().get_center() - match String(row.get("type", "")): - "damage": - var target := String(row.get("target", "")) - var crit := bool(row.get("crit", false)) - if target == "" or target == "hero": - # 主角受击弹道(迭代 6 T5 拆「敌→主角零反馈」早退):⚔ 危险红飞向主角站桩; - # crit 只加重字号不加金(金=收益/胜利语义位,不用于敌方对主角输出) - settlement_fx.play_projectile(from_v, stage.hero_standee_center(), "⚔", - UiKit.RED_MINE, crit) - return - var to_v = stage.enemy_standee_center(target) - if to_v != null: - settlement_fx.play_projectile(from_v, to_v, "⚔", - UiKit.GOLD if crit else UiKit.RED_MINE, crit) - "heal": - if String(row.get("target", "")) != "hero" and String(row.get("target", "")) != "": - return - settlement_fx.play_projectile(from_v, stage.hero_standee_center(), "✚", Color("7cc47c")) + _row_pacer.flush() func _on_round_started(ri: int) -> void: # 卡⑦:轮首分隔行随排播队列走(落在前轮各行之后——带序正确);轮横幅即轮切换时刻发射 - #(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过)。 + #(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用 + # _consume_row 轮首分隔行分支,与原直写逐字等价)。 if UiKit.motion_scale < 1.0: - log_view.finish_settlement_round() - log_view.append_bb("[color=#e0b860]── 第 %d 轮 ──[/color]" % ri) + _row_pacer.consume_now({"__round_line": ri}) else: - _row_queue.append({"__round_line": ri}) + _row_pacer.enqueue({"__round_line": ri}) settlement_fx.play_round_banner(ri) _refresh_used_round = 0 _refresh_presentations() @@ -1006,12 +952,13 @@ func _open_camp() -> void: return meta.refresh_workshop_unlocks() _start_overlay.visible = false + _open_modal(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈) camp_overlay.open_camp(_camp_data()) _refresh_presentations() func _on_camp_closed() -> void: - camp_overlay.visible = false + _close_modal(camp_overlay) # 模态栈单口(T3 收口批) _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新 _refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面) @@ -1021,11 +968,11 @@ func _on_camp_closed() -> void: func _open_settings() -> void: _start_overlay.visible = false - settings_overlay.visible = true + _open_modal(settings_overlay) # 模态栈单口(T3 收口批) func _on_settings_closed() -> void: - settings_overlay.visible = false + _close_modal(settings_overlay) # 模态栈单口(T3 收口批) _start_overlay.visible = true _refresh_resume_button() @@ -1083,116 +1030,12 @@ func _on_shop_leave() -> void: runner.continue_after_node() -# ---- 魔法道具 HUD 接线(迭代 7 T4——库存条+两步交互+叠加层呈现;数据源=runner 级库存只读面)---- - -# ---- 奖励节点三选一(迭代 7 T5——拍板②丙:奖励屏=小构筑点;「节点奖励=功能道具正式渠道」兑现)---- -# aux 解锁门控集组装(迭代 7 T5——拍板③甲:工坊解锁 ∩ 活行词表;罗盘退役/失义件不入集) -func _aux_unlocked_set() -> Dictionary: - var out := {} - if meta == null: - return out - for row in db.aux_active_rows(): - var iid := String(row.get("item_id", "")) - if meta.save.item_unlocked(iid): - out[iid] = true - return out - - -# 槽点击(第一步):AP 类(target=none)点击即用(无目标);空间类进/出选中态(toggle 取消)。 -func _on_aux_slot_clicked(slot_handle: int) -> void: - if settlement == null or runner == null or runner.aux_inventory == null: - return - if slot_handle == 0: - _aux_use_selection = 0 - _refresh_aux_slots() - return - var rec: Dictionary = {} - for r in _aux_inventory_projection(): - if int(r.get("handle", 0)) == slot_handle: - rec = r - break - if rec.is_empty() or bool(rec.get("used", false)): - return - var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", ""))) - if String(arow.get("target", "none")) == "none": - _attempt_aux_use(slot_handle, Vector2i(-9, -9)) # AP 类:无目标即用 - return - if not bool(rec.get("usable", true)): - board_zone.toast.show_toast("当前无可预读的未开格", "warn") - return - _aux_use_selection = 0 if _aux_use_selection == slot_handle else slot_handle # toggle 取消 - _refresh_aux_slots() - if _aux_use_selection != 0: - board_zone.toast.show_toast("%s:点击目标格使用(9 宫窗口),再点道具取消" % String(rec.get("name", "")), "info") - - -# 使用受理(第二步;core 侧 use_aux 四步编排——拒绝零消耗 toast 可辨认) -func _attempt_aux_use(handle: int, cell: Vector2i) -> void: - if settlement == null: - return - var v: Dictionary = settlement.use_aux(handle, cell) - if not bool(v.ok): - board_zone.toast.show_toast(String(v.get("reason", "无法使用")), "warn") - return - _aux_use_selection = 0 - _refresh_aux_slots() - _refresh_aux_overlays() - var arow: Dictionary = db.aux_item_row(String(v.get("item_id", ""))) - board_zone.toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info") - - -# 库存投影(C 区四槽取数面——字形=名首字;usable=板级零信息预判回调接排雷玩法只读面) -func _aux_inventory_projection() -> Array: - if runner == null or runner.aux_inventory == null: - return [] - var ms = settlement.minesweeper if settlement != null else null - var cb := Callable() - if ms != null: - cb = func(_c: Vector2i) -> bool: - for y in ms.current_board().height: - for x in ms.current_board().width: - if not ms.is_revealed(Vector2i(x, y)): - return true - return false - var out: Array = [] - for rec in runner.aux_inventory.query_inventory(cb): - var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", ""))) - var dn := String(arow.get("display_name", "")) - out.append({ - "handle": int(rec.get("handle", 0)), - "item_id": String(rec.get("item_id", "")), - "glyph": dn.left(1) if dn != "" else "?", - "name": dn, - "used": bool(rec.get("used", false)), - "usable": bool(rec.get("usable", true)), - }) - return out - - -func _refresh_aux_slots() -> void: - info_panel.set_aux_inventory(_aux_inventory_projection(), _aux_use_selection) - - -# 叠加层投影注入(use 成功/三清后——棋盘标记与预读徽章重算) -func _refresh_aux_overlays() -> void: - if runner == null or runner.aux_inventory == null or board_view == null: - return - var ov: Dictionary = runner.aux_inventory.query_overlays() - board_view.set_aux_overlays(ov.get("mine_reveal", []), ov.get("reward_preview", [])) - - func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 logger.log_wallet(event, gp, extra) - match String(event): # GP 影子账(迭代 6 T7——收束清零前取值,纯呈现零核心触碰=C1 呈现层方案) - "gain", "buy": - _adventure_gp_view = gp - if String(event) == "gain": - _last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) - "clear": - _adventure_gp_final = _adventure_gp_view - _adventure_gp_view = 0 - _last_gain_amount = 0 + _gp_ledger.apply_event(event, gp) # GP 影子账(迭代 6 T7——收束清零前取值,纯呈现零核心触碰=C1;账本归 gp_shadow_ledger.gd) + if String(event) == "gain": + _last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) if shop_overlay.visible and runner != null: shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) _refresh_presentations() @@ -1205,7 +1048,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: if _input_locked(): return # 结算演播期棋盘冻结(T17 用户明确令 v3——不可点/不可移/不可挖): - # 锁面=排播队列活跃(纯队列判据;_row_pace_wait 有末行尾迹窗口不可作演播中标志)。 + # 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。 # 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前 # _flush_row_queue 自行排空)。v1 撞收尾链的教训对策:ui_click_probe 的排播期 stall # 豁免随批(合法停顿态),Waiting 收尾等待期**不豁免**——probe 报卡即为真回归信号。 @@ -1213,14 +1056,15 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: return # 魔法道具选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格; # 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗) - if _aux_use_selection != 0 and kind == "dig": - _attempt_aux_use(_aux_use_selection, cell) + var aux_sel: int = _aux.active_selection() + if aux_sel != 0 and kind == "dig": + _aux.attempt_use(aux_sel, cell) return var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}]) var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted) logger.log_action(kind, cell, accepted) if runner != null and runner.aux_inventory != null: - _refresh_aux_slots() # 迭代 7 T4——授予发生在 settle_batch 结算段内(同步走完,行尾重查) + _aux.refresh_slots() # 迭代 7 T4——授予发生在 settle_batch 结算段内(同步走完,行尾重查) _handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {}) # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固) @@ -1306,34 +1150,32 @@ func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() if settling and not _was_settling: - settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带(演出链批) + settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带(演出链批)——时序敏感演出触发,例外留在组合根 _was_settling = settling - board_view.set_desaturated(settling) - board_view.set_dig_glow_enabled(not settling) - stage.set_settle_focus(settling) - log_view.set_mode("settle" if settling else "dig") - stage.set_round_text(_phase_text()) + HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, _phase_text()) func _is_settling() -> bool: # T17 修复:结算视觉期跟随排播演播到放完。核心层同步走完(轮末结算后逻辑秒回 Active), # 旧判定让演播期棋盘提前恢复可挖观感=用户「没意识到在结算演出」感知缺口的根源; - # 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(_row_pace_wait - # 在末行消费后到下帧泵清零前有 >0 尾迹窗口,不可作演播中标志)。battle 收尾演播例外: + # 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹 + # 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外: # overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。 + # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透(T3 收口批)。 if settlement == null or not settlement.is_battle_live(): return not _row_queue.is_empty() - if settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING: + if String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: return true return not _row_queue.is_empty() func _phase_text() -> String: - # 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫) + # 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫;相位取数经 + # presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) if settlement == null: return "第 — 轮 · —" return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result), - settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING, + String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, _boss_active, Narrator.boss_name(runner, db)) @@ -1368,11 +1210,11 @@ func _on_diagnose_pressed() -> void: func _show_tune_panel() -> void: _start_overlay.visible = false - tune_panel.visible = true + _open_modal(tune_panel) # 模态栈单口(T3 收口批) func _close_tune_panel() -> void: - tune_panel.visible = false + _close_modal(tune_panel) # 模态栈单口(T3 收口批) _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新 diff --git a/game/presentation/modal_stack.gd b/game/presentation/modal_stack.gd index 8d54fe06..642cf409 100644 --- a/game/presentation/modal_stack.gd +++ b/game/presentation/modal_stack.gd @@ -32,3 +32,12 @@ func close(m: Control) -> void: if is_instance_valid(h): h.visible = true _occluded.erase(m) + + +# 开态查询(T3 模态收口批 2026-10-02——组合根输入锁唯一判据:登记册上是否有任一层处于开态; +# 可见性即开态,与 open/close 记账同源——组合根不再手工罗列各层) +func has_open() -> bool: + for m in _list: + if is_instance_valid(m) and m.visible: + return true + return false diff --git a/game/presentation/row_pacer.gd b/game/presentation/row_pacer.gd new file mode 100644 index 00000000..f0707be9 --- /dev/null +++ b/game/presentation/row_pacer.gd @@ -0,0 +1,144 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 结算行排播泵(迭代 8 卡⑦;T3 拆件批 2026-10-02 自 main.gd 整体迁出——规范 1.1 单一职责): +# 演出层排播:逻辑先行零回压,行按段分组阶梯延迟进明细带。 +# 硬契约:核心层零 await 不破——本队列只做信号的"后置消费",结算顺序/结果/时序逻辑零改动(§3 契约 1); +# 减动效开=延迟归零走同帧直落快路(行呈现不跳过——§3 契约 5);模态开启前强制排空(明细回看/停点 +# 覆盖层所见明细带恒完整——排空入口在组合根 _open_modal)。 +# 装配纪律:视图句柄与呈现刷新回调由组合根注入(ADR-0001——本类零信号连接,仅做信号后置消费); +# 行消费与卡⑦前逐位一致(_consume_row 原文迁入,语义零改动)。 +extends Node + +const PACE_SEGMENT_GAP := 0.3 # 段间阶梯延迟(代拍板:白名单 0.2–0.4s 内取值,追认清单呈报) +const PACE_ROW_GAP := 0.22 # 段内间隔(三轮 #3 上调 0.06→0.22——逐条演出可感知,配合数字浮现/状态脉冲) + +var _rows: Array = [] # 待排播行(step_row 载荷原样入队;__round_line 标记=轮首分隔行) +var _pace_wait := 0.0 # 距下一行排播的剩余秒(_process 消耗) +var _prev_segment := "" # 上一排播行的段型(段间/段内延迟判据) +var _was_pumping := false # 排播活跃边沿(T17——末行消费帧触发结算视觉回挖掘态刷新) + +# 组合根注入面(_build_ui 装配——呈现句柄 + 演播完边沿的呈现刷新回调) +var log_view: BattleLogView +var stage: BattleStageView +var board_zone: BoardZoneView +var settlement_fx: SettlementFxView +var board_view: BoardView +var refresh_request := Callable() # () → 组合根 _refresh_presentations(行尾直呼/演播完边沿 call_deferred) + + +# 队列只读面(探针/测试经组合根委托属性读——共享同一 Array 引用,读数即实况) +func pending_rows() -> Array: + return _rows + + +# 入队(常态路径——减动效关;step_row 载荷原样,零加工) +func enqueue(row: Dictionary) -> void: + _rows.append(row) + + +# 同帧直落(减动效快路——延迟归零,行呈现不跳过;轮首分隔行减动效分支同用此口) +func consume_now(row: Dictionary) -> void: + _consume_row(row) + + +# 段间延迟取数面(减动效双态断言用——开=0 归零,行不跳过;常态=段间/段内两档) +func pace_delay_for(prev_seg: String, next_seg: String) -> float: + if UiKit.motion_scale < 1.0: + return 0.0 + return PACE_SEGMENT_GAP if prev_seg != next_seg else PACE_ROW_GAP + + +# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整) +func flush() -> void: + while not _rows.is_empty(): + _consume_row(_rows.pop_front()) + _pace_wait = 0.0 + _prev_segment = "" + + +func _process(delta: float) -> void: + # 排播泵(卡⑦——演出层帧驱动;队列空=零开销直返) + if _rows.is_empty(): + _pace_wait = 0.0 + _prev_segment = "" + if _was_pumping: + # 演播完边沿:结算视觉期由排播队列承载(_is_settling 排播项),队列排空的翻转 + # 无核心信号驱动——此处主动刷回挖掘态(去饱和解除/明细带收起/mode=dig)。 + _was_pumping = false + refresh_request.call_deferred() + return + _was_pumping = true + _pace_wait -= delta + if _pace_wait > 0.0: + return + var entry: Dictionary = _rows.pop_front() + _consume_row(entry) + var seg := String(entry.get("type", "")) + _pace_wait = 0.0 if UiKit.motion_scale < 1.0 else (PACE_SEGMENT_GAP if seg != _prev_segment else PACE_ROW_GAP) + _prev_segment = seg + + +# 行消费(排播队列的出队步 / 减动效快路共用):明细带入带+演出链+呈现刷新——与卡⑦前排位逐位一致 +func _consume_row(row: Dictionary) -> void: + if row.has("__round_line"): + log_view.finish_settlement_round() # 上轮摘要行(明细带 pattern 3)随轮首分隔行落带 + log_view.append_bb("[color=#e0b860]── 第 %d 轮 ──[/color]" % int(row["__round_line"])) + return + log_view.append_row(row) + stage.on_step_row_fx(row) + _play_cross_zone_fx(row) # 跨区因果演出(结算演出链批):开格之因 → 站桩之果 + _play_settlement_mark(row) # 三轮 #3——逐条标出:Buff/Debuff 色环脉冲 → 伤害/治疗数字浮现 + # 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道 + if String(row.get("type", "")) == "highlight": + board_zone.fx.play_perfect_sweep() + elif String(row.get("highlight", "")) == "flag_hit": + board_zone.fx.play_flag_hit_projectile(true) + refresh_request.call() + + +# 逐条结算标出(三轮 #3——用户裁决按结算顺序演出:先 Buff/Debuff 特效、再基础伤害数字; +# 打中谁标谁、主角被打标主角;道具/奖励行逐条冒出)。目标解析失败兜底棋盘中心。 +func _play_settlement_mark(row: Dictionary) -> void: + var mark_type := String(row.get("type", "")) + if mark_type != "effect" and mark_type != "damage" and mark_type != "heal": + return + var target := String(row.get("target", "")) + var center: Vector2 + if target == "hero": + center = stage.hero_standee_center() + else: + var v = stage.enemy_standee_center(target) if target != "" and target != "all_enemies" else null + center = v if v != null else board_view.get_global_rect().get_center() + match mark_type: + "effect": + var col: Color = Color("7cc47c") if int(row.get("value", 0)) > 0 else Color("c47c7c") + settlement_fx.play_status_pulse(center, col) + "damage": + settlement_fx.play_damage_number(center, "-%d" % int(row.get("value", 0)), + Color("d9402a"), bool(row.get("crit", false))) + "heal": + settlement_fx.play_damage_number(center, "+%d" % int(row.get("value", 0)), Color("7cc47c"), false) + + +# 跨区因果演出(§Transitions 结算逐行兑现——动画密度终稿归战斗表现样张批): +# 伤害行 → ⚔ 弹道自棋盘飞向目标敌方站桩(crit 金档加重);治疗行 → ✚ 弹道飞向主角站桩; +# 命中反馈(闪白/血条递减/灰化)由站桩侧 on_step_row_fx/set_data 承接 +func _play_cross_zone_fx(row: Dictionary) -> void: + var from_v: Vector2 = board_view.get_global_rect().get_center() + match String(row.get("type", "")): + "damage": + var target := String(row.get("target", "")) + var crit := bool(row.get("crit", false)) + if target == "" or target == "hero": + # 主角受击弹道(迭代 6 T5 拆「敌→主角零反馈」早退):⚔ 危险红飞向主角站桩; + # crit 只加重字号不加金(金=收益/胜利语义位,不用于敌方对主角输出) + settlement_fx.play_projectile(from_v, stage.hero_standee_center(), "⚔", + UiKit.RED_MINE, crit) + return + var to_v = stage.enemy_standee_center(target) + if to_v != null: + settlement_fx.play_projectile(from_v, to_v, "⚔", + UiKit.GOLD if crit else UiKit.RED_MINE, crit) + "heal": + if String(row.get("target", "")) != "hero" and String(row.get("target", "")) != "": + return + settlement_fx.play_projectile(from_v, stage.hero_standee_center(), "✚", Color("7cc47c")) diff --git a/game/presentation/row_pacer.gd.uid b/game/presentation/row_pacer.gd.uid new file mode 100644 index 00000000..ced55290 --- /dev/null +++ b/game/presentation/row_pacer.gd.uid @@ -0,0 +1 @@ +uid://68efaexi7rfr diff --git a/game/presentation/views/confirm_dialog.gd b/game/presentation/views/confirm_dialog.gd index e275925c..68e72138 100644 --- a/game/presentation/views/confirm_dialog.gd +++ b/game/presentation/views/confirm_dialog.gd @@ -52,12 +52,9 @@ func open(text: String, confirm_label: String, cancel_label: String = "取消") _text.text = text _confirm_btn.text = confirm_label _cancel_btn.text = cancel_label - # z 序自提(三轮修改批 #1——用户实机实证:本弹窗构建序早于开始覆盖层,而开始层是组合根 - # 尾部才 add_child 的(树序最末=绘制最上),弹窗被整层盖住——「看不见弹窗/离开菜单才显形」。 - # 每次打开强制提到宿主子级最末,绘制序恒最上。) - var host := get_parent() - if host != null: - host.move_child(self, host.get_child_count() - 1) + # z 序纪律(T3 收口批 2026-10-02):树序由组合根构建序保证——本弹窗在 main._build_ui 尾部 + # add_child(开始层/过场层/诊断钮之后 = 子级最末,登记序 = 树序 = 绘制序恒最上), + # 「每次打开 move_child 运行时自提」创可贴随之删除。 visible = true From 315e46038c8aad04d04412f5af3534585d91dbc3 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 08:46:43 +0800 Subject: [PATCH 5/8] =?UTF-8?q?T6=20=E6=8E=A2=E9=92=88=E5=9F=BA=E5=BB=BA?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=E6=AE=B5:=20=E4=B8=89=E6=97=8F=E6=A0=B7?= =?UTF-8?q?=E6=9D=BF=E6=94=B6=E5=8D=95=E7=82=B9=EF=BC=88probe=5Fui=5Futil?= =?UTF-8?q?=E6=A0=91=E5=AF=BB=E5=9D=80contains/begins=5Fwith=E5=8F=82?= =?UTF-8?q?=E6=95=B0=E5=8C=96+isolate=5Fsave=E4=B8=83=E4=BB=BD=E9=9A=94?= =?UTF-8?q?=E7=A6=BB=E6=A0=B7=E6=9D=BF/battle=5Fdriver=E4=BA=94=E4=BB=BD?= =?UTF-8?q?=E6=88=98=E6=96=97=E9=A9=B1=E5=8A=A8=EF=BC=89+level=5Fautoplay?= =?UTF-8?q?=E5=8F=8C=E6=88=98=E6=96=97=E5=BE=AA=E7=8E=AF=E5=90=88=E5=B9=B6?= =?UTF-8?q?=5Fdecide=5Fact+test=5Fkit=E6=94=B613=E5=A5=97=E4=BB=B6=5Fcheck?= =?UTF-8?q?=E6=A0=B7=E6=9D=BF=EF=BC=88=E6=96=AD=E8=A8=80=E6=95=B0=E9=80=90?= =?UTF-8?q?=E5=A5=97=E4=BB=B6=E9=9B=B6=E4=B8=A2=E5=A4=B1=E5=AF=B9=E7=85=A7?= =?UTF-8?q?=E8=A1=A8=E5=9C=A8=E6=8A=A5=E5=91=8A=EF=BC=89+BAD=5FDIR?= =?UTF-8?q?=E5=A4=B9=E5=85=B7=E5=85=B1=E4=BA=AB+smoke=5Fall=E4=BA=94?= =?UTF-8?q?=E4=BB=B6=EF=BC=88:953=E5=85=AC=E6=9C=89=E9=9D=A2clamped=5Fpool?= =?UTF-8?q?/=5Fbattle=5Fst=E4=B8=83=E8=A2=8B/check=5Fsilent=E5=B9=B6?= =?UTF-8?q?=E5=85=A5quiet=E5=8F=82/=5Fpct=E8=BD=AC=E5=8F=91=E5=88=A0/load?= =?UTF-8?q?=E6=94=B9preload=EF=BC=89=EF=BC=9B=E6=8E=A2=E9=92=88=E5=B0=8F?= =?UTF-8?q?=E4=BB=B6=3Dmain=E5=8F=A5=E6=9F=84Node=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=B3=A8/=E9=AD=94=E6=B3=95=E7=AD=89=E5=BE=85=E5=B8=B8?= =?UTF-8?q?=E9=87=8F=E5=8C=96/=E7=94=BB=E5=B8=83=E5=B8=B8=E6=95=B0?= =?UTF-8?q?=E5=90=8C=E6=BA=90=E9=94=9A=E6=B3=A8/=E6=AD=BB=E5=8F=82verbose?= =?UTF-8?q?=E5=88=A0=EF=BC=9B=E7=BA=A2=E7=BA=BF=E5=AE=88=E4=BD=8F=3Dui=5Fi?= =?UTF-8?q?nput=20case=E2=91=A3b=E4=B8=8Eclick=E5=BE=AA=E7=8E=AF=E4=BD=93?= =?UTF-8?q?=E9=9B=B6=E8=A7=A6=E7=A2=B0/=E6=8B=8D=E6=9D=BF=E7=82=B9?= =?UTF-8?q?=E4=B8=89=E4=BB=B6=E6=9C=AA=E9=80=80=E5=BD=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/smoke/battle_driver.gd | 37 ++++++ game/smoke/battle_driver.gd.uid | 1 + game/smoke/damage_probe.gd | 42 ++----- game/smoke/dump_battle.gd | 18 +-- game/smoke/fx_shot_probe.gd | 40 +++---- game/smoke/level_autoplay.gd | 67 +++-------- game/smoke/monte_carlo.gd | 24 +--- game/smoke/probe_ui_util.gd | 112 ++++++++++++++++++ game/smoke/probe_ui_util.gd.uid | 1 + game/smoke/shot_probe.gd | 85 +++---------- game/smoke/skill_rhythm_probe.gd | 19 +-- game/smoke/smoke_all.gd | 40 ++++--- game/smoke/ui_click_probe.gd | 81 ++++--------- game/smoke/ui_input_probe.gd | 19 ++- game/smoke/ui_layout_probe.gd | 17 +-- game/smoke/ui_probe.gd | 34 ++---- game/smoke/ui_tune_probe.gd | 20 ++-- .../damage_controller_test.gd | 88 ++++++-------- .../battle_settlement/damage_math_test.gd | 61 ++++------ .../unit/contracts/system_contracts_test.gd | 50 +++----- game/tests/unit/core/aux_items_test.gd | 42 +++---- .../tests/unit/core/breakpoint_resume_test.gd | 96 +++++++-------- .../data_config/aux_target_violation_test.gd | 19 +-- game/tests/unit/data_config/config_db_test.gd | 72 +++++------ .../data_config/reward_dangling_aux_test.gd | 18 +-- .../unit/data_config/rng_service_test.gd | 32 ++--- game/tests/unit/example_test.gd | 21 +--- .../unit/presentation/reward_overlay_test.gd | 48 ++++---- .../presentation/terrain_tile_sets_test.gd | 44 +++---- .../unit/presentation/ui_font_asset_test.gd | 30 ++--- game/tests/unit/test_kit.gd | 72 +++++++++++ game/tests/unit/test_kit.gd.uid | 1 + 32 files changed, 587 insertions(+), 764 deletions(-) create mode 100644 game/smoke/battle_driver.gd create mode 100644 game/smoke/battle_driver.gd.uid create mode 100644 game/smoke/probe_ui_util.gd create mode 100644 game/smoke/probe_ui_util.gd.uid create mode 100644 game/tests/unit/test_kit.gd create mode 100644 game/tests/unit/test_kit.gd.uid diff --git a/game/smoke/battle_driver.gd b/game/smoke/battle_driver.gd new file mode 100644 index 00000000..250098c0 --- /dev/null +++ b/game/smoke/battle_driver.gd @@ -0,0 +1,37 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 战斗驱动循环单点(T6 样板收口批 2026-10-02)——smoke 族探针共用的「挖掘至停手」整轮驱动。 +# 策略 = AutoPlayer.choose_dig(O4 单一实现):挖最近可挖格(异格先移位)/ 无可挖或相位离 Active 停手。 +# 语义锚(monte_carlo 原型逐位保持):guard 400 步防挂死;移位后相位/轮次离场即 break; +# 轮末仍 Active 且未换轮 → settle_batch stop。smoke_all._run_battle/_auto_play_round 为冒烟本体 +# 不经本面(含回归断言——2026-10-02 修复批加,禁并入);fx_shot_probe 为带墙钟抓帧的异步变体亦不在列。 +extends RefCounted + +const AutoPlayer := preload("res://smoke/auto_player.gd") + +const GUARD_ACTIONS := 400 # 单轮动作步防挂死上限(被收口探针原值逐一同值) + + +# 驱动一整轮挖掘。still_running = 调用方战斗活性谓词(缺省恒真): +# monte_carlo / skill_rhythm 传 `func(): return out.result == ""`(battle_finished 已发的轮内不再动手), +# damage_probe 关卡驱动传 runner 绑定 + battle_result 谓词,battle 级探针传空即可。 +static func drive_round(settlement, still_running: Callable = Callable()) -> void: + var start_round: int = settlement.round_index + var minesweeper = settlement.minesweeper + var guard := GUARD_ACTIONS + while _alive(still_running) and settlement.action_economy.phase == "Active" \ + and settlement.round_index == start_round and guard > 0: + guard -= 1 + var plan := AutoPlayer.choose_dig(minesweeper) + if plan.is_empty(): + break + if plan.hero_cell != minesweeper.hero_pos(): + minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) + if settlement.action_economy.phase != "Active" or settlement.round_index != start_round: + break + minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) + if _alive(still_running) and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: + minesweeper.settle_batch([{"action": "stop"}]) + + +static func _alive(still_running: Callable) -> bool: + return still_running.is_null() or bool(still_running.call()) diff --git a/game/smoke/battle_driver.gd.uid b/game/smoke/battle_driver.gd.uid new file mode 100644 index 00000000..a40674d7 --- /dev/null +++ b/game/smoke/battle_driver.gd.uid @@ -0,0 +1 @@ +uid://dbattledrv13r diff --git a/game/smoke/damage_probe.gd b/game/smoke/damage_probe.gd index 6d86aa07..8bf58a11 100644 --- a/game/smoke/damage_probe.gd +++ b/game/smoke/damage_probe.gd @@ -12,7 +12,7 @@ # Godot_console.exe --headless --path . --script res://smoke/damage_probe.gd -- level 42 extends SceneTree -const AutoPlayer := preload("res://smoke/auto_player.gd") +const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现) var _checks := 0 var _fails := 0 @@ -67,7 +67,7 @@ func _probe_boss(db, seed_val: int) -> void: print("--- Run A:fresh 池(零挖掘停手轮——无授予,攻击链裸验)---") var run_a := _run_boss(db, seed_val, false) - _print_rounds(db, math, run_a, true) + _print_rounds(db, math, run_a) var a_ok := _assert_rounds(db, math, run_a, true) check(a_ok.fresh_and_full, "A1 攻击链健康(fresh 池·掷点口径):非蓄力拍 d_tick = max(下限, 掷点档位基线+附加)(×%d 拍)——H2 攻击链缺陷否证" % a_ok.n_attack) check(a_ok.arith_ok, "A2 池算术完整:全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % a_ok.n_attack) @@ -77,7 +77,7 @@ func _probe_boss(db, seed_val: int) -> void: print("--- Run B:自动挖掘(授予照常发生——当前数据承伤行为实测)---") var run_b := _run_boss(db, seed_val, true) - _print_rounds(db, math, run_b, true) + _print_rounds(db, math, run_b) var b_ok := _assert_rounds(db, math, run_b, false) check(b_ok.arith_ok, "B1 池算术完整(含授予轮):全部非蓄力拍 d_tick == 掷点口径期望(×%d 拍)" % b_ok.n_attack) check(b_ok.wall_tick_positive, "B2 负池轮不归零(伤害下限):hero_def<0 的轮 d_tick 恒 ≥ damage_floor(×%d 轮,最深实测池 %d)" % [b_ok.n_wall, b_ok.deepest]) @@ -98,28 +98,16 @@ func _run_boss(db, seed_val: int, dig: bool) -> Dictionary: var rounds := 0 while settlement.battle_result == "" and rounds < 40: rounds += 1 - var start_round: int = settlement.round_index if dig: - var minesweeper = settlement.minesweeper - var guard := 400 - while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0: - guard -= 1 - var plan := AutoPlayer.choose_dig(minesweeper) - if plan.is_empty(): - break - if plan.hero_cell != minesweeper.hero_pos(): - minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) - if settlement.action_economy.phase == "Active": - minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if settlement.action_economy.phase == "Active" and settlement.round_index == start_round: - minesweeper.settle_batch([{"action": "stop"}]) + BattleDriver.drive_round(settlement) else: settlement.minesweeper.settle_batch([{"action": "stop"}]) return cap # 逐轮:授予行汇总 → 重建战斗段时点池值(effect_pool_map 表驱动归属)→ 敌行动行逐行打印 -func _print_rounds(db, math, run: Dictionary, verbose: bool) -> void: +#(T6 收口批:原形参 verbose 为死参——打印行为无档位差异,随批删除) +func _print_rounds(db, math, run: Dictionary) -> void: var groups := _round_groups(run.rows) var beats := _played_beats(run, db) var rounds: Array = groups.keys() @@ -356,22 +344,8 @@ func _drive_battle(runner, settlement, cap: Dictionary) -> void: var round_guard := 60 while cap["result"] == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0: round_guard -= 1 - var start_round: int = settlement.round_index - var minesweeper = settlement.minesweeper - var action_guard := 400 - while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \ - and settlement.round_index == start_round and action_guard > 0: - action_guard -= 1 - var plan := AutoPlayer.choose_dig(minesweeper) - if plan.is_empty(): - break - if plan.hero_cell != minesweeper.hero_pos(): - minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) - if settlement.action_economy.phase != "Active" or settlement.round_index != start_round: - break - minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: - minesweeper.settle_batch([{"action": "stop"}]) + BattleDriver.drive_round(settlement, func() -> bool: + return runner.current_battle() == settlement and settlement.battle_result == "") # 战斗判定摘要:round-1 victory 的战斗必须能从结算行分辨「首轮全灭」vs「空场」—— diff --git a/game/smoke/dump_battle.gd b/game/smoke/dump_battle.gd index ad352a5b..b411ef41 100644 --- a/game/smoke/dump_battle.gd +++ b/game/smoke/dump_battle.gd @@ -5,7 +5,7 @@ # 运行:Godot_console.exe --headless --path . --script res://smoke/dump_battle.gd extends SceneTree -const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现 +const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现) func _initialize() -> void: var db = load("res://core/config_db.gd").new() @@ -28,21 +28,7 @@ func _initialize() -> void: var rounds := 0 while settlement.battle_result == "" and rounds < 40: rounds += 1 - var start_round: int = settlement.round_index - var minesweeper = settlement.minesweeper - var action_economy = settlement.action_economy - var guard := 400 - while action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0: - guard -= 1 - var plan := AutoPlayer.choose_dig(minesweeper) - if plan.is_empty(): - break - if plan.hero_cell != minesweeper.hero_pos(): - minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) - if action_economy.phase == "Active": - minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if action_economy.phase == "Active" and settlement.round_index == start_round: - minesweeper.settle_batch([{"action": "stop"}]) + BattleDriver.drive_round(settlement) # T6 收口:原内联循环语义同型(移动期免 AP——移动不换轮,移位后重查 break 分支为防御性等价) print("战斗结果: %s(%d 轮)" % [settlement.battle_result, rounds]) print("=== 结算步骤流(%d 行)===" % stream.size()) for row in stream: diff --git a/game/smoke/fx_shot_probe.gd b/game/smoke/fx_shot_probe.gd index b9ce1ef9..4c346b91 100644 --- a/game/smoke/fx_shot_probe.gd +++ b/game/smoke/fx_shot_probe.gd @@ -6,7 +6,16 @@ # 渲染在(本探针 PNG)」双证的第二半边。PNG 落输出目录(artwork/qa-fx-*/,素材纪律不入 git)。 extends Node -var main +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点 + +# ── 演出采样窗口口径(拍板点:headless/窗口化帧率不定,帧时刻以墙钟近似锚定;改窗口 = 改抓帧 +# 时刻观测面,非断言语义。窗口值与原字面量逐一同值——T6 收口批仅提具名。) ── +const T_DIG_FLIGHT := 0.12 # 挖格行到达/弹道在飞窗口(settle_batch 回执后停留) +const T_SETTLE := 0.35 # 结算排播窗口(行到达后二连抓) +const T_ROUND_TICK := 0.25 # 停手轮末(敌方攻击弹道/受击闪白 0.2-1.6s 流过)连抓 6 帧的采样步距 +const T_FINAL := 0.6 # 收尾帧前静置 + +var main: Node var out_dir := "" var _problems: Array = [] @@ -19,17 +28,10 @@ func _ready() -> void: main = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame - for p0 in ["user://fx_shot_probe.json", "user://fx_shot_probe.json.tmp", "user://fx_shot_probe_settings.cfg"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) - var iso_c = load("res://core/save_manager.gd").new("user://fx_shot_probe.json") - iso_c.load_from_disk() - main.save_manager = iso_c - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c) - main.settings_store = load("res://presentation/settings_store.gd").new("user://fx_shot_probe_settings.cfg") + ProbeUiUtil.isolate_save(main, "fx_shot_probe", false, true, false) for i in 8: await get_tree().process_frame # UI 装配完成等足帧(canvas_item 空引用窗口) - var lvbtn: Button = _find_button_by_sub(main._start_overlay, "开始冒险") + var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains") if lvbtn == null: _fail("开始冒险按钮缺失") return @@ -68,9 +70,9 @@ func _ready() -> void: if out.is_empty() or not bool(out[0].get("accepted", false)): continue dug += 1 - await get_tree().create_timer(0.12).timeout + await get_tree().create_timer(T_DIG_FLIGHT).timeout await _snap("fx_dig%d" % dug) # 行到达/弹道在飞窗口 - await get_tree().create_timer(0.35).timeout + await get_tree().create_timer(T_SETTLE).timeout await _snap("fx_settle%d" % dug) # 结算排播窗口 continue # 无相邻可挖 → 走向有未开安全格邻居的已揭开格(挖掘期移动免费) @@ -90,12 +92,12 @@ func _ready() -> void: # 停手 → 轮末结算(敌方攻击弹道/受击闪白/结算排播在 0.2-1.6s 窗口流过)——连抓 6 帧 rules.settle_batch([{"action": "stop"}]) for i in 6: - await get_tree().create_timer(0.25).timeout + await get_tree().create_timer(T_ROUND_TICK).timeout await _snap("fx_round%d" % (i + 1)) if dug == 0: _fail("无安全格可挖(首板全雷不可信)") return - await get_tree().create_timer(0.6).timeout + await get_tree().create_timer(T_FINAL).timeout await _snap("fx_final") if _problems.is_empty(): print("[fx_shot_probe] PASS — 演出帧 %d 张落 %s(挖格 %d)" % [dug * 2 + 1, out_dir, dug]) @@ -115,16 +117,6 @@ func _snap(tag: String) -> void: img.save_png("%s/%s.png" % [out_dir, tag]) -func _find_button_by_sub(root: Node, needle: String) -> Button: - if root is Button and String(root.text).contains(needle): - return root - for c in root.get_children(): - var r := _find_button_by_sub(c, needle) - if r != null: - return r - return null - - func _fail(msg: String) -> void: _problems.append(msg) push_error("[fx_shot_probe] FAIL — " + msg) diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index 501342c8..aca66b9d 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -396,13 +396,17 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void: static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void: - if String(st.get("policy", "normal")) == "perfect": - _autoplay_battle_perfect(runner, settlement, st, logger) - return + # 策略档经 _decide_act 单点分派(T6 样板收口批 2026-10-02——原 _autoplay_battle/_autoplay_battle_perfect + # 双循环逐行相同仅差 act 来源行,合并单循环;三档决策语义逐位不变,见 _decide_act)。 # 普通策略档 v3(2026-10-01 用户重定义):完美策略 + 受控失误注入——v2「新手级单步推理+盲挖 # 兜底」画像被真人实证推翻(零加成真人首通三关踩 23 雷 vs v2 普通档 L2 条件通关率 10.6%)。 # v3 语义 = 会玩的普通玩家:推理与完美档同源(subset 解算/标旗/卡死刷新),失误只注入两手—— # 标雷误挖(NORMAL_MINE_SLIP_P)+ 安全挖跳过(NORMAL_SKIP_DIG_P)。连续拒绝 ≥3 = 编排违约防御。 + # 完美策略档(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交, + # refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板), + # stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。 + # 2026-10-01 上界修正:选路改目标价值评分(_route_pick)+ 商店贪心买齐(_autoplay_shop)—— + # 「完美」须是元游戏意义上的上界(会购物会补给),不再只是扫雷子游戏上界。 var round_guard := 60 while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0: round_guard -= 1 @@ -413,11 +417,7 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \ and settlement.round_index == start_round and action_guard > 0 and rejects < 3: action_guard -= 1 - var act: Dictionary - if bool(st.get("adv", false)): - act = _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误 - else: - act = AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持 + var act: Dictionary = _decide_act(st, settlement) var kind := String(act.get("kind", "stop")) if kind == "stop": break @@ -487,47 +487,16 @@ static func _gamble_action(plan: Dictionary, settlement) -> Dictionary: return {"kind": "move", "cell": plan.hero_cell} -# 完美策略档战斗驱动(2026-09-14):单步决策循环——move/dig/flag 经 排雷玩法 settle_batch 提交, -# refresh 经 settlement.player_request_refresh(战斗结算 核心编排:行动经济 扣 RAP + 退役 + 当场换新板), -# stop 出内环走统一停手提交。连续拒绝 ≥3 = 编排违约防御(fail-fast 语义,停手防死循环)。 -# 2026-10-01 上界修正:选路改目标价值评分(_route_pick)+ 商店贪心买齐(_autoplay_shop)—— -# 「完美」须是元游戏意义上的上界(会购物会补给),不再只是扫雷子游戏上界。 -static func _autoplay_battle_perfect(runner, settlement, st: Dictionary, logger) -> void: - var round_guard := 60 - while st.result == "" and runner.current_battle() == settlement and settlement.battle_result == "" and round_guard > 0: - round_guard -= 1 - st.rounds += 1 - var start_round: int = settlement.round_index - var action_guard := 400 - var rejects := 0 - while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \ - and settlement.round_index == start_round and action_guard > 0 and rejects < 3: - action_guard -= 1 - var act := AutoPlayer.perfect_action(settlement.minesweeper, settlement) - var kind := String(act.get("kind", "stop")) - if kind == "stop": - break - var ok := false - if kind == "move": - ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round) - elif kind == "dig": - ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round) - elif kind == "flag": - ok = _submit_intent(settlement, "flag_place", act.get("cell"), logger, start_round) - elif kind == "refresh": - ok = bool(settlement.player_request_refresh()) - if logger != null: - logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round}) - if ok: - if kind == "refresh": - st.refreshes = int(st.refreshes) + 1 - rejects = 0 - else: - rejects += 1 - if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round: - var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) - if logger != null: - logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round}) +# 战斗决策单行分派(T6 双循环合并抽取——原两循环唯一差异行): +# perfect 档 = 完美策略单步决策(上界口径); +# normal 档 v3 = 完美+受控失误注入(冒险测量面 adv=true);单关机制夹具保持 v2 新手画像(adv 缺省)。 +# 决策函数纯读板面/编排面(零 RNG 消费——失误流独走 st.prng,见 _apply_normal_slips)。 +static func _decide_act(st: Dictionary, settlement) -> Dictionary: + if String(st.get("policy", "normal")) == "perfect": + return AutoPlayer.perfect_action(settlement.minesweeper, settlement) + if bool(st.get("adv", false)): + return _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误 + return AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持 static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool: diff --git a/game/smoke/monte_carlo.gd b/game/smoke/monte_carlo.gd index 41c9eeef..e7da2121 100644 --- a/game/smoke/monte_carlo.gd +++ b/game/smoke/monte_carlo.gd @@ -5,7 +5,7 @@ # 运行:Godot_console.exe --headless --path . --script res://smoke/monte_carlo.gd extends SceneTree -const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口(批注 2026-09-15:原文件缺失 preload——HEAD 起即解析失败,本批顺手修复) +const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现) const N := 30 const SEED0 := 42 @@ -43,7 +43,7 @@ func _run_encounter(db, L: int) -> void: else: timeouts += 1 print("=== L%d · %d 种子(败 %d / 超时 %d)===" % [L, N, losses, timeouts]) - print(" 胜利轮数 均值 %.2f 中位 %d p10 %.1f p90 %.1f 分布 %s" % [_mean(rounds_v), _median(rounds_v), _pct(rounds_v, 0.1), _pct(rounds_v, 0.9), _hist(rounds_v)]) + print(" 胜利轮数 均值 %.2f 中位 %d p10 %.1f p90 %.1f 分布 %s" % [_mean(rounds_v), _median(rounds_v), _pct_f(rounds_v, 0.1), _pct_f(rounds_v, 0.9), _hist(rounds_v)]) print(" 每轮承伤 均值 %.2f 中位 %.1f p90 %.1f" % [_mean(dmg_per_round), _median_f(dmg_per_round), _pct_f(dmg_per_round, 0.9)]) print(" 整场承伤 均值 %.1f 中位 %.0f(主角 max_hp=%d)" % [_mean(dmg_total), _median_f(dmg_total), int(db.hero.max_hp)]) print(" 开安全格 均值 %.1f 中位 %d" % [_mean(safe_opened), _median(safe_opened)]) @@ -72,21 +72,7 @@ func _run_battle(db, seed_val: int, L: int) -> Dictionary: while out.result == "" and guard_rounds > 0: guard_rounds -= 1 out.rounds += 1 - var start_round: int = settlement.round_index - var minesweeper = settlement.minesweeper - var guard := 400 - while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "": - guard -= 1 - var plan := AutoPlayer.choose_dig(minesweeper) - if plan.is_empty(): - break - if plan.hero_cell != minesweeper.hero_pos(): - minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) - if settlement.action_economy.phase != "Active" or settlement.round_index != start_round: - break - minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "": - minesweeper.settle_batch([{"action": "stop"}]) + BattleDriver.drive_round(settlement, func() -> bool: return out.result == "") if out.result == "": out.result = "timeout" return out @@ -114,10 +100,6 @@ func _median_f(a: Array) -> float: return float(b[b.size() / 2]) -func _pct(a: Array, p: float) -> float: - return _pct_f(a, p) - - func _pct_f(a: Array, p: float) -> float: if a.is_empty(): return 0.0 diff --git a/game/smoke/probe_ui_util.gd b/game/smoke/probe_ui_util.gd new file mode 100644 index 00000000..fbcc6ce1 --- /dev/null +++ b/game/smoke/probe_ui_util.gd @@ -0,0 +1,112 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 探针共用面:树寻址助手 + 存档/设置/元进度隔离样板单点(T6 样板收口批 2026-10-02)。 +# 收口范围:ui_probe / ui_click_probe / shot_probe / fx_shot_probe 四探针的 7 处树寻址样板 +# 与 7 处隔离样板(各探针隔离文件名后缀 tag 保持不变)。 +# 用法:探针侧 `const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd")` 后静态调用 +#(探针载体含 --script 场景两种,均无全局标识符——禁 class_name 裸引用)。 +extends RefCounted + +const SaveManagerScript := preload("res://core/save_manager.gd") +const MetaProgressionScript := preload("res://core/meta_progression.gd") +const SettingsStoreScript := preload("res://presentation/settings_store.gd") + +# ── 树寻址(递归只读——按钮/标签句柄未存组合根成员的探针专用面) ────────────── +# 匹配模式参数化(历史两种语义,禁混用禁静默改变): +# mode="prefix" = text.begins_with(needle)(ui_probe / ui_click_probe 历史语义) +# mode="contains" = text.contains(needle)(shot_probe / fx_shot_probe 历史语义) +# 调用点替换时逐一核对原语义后选参。 + +static func find_button(root: Node, needle: String, mode: String = "prefix") -> Button: + if root == null: + return null + if root is Button and _text_match(String(root.text), needle, mode): + return root + for child in root.get_children(): + var found := find_button(child, needle, mode) + if found != null: + return found + return null + + +# CheckButton 同款寻址(ui_click_probe 历史语义 = contains——设置面旧入口退役断言用) +static func find_check_button(root: Node, needle: String) -> CheckButton: + if root == null: + return null + if root is CheckButton and String(root.text).contains(needle): + return root + for child in root.get_children(): + var found := find_check_button(child, needle) + if found != null: + return found + return null + + +# Label 按文案包含寻址(shot_probe GP 徽章读数锚) +static func find_label(root: Node, text_fragment: String) -> Label: + if root == null: + return null + if root is Label and String(root.text).contains(text_fragment): + return root + for child in root.get_children(): + var found := find_label(child, text_fragment) + if found != null: + return found + return null + + +# 控件实测几何(shot_probe checker 文字框取样坐标源——禁硬编码像素,分辨率档位自适应) +static func label_rect(root: Node, text_fragment: String) -> Dictionary: + if root == null: + return {} + if root is Label and String(root.text).contains(text_fragment): + return {"x": root.global_position.x, "y": root.global_position.y, + "w": root.size.x, "h": root.size.y} + for child in root.get_children(): + var r := label_rect(child, text_fragment) + if not r.is_empty(): + return r + return {} + + +# 按脚本名寻址(resource_path contains——shot_probe hero 面板锚) +static func find_by_script(root: Node, script_name: String) -> Node: + if root == null: + return null + var s: Script = root.get_script() + if s != null and String(s.resource_path).contains(script_name): + return root + for child in root.get_children(): + var r := find_by_script(child, script_name) + if r != null: + return r + return null + + +static func _text_match(text: String, needle: String, mode: String) -> bool: + return text.begins_with(needle) if mode == "prefix" else text.contains(needle) + + +# ── 存档/设置/元进度三面隔离(探针禁碰真实档 user://save.json——SaveManager 路径注入纪律) ── +# tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。 +# mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注 +#(隔离 cfg 名 = _settings.cfg,与被收口探针原文件名逐一同名);refresh_resume = 组合根 +# 「继续上局」按钮重刷;swap_save = 是否换注 main.save_manager(营地先行段只换 meta 不换 save_manager)。 +# 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。 +static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false, + refresh_resume: bool = false, swap_save: bool = true): + var save_path := "user://%s.json" % tag + for p in [save_path, save_path + ".tmp", "user://%s_settings.cfg" % tag]: + if FileAccess.file_exists(p): + DirAccess.remove_absolute(p) + var iso = SaveManagerScript.new(save_path) + iso.load_from_disk() + if mark_first_done: + iso.mark_first_adventure_done() + if swap_save: + main.save_manager = iso + main.meta = MetaProgressionScript.new(main.db, iso) + if with_settings: + main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) + if refresh_resume: + main._refresh_resume_button() + return iso diff --git a/game/smoke/probe_ui_util.gd.uid b/game/smoke/probe_ui_util.gd.uid new file mode 100644 index 00000000..4e4f43f1 --- /dev/null +++ b/game/smoke/probe_ui_util.gd.uid @@ -0,0 +1 @@ +uid://c1probeuiutil diff --git a/game/smoke/shot_probe.gd b/game/smoke/shot_probe.gd index edda5c6f..fb1b48a0 100644 --- a/game/smoke/shot_probe.gd +++ b/game/smoke/shot_probe.gd @@ -9,7 +9,9 @@ # 最小自检 = 四件存在且非零字节(像素方差断言归 tools/shot_checker.py——B 档骨架分工)。 extends Node -var main +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点 + +var main: Node var out_dir := "" var _problems: Array = [] var _meta := {} # 控件实测几何(全局 rect——shot_checker 文字框/明度带取样坐标源,禁硬编码像素) @@ -23,36 +25,28 @@ func _ready() -> void: main = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame - # ── 存档/元进度/设置三面隔离(ui_click_probe 同款——真档零污染) ── - for p0 in ["user://smoke_shot_probe.json", "user://smoke_shot_probe.json.tmp", - "user://smoke_shot_probe_settings.cfg"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) - var iso_c = load("res://core/save_manager.gd").new("user://smoke_shot_probe.json") - iso_c.load_from_disk() - main.save_manager = iso_c - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c) - main.settings_store = load("res://presentation/settings_store.gd").new("user://smoke_shot_probe_settings.cfg") + # ── 存档/元进度/设置三面隔离(ui_click_probe 同款——真档零污染;历史语义 = contains 寻址) ── + ProbeUiUtil.isolate_save(main, "smoke_shot_probe", false, true, false) await get_tree().process_frame # ① 菜单(启动态) - _meta["menu_sub"] = _find_label_rect(main._start_overlay, "改造批") - _meta["menu_help"] = _find_label_rect(main._start_overlay, "玩法:") - _meta["menu_title"] = _find_label_rect(main._start_overlay, "Dungeon Sweeper") + _meta["menu_sub"] = ProbeUiUtil.label_rect(main._start_overlay, "改造批") + _meta["menu_help"] = ProbeUiUtil.label_rect(main._start_overlay, "玩法:") + _meta["menu_title"] = ProbeUiUtil.label_rect(main._start_overlay, "Dungeon Sweeper") await _snap("menu") # ② 营地 - var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地") + var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "contains") if campbtn == null: _problems.append("营地入口缺失(开始层 campbtn)") else: campbtn.pressed.emit() await get_tree().process_frame await _snap("camp") - var close_btn: Button = _find_button_by_prefix(main.camp_overlay, "关闭营地") + var close_btn: Button = ProbeUiUtil.find_button(main.camp_overlay, "关闭营地", "contains") if close_btn != null: close_btn.pressed.emit() await get_tree().process_frame # ③ 关卡画面(开始冒险→停点出图——冒险开始即出图,停点前移批语义) - var lvbtn: Button = _find_button_by_prefix(main._start_overlay, "开始冒险") + var lvbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "开始冒险", "contains") if lvbtn == null: _problems.append("开始冒险按钮缺失(开始层 lvbtn)") else: @@ -61,7 +55,7 @@ func _ready() -> void: await get_tree().process_frame await _snap("level") # GP 常驻徽章断言(迭代 9 卡⑪核验——读数=runner.gp 现值,刷新=写路径回执后重查〔open_choices〕) - var gp_label: Label = _find_label_by_text(main.level_screen, "◈ ") + var gp_label: Label = ProbeUiUtil.find_label(main.level_screen, "◈ ") if gp_label == null or not gp_label.is_visible_in_tree(): _problems.append("GP 徽章不可见(关卡画面顶栏)") elif main.level_screen.get("_runner") != null: @@ -75,8 +69,8 @@ func _ready() -> void: await get_tree().process_frame await _snap("battle") # 战斗屏文字框/明度带锚点(全局 rect——checker 取样坐标源) - _meta["battle_log_hint"] = _find_label_rect(main.stage, "结算明细") - _meta["hero_panel"] = _node_rect(_find_by_script(main, "hero_info_panel_view")) + _meta["battle_log_hint"] = ProbeUiUtil.label_rect(main.stage, "结算明细") + _meta["hero_panel"] = _node_rect(ProbeUiUtil.find_by_script(main, "hero_info_panel_view")) _meta["stage_view"] = _node_rect(main.stage) _meta["board_zone"] = _node_rect(main.board_zone) # draw calls 真机测量(迭代 9 卡⑦——基线 270→目标 <200;窗口化渲染帧读数, @@ -116,59 +110,8 @@ func _snap(key: String) -> void: print("[shot_probe] saved %s (%dx%d)" % [key, img.get_width(), img.get_height()]) -func _find_button_by_prefix(root: Node, prefix: String) -> Button: - if root == null: - return null - if root is Button and String(root.text).contains(prefix): - return root - for c in root.get_children(): - var r := _find_button_by_prefix(c, prefix) - if r != null: - return r - return null - - -# 控件实测几何采集(checker 断言坐标源——禁硬编码像素,分辨率档位自适应) -func _find_label_rect(root: Node, text_fragment: String) -> Dictionary: - if root == null: - return {} - if root is Label and String(root.text).contains(text_fragment): - return {"x": root.global_position.x, "y": root.global_position.y, - "w": root.size.x, "h": root.size.y} - for c in root.get_children(): - var r := _find_label_rect(c, text_fragment) - if not r.is_empty(): - return r - return {} - - -func _find_label_by_text(root: Node, text_fragment: String) -> Label: - if root == null: - return null - if root is Label and String(root.text).contains(text_fragment): - return root - for c in root.get_children(): - var r := _find_label_by_text(c, text_fragment) - if r != null: - return r - return null - - func _node_rect(n: Node) -> Dictionary: if n == null or not (n is CanvasItem): return {} return {"x": n.global_position.x, "y": n.global_position.y, "w": n.size.x, "h": n.size.y} - - -func _find_by_script(root: Node, script_name: String) -> Node: - if root == null: - return null - var s: Script = root.get_script() - if s != null and String(s.resource_path).contains(script_name): - return root - for c in root.get_children(): - var r := _find_by_script(c, script_name) - if r != null: - return r - return null diff --git a/game/smoke/skill_rhythm_probe.gd b/game/smoke/skill_rhythm_probe.gd index f607d1d1..fab457c1 100644 --- a/game/smoke/skill_rhythm_probe.gd +++ b/game/smoke/skill_rhythm_probe.gd @@ -7,6 +7,8 @@ # 运行:Godot_console.exe --headless --path . --script res://smoke/skill_rhythm_probe.gd extends SceneTree +const BattleDriver := preload("res://smoke/battle_driver.gd") # T6 收口:挖掘驱动循环单点(策略仍归 AutoPlayer 单一实现) + func _initialize() -> void: var db = load("res://core/config_db.gd").new() db.load_tables("res://data/tables", false) @@ -50,23 +52,8 @@ func _run_battle(db, seed_val: int, L: int, depth: int, casts: Dictionary) -> Di casts[sid] = casts.get(sid, 0) + 1) if not settlement.begin_battle({"level_index": L, "node_depth": depth, "hero_hp": int(db.hero.max_hp)}): return out - var AutoPlayer = load("res://smoke/auto_player.gd") var guard_rounds := 40 while out.result == "" and guard_rounds > 0: guard_rounds -= 1 - var start_round: int = settlement.round_index - var minesweeper = settlement.minesweeper - var guard := 400 - while settlement.action_economy.phase == "Active" and settlement.round_index == start_round and guard > 0 and out.result == "": - guard -= 1 - var plan = AutoPlayer.choose_dig(minesweeper) - if plan.is_empty(): - break - if plan.hero_cell != minesweeper.hero_pos(): - minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}]) - if settlement.action_economy.phase != "Active" or settlement.round_index != start_round: - break - minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if settlement.action_economy.phase == "Active" and settlement.round_index == start_round and out.result == "": - minesweeper.settle_batch([{"action": "stop"}]) + BattleDriver.drive_round(settlement, func() -> bool: return out.result == "") return out diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index a45b5e7e..46181d99 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -6,15 +6,20 @@ extends SceneTree const AutoPlayer := preload("res://smoke/auto_player.gd") # O4 收口:自动玩家策略单一实现 +const ConfigDBScript := preload("res://core/config_db.gd") # T6 收口:表宿主 preload 常量(原 _initialize 运行期 load) var _failures := 0 var _checks := 0 -func check(cond: bool, label: String) -> void: +func check(cond: bool, label: String, quiet: bool = false) -> void: _checks += 1 if cond: - print(" [PASS] " + label) + if not quiet: + print(" [PASS] " + label) + elif quiet: + _failures += 1 + print(" [FAIL] (silent check)") # 原 check_silent 输出逐位保持 else: _failures += 1 print(" [FAIL] " + label) @@ -22,7 +27,7 @@ func check(cond: bool, label: String) -> void: func _initialize() -> void: print("=== Dungeon Sweeper 垂直切片冒烟 ===") - var db = load("res://core/config_db.gd").new() + var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) check(load_ok, "全部表加载 + 校验通过(含跨表护栏)") if not load_ok: @@ -238,7 +243,7 @@ func _smoke_adventure_api() -> void: # 驱动首战到胜利 → 选路态(非战斗期)→ abandon 受理 var autoplay = load("res://smoke/level_autoplay.gd") var battle = adr.runner().current_battle() - var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"} + var st := _battle_st("normal") var guard := 30 while adr.runner().current_battle() == battle and guard > 0: guard -= 1 @@ -950,7 +955,8 @@ func _weaken_refresh_probe() -> Dictionary: settlement._status_effects.apply_effect("weaken", src) # 续时:remaining ← 2,层数不动 for eff in settlement._status_effects.query_active("enemy"): if String(eff.effect_id) == "weaken": - return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._clamped_pool("enemy_output"))} + # T6 收口批:改走伤害控制器公有算术面 clamped_pool(原编排器 _clamped_pool 薄委托的直呼退役) + return {"layers": int(eff.layers), "remaining": int(eff.remaining_settlements), "pool": int(settlement._damage.clamped_pool("enemy_output"))} return {} @@ -1760,7 +1766,7 @@ func _smoke_encounter_roll() -> void: var rng = load("res://core/rng_service.gd").new() rng.set_master_seed(30000 + i) var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng) - check_silent(settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)})) + check(settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.hero.max_hp)}), "遭遇扫描战斗启动(quiet 计数档——计数入总账,成败由后续 spawn 断言归因)", true) counts.append(settlement.presentation_snapshot().enemies.size()) # 区间生效:20 个种子应出现 1 和 2(D0 [1,2]) check(counts.has(1) and counts.has(2), "D0 区间 [1,2] 两值均出现(分布 %s)" % str(counts)) @@ -2417,11 +2423,11 @@ func _has_effect_id(rows: Array, effect_id: String) -> bool: return true return false -func check_silent(cond: bool) -> void: - _checks += 1 - if not cond: - _failures += 1 - print(" [FAIL] (silent check)") + +# 手搓战斗状态袋单点(T6 收口批——7 处字面量收编;_autoplay_battle 直接消费的最小字段族。 +# 大字段族同构袋仍走 _adv_st()——_drive_level/_autoplay_battle 全字段消费面) +func _battle_st(policy: String) -> Dictionary: + return {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": policy} # ==================== 敌人技能池批(2026-09-16 用户拍板——开轮摇号出招系统) ==================== @@ -2883,7 +2889,7 @@ func _smoke_level_graph() -> void: var choice_sizes: Array = [] runner.choices_presented.connect(func(c): choice_sizes.append(c.size())) check(runner.start_level(2, 42, {}, 0), "手动驱动:L2 启动(入口战斗直接开打——入口无前置兑付;template_pick=0 固定现行图——精确选路断言)") - var st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"} + var st = _battle_st("normal") var first = runner.current_battle() var res_box := {} # 信号写回容器(lambda 按值捕获局部变量——Dictionary 引用可变) first.battle_finished.connect(func(p): res_box["result"] = String(p.get("result", ""))) @@ -2908,7 +2914,7 @@ func _smoke_level_graph() -> void: for i in range(12): var g_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(9100 + i)) g_runner.start_level(2, 9100 + i) - var g_st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "perfect"} + var g_st = _battle_st("perfect") var g_first = g_runner.current_battle() var g_box := {} g_first.battle_finished.connect(func(p): @@ -2989,7 +2995,7 @@ func _smoke_level_graph() -> void: var g3_runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(9100 + i)) g3_runner.set_hero_override(200, 100) g3_runner.start_level(3, 9100 + i) - var g3_st = {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "perfect"} + var g3_st = _battle_st("perfect") var g3_first = g3_runner.current_battle() var g3_box := {} g3_first.battle_finished.connect(func(p): @@ -3035,7 +3041,7 @@ func _smoke_gp_wallet() -> void: var rows: Array = [] battle.step_row.connect(func(row): rows.append(row)) battle.battle_finished.connect(func(p): fin["p"] = p) - var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"} + var st := _battle_st("normal") var guard := 30 while runner.current_battle() == battle and not fin.has("p") and guard > 0: guard -= 1 @@ -3199,7 +3205,7 @@ func _smoke_shop_room() -> void: var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(s)) runner.set_hero_override(900, 400) # 行进段快速通关档(购买断言与百分比轴正交——加算区单独断言) runner.start_level(2, s, {}, 0) # 固定模板 0——商店经济断言按现行图 GP 账本 - var st := {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"} + var st := _battle_st("normal") var guard := 40 while String(runner.level_result) == "" and int(runner.current_node_id()) != 8 and guard > 0: guard -= 1 @@ -3294,7 +3300,7 @@ func _smoke_shop_room() -> void: var b_end = runner.current_battle() if b_end == null: break - autoplay._autoplay_battle(runner, b_end, {"result": "", "rounds": 0, "refreshes": 0, "mine_hits": 0, "hero_dmg": 0, "policy": "normal"}, null) + autoplay._autoplay_battle(runner, b_end, _battle_st("normal"), null) check(fin_end.has("p") and String(fin_end["p"].get("result", "")) in ["victory", "defeat"], "冒险收束(Boss 战终局:%s)" % String(fin_end.get("p", {}).get("result", ""))) check(runner.gp == 0, "冒险收束钱包清零(局内货币生命周期终点——不入存档不入摘要)") diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 3a7d9584..44a7a535 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -11,8 +11,9 @@ extends Node const AutoPlayer := preload("res://smoke/auto_player.gd") # O4:自动玩家策略单一实现 +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点 -var main +var main: Node var _last_state := {} var _stall_frames := 0 var _done := false @@ -41,14 +42,11 @@ func _ready() -> void: var t_ui_ready := Time.get_ticks_msec() - t0 # ---- 营地购买合成点击路径(元进度实现批——campbtn/购买/关闭全走真实按钮信号路径; # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json);陈旧档段首清理)---- - for p0 in ["user://smoke_click_probe_camp.json", "user://smoke_click_probe_camp.json.tmp", - "user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]: + for p0 in ["user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]: if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) - var iso_c = load("res://core/save_manager.gd").new("user://smoke_click_probe_camp.json") - iso_c.load_from_disk() - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_c) - var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地") + DirAccess.remove_absolute(p0) # 重置档段前清场兜底(营地/设置段不触该档——防陈旧断点泄入后续段) + var iso_c = ProbeUiUtil.isolate_save(main, "smoke_click_probe_camp", false, false, false, false) + var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix") if campbtn == null: _problems.append("营地入口按钮缺失(开始层 campbtn)") else: @@ -90,7 +88,7 @@ func _ready() -> void: iso_s.load_settings() main.settings_store = iso_s main.settings_overlay.retarget(iso_s) - var settings_btn: Button = _find_button_by_prefix(main._start_overlay, "设置") + var settings_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "设置", "prefix") if settings_btn == null: _problems.append("设置入口按钮缺失(开始层 settingsbtn)") else: @@ -100,7 +98,7 @@ func _ready() -> void: _problems.append("设置面未打开(点击路径)") else: # 减动效转正双态(卡④ AC③):开=0.5 引擎态即时生效+落盘持久;关=1.0 回常态 - var motion_btn: Button = _find_button_by_text(main.settings_overlay, "减动效") # 二轮 D2——开关已换 UiKit toggle 按钮形态 + var motion_btn: Button = ProbeUiUtil.find_button(main.settings_overlay, "减动效", "contains") # 二轮 D2——开关已换 UiKit toggle 按钮形态 if motion_btn == null: _problems.append("设置面减动效开关缺失") else: @@ -127,10 +125,10 @@ func _ready() -> void: if absf(iso_s.volume_linear - 0.3) > 0.001: _problems.append("音量未入 store(%f ≠ 0.3)" % iso_s.volume_linear) # 调试入口默认隐藏门控(卡④ AC④——探针环境 DS_DEBUG 未设) - var dbg_btn: Button = _find_button_by_prefix(main._start_overlay, "调试") + var dbg_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "调试", "prefix") if dbg_btn != null and dbg_btn.visible: _problems.append("调试入口未隐藏(DS_DEBUG 门控失守——卡④ AC④)") - var settings_close: Button = _find_button_by_prefix(main.settings_overlay, "返回") + var settings_close: Button = ProbeUiUtil.find_button(main.settings_overlay, "返回", "prefix") if settings_close == null: _problems.append("设置面返回按钮缺失") else: @@ -139,25 +137,17 @@ func _ready() -> void: if main.settings_overlay.visible or not main._start_overlay.visible: _problems.append("设置面关闭后未恢复开始层") # 旧入口退役断言(卡④ AC③——测试调整板不再含减动效开关) - var legacy_motion = _find_checkbutton_by_text(main.tune_panel, "减动效") + var legacy_motion = ProbeUiUtil.find_check_button(main.tune_panel, "减动效") if legacy_motion != null: _problems.append("调整板减动效旧入口未退役(设置面唯一入口失守——卡④ AC③)") # D1 断言(二轮):退出游戏只在一级菜单——开始菜单有、设置面无 - if _find_button_by_prefix(main._start_overlay, "退出游戏") == null: + if ProbeUiUtil.find_button(main._start_overlay, "退出游戏", "prefix") == null: _problems.append("开始菜单缺退出游戏按钮(二轮 D1)") - if _find_button_by_prefix(main.settings_overlay, "退出游戏") != null: + if ProbeUiUtil.find_button(main.settings_overlay, "退出游戏", "prefix") != null: _problems.append("设置面仍含退出游戏按钮(二轮 D1——只在一级菜单出现)") # 换回全新零档(营地购买态不泄入后续冒险段——确定性;档案残留下次段首清理兜底) - for p0 in ["user://smoke_click_probe_reset.json", "user://smoke_click_probe_reset.json.tmp"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退 - var iso_reset = load("res://core/save_manager.gd").new("user://smoke_click_probe_reset.json") - iso_reset.load_from_disk() - iso_reset.mark_first_adventure_done() # 对齐基线(真实档已有游玩史)——非首次脚本模式=逐关抽签模板 - main.save_manager = iso_reset # 迭代 8 卡①——冒险断点写盘同步隔离(禁碰真实档 user://save.json) - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_reset) - main._refresh_resume_button() - var resume_btn0: Button = _find_button_by_prefix(main._start_overlay, "▶ 继续上局") + ProbeUiUtil.isolate_save(main, "smoke_click_probe_reset", true, false, true) # 零档换注 + 继续上局按钮重刷(原三行样板收口) + var resume_btn0: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 继续上局", "prefix") if resume_btn0 == null: _problems.append("继续上局按钮缺失(开始层 resumebtn——装配器)") elif resume_btn0.visible: @@ -227,7 +217,7 @@ func _ready() -> void: _problems.append("大奖励屏打开时输入未锁定") _inter_confirms += 1 if _inter_confirms == 1: - var next_btn: Button = _find_button_by_prefix(main._inter_overlay, "进入下一层") + var next_btn: Button = ProbeUiUtil.find_button(main._inter_overlay, "进入下一层", "prefix") if next_btn == null: _problems.append("大奖励屏确认按钮缺失") else: @@ -309,11 +299,11 @@ func _ready() -> void: await get_tree().process_frame if not main._start_overlay.visible: _problems.append("断点段:终局返回未回开始菜单") - var resume_btn: Button = _find_button_by_prefix(main._start_overlay, "▶ 继续上局") + var resume_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 继续上局", "prefix") if resume_btn != null and resume_btn.visible: _problems.append("断点段:退出清断点后继续上局按钮未隐藏(卡②/T3 AC③)") # ② 中断→续玩:新冒险达入口停点=断点在档 → 合成回菜单(模拟关游戏重开)→ 按钮点亮 → 点击恢复至停点 - var lv_btn: Button = _find_button_by_prefix(main._start_overlay, "▶ 开始冒险") + var lv_btn: Button = ProbeUiUtil.find_button(main._start_overlay, "▶ 开始冒险", "prefix") if lv_btn == null or resume_btn == null: _problems.append("断点段:开始冒险/继续上局按钮缺失") else: @@ -356,7 +346,7 @@ func _ready() -> void: await get_tree().process_frame if not main.confirm_dialog.is_open() or main.adventure != null: _problems.append("断点段:有断点点开始冒险未弹确认框(二轮 A 失守)") - var cancel_btn: Button = _find_button_by_text(main.confirm_dialog, "取消") + var cancel_btn: Button = ProbeUiUtil.find_button(main.confirm_dialog, "取消", "contains") if cancel_btn == null: _problems.append("断点段:确认框取消按钮缺失") else: @@ -366,7 +356,7 @@ func _ready() -> void: _problems.append("断点段:取消未回开始菜单(二轮 A)") lv_btn.pressed.emit() await get_tree().process_frame - var confirm_btn: Button = _find_button_by_text(main.confirm_dialog, "开新冒险") + var confirm_btn: Button = ProbeUiUtil.find_button(main.confirm_dialog, "开新冒险", "contains") if confirm_btn == null: _problems.append("断点段:确认框开新冒险按钮缺失") else: @@ -561,34 +551,3 @@ func _dump_state() -> void: String(main.runner.current_node().get("type", "")), str(main.bonus_overlay.visible), str(main._inter_overlay.visible), str(main.shop_overlay.visible), str(main.runner.next_candidate_ids())]) - - -# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;与 ui_probe 同款助手) -func _find_button_by_prefix(root: Node, prefix: String) -> Button: - if root is Button and String(root.text).begins_with(prefix): - return root - for child in root.get_children(): - var found := _find_button_by_prefix(child, prefix) - if found != null: - return found - return null - - -func _find_button_by_text(root: Node, needle: String) -> Button: - if root is Button and String(root.text).contains(needle): - return root - for child in root.get_children(): - var found := _find_button_by_text(child, needle) - if found != null: - return found - return null - - -func _find_checkbutton_by_text(root: Node, needle: String) -> CheckButton: - if root is CheckButton and String(root.text).contains(needle): - return root - for child in root.get_children(): - var found := _find_checkbutton_by_text(child, needle) - if found != null: - return found - return null diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index e7615c2a..28cc3088 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -13,7 +13,9 @@ # 运行:Godot_console.exe --headless --path . res://smoke/ui_input_probe.tscn extends Node -var main +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点 + +var main: Node var _problems: Array = [] var _actions: Array = [] # action_requested 收集([cell, kind]) var _chords: Array = [] # chord_attempted 收集 @@ -26,16 +28,9 @@ func _ready() -> void: add_child(main) await get_tree().process_frame # 存档隔离(迭代 8 卡①②——断点写盘面随冒险挂 main.save_manager;探针禁碰真实档 user://save.json, - # 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变) - for p0 in ["user://smoke_input_probe_save.json", "user://smoke_input_probe_save.json.tmp"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退 - var iso = load("res://core/save_manager.gd").new("user://smoke_input_probe_save.json") - iso.load_from_disk() - iso.mark_first_adventure_done() - main.save_manager = iso - main.meta = load("res://core/meta_progression.gd").new(main.db, iso) - main._refresh_resume_button() + # 同 ui_click_probe 隔离纪律。首次冒险布尔位置位=对齐真实档游玩史口径,模板抽签路径不变; + # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) + ProbeUiUtil.isolate_save(main, "smoke_input_probe_save", true, false, true) main._start_adventure_from_menu() await get_tree().process_frame # ---- ⑫ 图上命中真实输入(关卡画面实装批 2026-09-28 回归闸):入口停点候选节点中心 @@ -99,6 +94,8 @@ func _ready() -> void: var maxl2 := 0 for k2 in layer2.keys(): maxl2 = maxi(maxl2, int(layer2[k2])) + # 常数锚(画布拟合探针):70/122/26/8 与 presentation/views/level_map_screen.gd 的 _content_size + # 公式(70 + maxl*122 + NODE_HALF.x + 8)同源复刻——改常数须两侧同步(本探针只读断言层,真源在屏) var need_x2: float = 70.0 + maxl2 * 122.0 + 26.0 + 8.0 if float(ro2._canvas.custom_minimum_size.x) < need_x2 - 0.5: _pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [ro2._canvas.custom_minimum_size.x, need_x2]) diff --git a/game/smoke/ui_layout_probe.gd b/game/smoke/ui_layout_probe.gd index d0dfb026..cbdc2538 100644 --- a/game/smoke/ui_layout_probe.gd +++ b/game/smoke/ui_layout_probe.gd @@ -4,22 +4,17 @@ # 运行:Godot_console.exe --headless --path . res://smoke/ui_layout_probe.tscn extends Node +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点 + func _ready() -> void: await get_tree().process_frame # autoload 就绪 - var main = load("res://presentation/main.tscn").instantiate() + var main: Node = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame - # 存档隔离(迭代 8 卡①②——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律) - for p0 in ["user://smoke_layout_probe_save.json", "user://smoke_layout_probe_save.json.tmp"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退 - var iso = load("res://core/save_manager.gd").new("user://smoke_layout_probe_save.json") - iso.load_from_disk() - iso.mark_first_adventure_done() - main.save_manager = iso - main.meta = load("res://core/meta_progression.gd").new(main.db, iso) - main._refresh_resume_button() + # 存档隔离(迭代 8 卡①②——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律; + # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) + ProbeUiUtil.isolate_save(main, "smoke_layout_probe_save", true, false, true) main._start_adventure_from_menu() for i in range(3): await get_tree().process_frame diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index 2c2506da..d4ab4c06 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -7,10 +7,12 @@ # 运行:Godot_console.exe --headless --path . res://smoke/ui_probe.tscn extends Node +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:树寻址/隔离样板单点 + func _ready() -> void: await get_tree().process_frame # autoload 就绪 - var main = load("res://presentation/main.tscn").instantiate() + var main: Node = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame var problems: Array = [] @@ -20,11 +22,10 @@ func _ready() -> void: get_tree().quit(1) return # ---- 营地覆盖层(元进度实现批——主菜单入口/开合/余额文案/购买禁用态; - # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律)---- - var iso = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json") - iso.load_from_disk() - main.meta = load("res://core/meta_progression.gd").new(main.db, iso) - var campbtn: Button = _find_button_by_prefix(main._start_overlay, "营地") + # 隔离存档换注 main.meta 保确定性(禁碰真实档 user://save.json——SaveManager 路径注入纪律; + # 营地先行段只换 meta 不换 save_manager——冒险段换注零档在前)---- + ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", false, false, false, false) + var campbtn: Button = ProbeUiUtil.find_button(main._start_overlay, "营地", "prefix") if campbtn == null: problems.append("营地入口按钮缺失(开始层 campbtn)") else: @@ -52,15 +53,7 @@ func _ready() -> void: if main.camp_overlay.visible or not main._start_overlay.visible: problems.append("营地关闭后未恢复开始层(开合往返失守)") # 换回全新零档(营地段购买态不泄入后续冒险段——确定性;先删后建防陈旧档泄入) - for p in ["user://smoke_ui_probe_camp.json", "user://smoke_ui_probe_camp.json.tmp"]: - if FileAccess.file_exists(p): - DirAccess.remove_absolute(p) - var iso_zero = load("res://core/save_manager.gd").new("user://smoke_ui_probe_camp.json") - iso_zero.load_from_disk() - iso_zero.mark_first_adventure_done() # 对齐真实档游玩史口径(模板抽签路径不变) - main.save_manager = iso_zero # 迭代 8 卡①——断点写盘面同步隔离(禁碰真实档) - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_zero) - main._refresh_resume_button() + ProbeUiUtil.isolate_save(main, "smoke_ui_probe_camp", true, false, true) main._start_adventure_from_menu() await get_tree().process_frame await get_tree().process_frame @@ -244,14 +237,3 @@ func _ready() -> void: push_error("[ui_probe] FAIL — " + p) print("[ui_probe] FAIL — " + "; ".join(problems)) get_tree().quit(1) - - -# 开始层按钮按文案前缀寻址(campbtn 句柄未存组合根成员——树遍历只读;text 前缀匹配首枚按钮) -func _find_button_by_prefix(root: Node, prefix: String) -> Button: - if root is Button and String(root.text).begins_with(prefix): - return root - for child in root.get_children(): - var found := _find_button_by_prefix(child, prefix) - if found != null: - return found - return null diff --git a/game/smoke/ui_tune_probe.gd b/game/smoke/ui_tune_probe.gd index 87d7314f..43c48be8 100644 --- a/game/smoke/ui_tune_probe.gd +++ b/game/smoke/ui_tune_probe.gd @@ -6,10 +6,14 @@ # 运行:Godot_console.exe --headless --path . res://smoke/ui_tune_probe.tscn extends Node +const ProbeUiUtil := preload("res://smoke/probe_ui_util.gd") # T6 收口:隔离样板单点 + +const SIM_GUARD_FRAMES := 1800 # ⑤ N=10 headless 模拟防挂死帧预算上限(ADR-0004:探针内停滞检测第一道——坏态不等满预算才报,超时即断言失败) + func _ready() -> void: await get_tree().process_frame - var main = load("res://presentation/main.tscn").instantiate() + var main: Node = load("res://presentation/main.tscn").instantiate() add_child(main) await get_tree().process_frame var problems: Array = [] @@ -37,7 +41,7 @@ func _ready() -> void: main.tune_panel.set_sliders(100, 0) main.tune_panel.set_sim_count(10) main.tune_panel.press_simulate_for_probe() - var guard := 1800 # N=10 headless 模拟防挂死上限(ADR-0004:探针内停滞检测第一道) + var guard := SIM_GUARD_FRAMES while main.tune_panel.is_running() and guard > 0: guard -= 1 await get_tree().process_frame @@ -59,15 +63,9 @@ func _ready() -> void: problems.append("hero 表被运行时改写(E4 违例:atk=%s hp=%s,期望恒基线 %d/%d)" % [str(main.db.hero.atk), str(main.db.hero.max_hp), base_atk, base_hp]) # ⑧ 单次加成生效回归断言:应用加成 + 启动单场战斗 → 呈现快照 hero 必须是"基线 × 单次加成" # (攻击 ×2 / 生命 +50%)——旧缺陷(残留值被当基线再乘)在 encounter 参数链上同样被抓获 - # 存档隔离(迭代 8 卡①——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律) - for p0 in ["user://smoke_tune_probe_save.json", "user://smoke_tune_probe_save.json.tmp"]: - if FileAccess.file_exists(p0): - DirAccess.remove_absolute(p0) # 段首清场——上次运行的残留断点会触发互斥确认早退 - var iso_tune = load("res://core/save_manager.gd").new("user://smoke_tune_probe_save.json") - iso_tune.load_from_disk() - iso_tune.mark_first_adventure_done() - main.save_manager = iso_tune - main.meta = load("res://core/meta_progression.gd").new(main.db, iso_tune) + # 存档隔离(迭代 8 卡①——停点断点写盘随 main.save_manager;探针禁碰真实档,同族探针隔离纪律; + # isolate_save 先删后建——上次运行的残留断点会触发互斥确认早退) + ProbeUiUtil.isolate_save(main, "smoke_tune_probe_save", true, false, false) main.tune_panel.set_sliders(100, 50) main.tune_panel.press_apply_for_probe() main._start_adventure_from_menu() # 完整冒险路径(唯一试玩入口;_start_single 按语义清零滑条 = 恒基线,不承载加成断言) diff --git a/game/tests/unit/battle_settlement/damage_controller_test.gd b/game/tests/unit/battle_settlement/damage_controller_test.gd index 25513331..143d5a69 100644 --- a/game/tests/unit/battle_settlement/damage_controller_test.gd +++ b/game/tests/unit/battle_settlement/damage_controller_test.gd @@ -16,15 +16,15 @@ const DamageControllerScript := preload("res://core/damage_controller.gd") const TEST_SEED := 20260928 # 夹具种子(基建值非玩法数值——回放确定性契约的输入) -var _failures := 0 -var _checks := 0 +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 + var _rows: Array = [] # 当前 run 的行协议收集(_on_row 绑定法——避 lambda 多行控制流坑) func _init() -> void: var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) - _check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) + Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) if load_ok: _test_five_recipes_and_replay(db) _test_policy_immediate_mode(db) @@ -33,21 +33,7 @@ func _init() -> void: _test_hero_death_halt(db) _test_ac_merge(db) _test_pool_clamp(db) - if _failures > 0: - push_error("[damage_controller_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[damage_controller_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + quit(Kit.finish("damage_controller_test")) # ---- 夹具 ---- @@ -84,7 +70,7 @@ func _run_recipe_sequence(db) -> Array: var units = h.units var ctrl = h.ctrl if not h.spawn_ok: - _check("前置:units.spawn(1,0) 成功(遭遇行在案)", false) + Kit.check("前置:units.spawn(1,0) 成功(遭遇行在案)", false) return [] var hero_before: int = int(units.hero_unit().hp) var enemy_hp_before: Array = [] @@ -122,27 +108,27 @@ func _run_recipe_sequence(db) -> Array: for id in [t1, t2, t3, t4, t5]: if ctrl.outcome(int(id)) != "redeemed": outcomes_redeemed = false - _check("五配方单据 flush 后全部 redeemed(B 模式段界结账)", outcomes_redeemed) + Kit.check("五配方单据 flush 后全部 redeemed(B 模式段界结账)", outcomes_redeemed) var hero_after: int = int(units.hero_unit().hp) var r2: Dictionary = _row_by_display_prefix("T2敌方技") var r5: Dictionary = _row_by_display_prefix("T5反噬") - _check("主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)", + Kit.check("主角扣血 = 敌方技行值 + 反噬行值(逐行归因闭合)", hero_before - hero_after == int(r2.get("value", -1)) + int(r5.get("value", -1))) - _check("敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)", int(r2.get("value", 0)) >= floor_v + floor_v) - _check("反噬行值 = 落地下限(中性无类型表值)", int(r5.get("value", 0)) == floor_v) + Kit.check("敌方技行值 ≥ 下限 + rider(rider 后置不进乘区)", int(r2.get("value", 0)) >= floor_v + floor_v) + Kit.check("反噬行值 = 落地下限(中性无类型表值)", int(r5.get("value", 0)) == floor_v) var r1: Dictionary = _row_by_display_prefix("T1主角攻击") var ex1: Dictionary = r1.get("extra", {}) - _check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected) - _check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces)) + Kit.check("主角攻击行 AC = 防守方 AC + 攻击加成(开单前快照一致)", int(ex1.get("ac", -1)) == threshold_expected) + Kit.check("掷点 ∈ [1, 骰面]", int(ex1.get("roll", -1)) >= 1 and int(ex1.get("roll", -1)) <= int(h.db.settlement.roll_faces)) var tier_ok := false for dv in h.db.settlement.roll_tier_discounts: if int(dv) == int(ex1.get("tier", -1)): tier_ok = true if int(ex1.get("tier", -1)) == 100: tier_ok = true - _check("掷点档位 ∈ 表档位值 ∪ 满额", tier_ok) + Kit.check("掷点档位 ∈ 表档位值 ∪ 满额", tier_ok) var r4: Dictionary = _row_by_display_prefix("T4群伤") var alive_at_blast: int = int(r4.get("extra", {}).get("count", -1)) @@ -158,10 +144,10 @@ func _run_recipe_sequence(db) -> Array: var attack_landed: int = int(r1.get("value", 0)) var typed_landed: int = int(_row_by_display_prefix("T3克制直伤").get("value", 0)) if any_enemy_dead: - _check("敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)", + Kit.check("敌方总扣血 ≥ 攻击+克制+群伤(死亡溢出合法,下界归因)", enemy_drop_total >= attack_landed + typed_landed + int(r4.get("value", 0)) * maxi(0, alive_at_blast)) else: - _check("敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)", + Kit.check("敌方总扣血 = 攻击 + 克制直伤 + 群伤×存活数(逐行归因闭合)", enemy_drop_total == attack_landed + typed_landed + int(r4.get("value", 0)) * alive_at_blast) var protocol_ok := true @@ -174,7 +160,7 @@ func _run_recipe_sequence(db) -> Array: protocol_ok = false if not (r.extra is Dictionary) or not r.extra.has("target"): protocol_ok = false - _check("行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)", protocol_ok) + Kit.check("行协议公共字段封闭(type/source_class/display/value≥下限/extra.target)", protocol_ok) # 复现序列(供双跑对拍):逐行 (display, value) var seq: Array = [] @@ -185,9 +171,9 @@ func _run_recipe_sequence(db) -> Array: func _test_five_recipes_and_replay(db) -> void: var run1: Array = _run_recipe_sequence(db) - _check("五配方序列产出 5 行伤害行", run1.size() == 5) + Kit.check("五配方序列产出 5 行伤害行", run1.size() == 5) var run2: Array = _run_recipe_sequence(db) - _check("同种子双跑行序列逐位一致(回放确定性契约)", run1 == run2) + Kit.check("同种子双跑行序列逐位一致(回放确定性契约)", run1 == run2) # ---- A 模式(开单即结——回切位) ---- @@ -201,8 +187,8 @@ func _test_policy_immediate_mode(db) -> void: var floor_v: int = int(h.db.settlement.damage_floor) var t: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "A模式反噬", "棋盘兑付", floor_v, DamageControllerScript.TARGET_HERO, 1)) - _check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed") - _check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v) + Kit.check("A 模式开单即结(无 flush 即 redeemed)", h.ctrl.outcome(t) == "redeemed") + Kit.check("A 模式主角即时扣血", int(h.units.hero_unit().hp) == int(h.db.hero.max_hp) - floor_v) # ---- 来源敌死亡:队列中未结单据作废(死亡即不动手) ---- @@ -221,8 +207,8 @@ func _test_source_death_void(db) -> void: var t: int = h.ctrl.submit(ticket) h.units.apply_damage(src, int(src.max_hp)) # 敌当场倒下 → unit_died → 该敌未结单作废 h.ctrl.flush() - _check("来源敌死亡后其未结单据 voided(死亡即不动手)", h.ctrl.outcome(t) == "voided") - _check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty()) + Kit.check("来源敌死亡后其未结单据 voided(死亡即不动手)", h.ctrl.outcome(t) == "voided") + Kit.check("作废单零行零扣血", int(h.units.hero_unit().hp) == hero_before and _rows.is_empty()) # ---- requires 前置门:前置单未实际有效兑付 → 后继单静默作废;无存活敌 blast = 合法零兑付 ---- @@ -238,7 +224,7 @@ func _test_requires_gate_and_zero(db) -> void: var big: int = front_hp * 10 var t_clear: int = h.ctrl.flush_submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "清场群伤", "棋盘兑付", big, DamageControllerScript.TARGET_ALL_ALIVE, 1)) - _check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty()) + Kit.check("flush_submit 同步开单即结(清图批契约)", h.ctrl.outcome(t_clear) == "redeemed" and h.units.query_alive("enemy").is_empty()) var t_zero: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_BLAST, "clear_reward", "空场群伤", "棋盘兑付", floor_v, DamageControllerScript.TARGET_ALL_ALIVE, 1)) var gated_ticket: Dictionary = h.ctrl.open_ticket(DamageControllerScript.KIND_HERO_ATTACK, "hero_attack", @@ -246,8 +232,8 @@ func _test_requires_gate_and_zero(db) -> void: gated_ticket["requires"] = t_zero var t_gated: int = h.ctrl.submit(gated_ticket) h.ctrl.flush() - _check("无存活敌 blast = 合法零兑付(zero 非 voided)", h.ctrl.outcome(t_zero) == "zero") - _check("前置单未实际有效 → 后继单 voided(requires 门)", h.ctrl.outcome(t_gated) == "voided") + Kit.check("无存活敌 blast = 合法零兑付(zero 非 voided)", h.ctrl.outcome(t_zero) == "zero") + Kit.check("前置单未实际有效 → 后继单 voided(requires 门)", h.ctrl.outcome(t_gated) == "voided") # ---- 主角倒下:flush 余单全部作废(_halted 终止) ---- @@ -263,8 +249,8 @@ func _test_hero_death_halt(db) -> void: var t_after: int = h.ctrl.submit(h.ctrl.open_ticket(DamageControllerScript.KIND_BOARD_FLAT_HERO, "mine_hit", "余单", "棋盘兑付", 1, DamageControllerScript.TARGET_HERO, 2)) h.ctrl.flush() - _check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0) - _check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided") + Kit.check("致死单 redeemed 且主角 hp 归零", h.ctrl.outcome(t_kill) == "redeemed" and int(h.units.hero_unit().hp) == 0) + Kit.check("主角倒下后余单 voided(flush 终止)", h.ctrl.outcome(t_after) == "voided") # ---- AC 合流:override 入参 / 表值兜底 / ac 型状态叠层(不入池) ---- @@ -275,10 +261,10 @@ func _test_ac_merge(db) -> void: if not h.spawn_ok: return var base_ac: int = int(h.db.hero.armor_class) - _check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac) + Kit.check("缺省兜底:defender_ac(hero) = hero 表值", h.ctrl.defender_ac(h.units.hero_unit()) == base_ac) var override_v: int = base_ac + int(h.db.settlement.damage_floor) h.ctrl.set_hero_ac_override(override_v) - _check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v) + Kit.check("override 合流入参生效", h.ctrl.defender_ac(h.units.hero_unit()) == override_v) # ac 型状态效果叠其上(找 hero 侧 unit=ac 行) var ac_eff := {} for row in h.db.status_effects.rows: @@ -286,12 +272,12 @@ func _test_ac_merge(db) -> void: ac_eff = row break if ac_eff.is_empty(): - _check("前置:状态表存在 hero 侧 ac 型行", false) + Kit.check("前置:状态表存在 hero 侧 ac 型行", false) return var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} h.effects.apply_effect(String(ac_eff.effect_id), src) var expected: int = override_v + int(ac_eff.magnitude) - _check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected) + Kit.check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected) # ---- 池乘区收口:池内加算 → clamp;ac 型不入池 ---- @@ -303,7 +289,7 @@ func _test_pool_clamp(db) -> void: return var lo: int = int(h.db.settlement.pool_clamp_lo) var hi: int = int(h.db.settlement.pool_clamp_hi) - _check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0) + Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0) # 找 hero_offense 池的 percent 型 hero 侧效果行 var pool_eff := {} for row in h.db.status_effects.rows: @@ -313,14 +299,14 @@ func _test_pool_clamp(db) -> void: pool_eff = row break if pool_eff.is_empty(): - _check("前置:hero_offense 池存在 percent 型 hero 侧行", false) + Kit.check("前置:hero_offense 池存在 percent 型 hero 侧行", false) return var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src) var expected1: int = clampi(int(pool_eff.magnitude), lo, hi) - _check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1) + Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1) var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src) - _check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变", + Kit.check("refresh 政策重复授予受理且层数恒 1(叠层改续时裁决)——池值不变", second_ok and h.ctrl.clamped_pool("hero_offense") == expected1) # ac 型效果不入 percent 池 var ac_eff2 := {} @@ -331,7 +317,7 @@ func _test_pool_clamp(db) -> void: if not ac_eff2.is_empty(): var before: int = h.ctrl.clamped_pool("hero_offense") h.effects.apply_effect(String(ac_eff2.effect_id), src) - _check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before) + Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before) # stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入) var stack_eff := {} for row3 in h.db.status_effects.rows: @@ -339,7 +325,7 @@ func _test_pool_clamp(db) -> void: stack_eff = row3 break if stack_eff.is_empty(): - _check("前置:状态表存在 stack 政策行", false) + Kit.check("前置:状态表存在 stack 政策行", false) return var effects2 = StatusContainerScript.new(h.db) effects2.notify_battle_start() @@ -348,4 +334,4 @@ func _test_pool_clamp(db) -> void: for i in range(max_n + 1): if effects2.apply_effect(String(stack_eff.effect_id), src): granted += 1 - _check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n) + Kit.check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n) diff --git a/game/tests/unit/battle_settlement/damage_math_test.gd b/game/tests/unit/battle_settlement/damage_math_test.gd index e1d5a294..6d103ce3 100644 --- a/game/tests/unit/battle_settlement/damage_math_test.gd +++ b/game/tests/unit/battle_settlement/damage_math_test.gd @@ -6,36 +6,21 @@ extends SceneTree const DamageMathScript := preload("res://core/damage_math.gd") const ConfigDBScript := preload("res://core/config_db.gd") +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 -var _failures := 0 -var _checks := 0 var db func _init() -> void: db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) - _check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0) + Kit.check("前置:settlement 表加载零 error", load_ok and db.diagnostics_count("error") == 0) if load_ok: _test_pool_damage() _test_counter_component() _test_roll_discount_percent() _test_rolled_component() - if _failures > 0: - push_error("[damage_math_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[damage_math_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + quit(Kit.finish("damage_math_test")) func _floor_val() -> int: @@ -46,23 +31,23 @@ func _floor_val() -> int: func _test_pool_damage() -> void: var d := _floor_val() + 9 # 表值推导的任意正探针量(非玩法行值) - _check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d) - _check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d) - _check("正主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d) - _check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0)) - _check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30)) - _check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0) + Kit.check("中性双池 (0,0) 原样透传", DamageMathScript.pool_damage(d, 0, 0) == d) + Kit.check("负敌方池压低不增高", DamageMathScript.pool_damage(d, 0, int(db.settlement.pool_clamp_lo)) <= d) + Kit.check("正主角池抬升不低于原值", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_hi), 0) >= d) + Kit.check("主角池单调不减(+50% vs +49% 探针档)", DamageMathScript.pool_damage(d, 50, 0) >= DamageMathScript.pool_damage(d, 49, 0)) + Kit.check("对称性:池值互换结果不变(乘算交换)", DamageMathScript.pool_damage(d, 30, -20) == DamageMathScript.pool_damage(d, -20, 30)) + Kit.check("输出非负(maxi(0,·) 封底)", DamageMathScript.pool_damage(d, int(db.settlement.pool_clamp_lo), int(db.settlement.pool_clamp_lo)) >= 0) # ---- 克制分量:空桶不吃修正恒 0;落地下限保底([1,+∞) 全来源)---- func _test_counter_component() -> void: var floor_v := _floor_val() - _check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0 + Kit.check("空桶(点数≤0)恒 0 不吃修正", DamageMathScript.counter_component(0, floor_v, floor_v) == 0 and DamageMathScript.counter_component(-1, 5, floor_v) == 0) - _check("正向修正直加", DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2) - _check("负修正触底保落地伤害下限", DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v) - _check("下限参数缺省 = 0(无下限语义面)", DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0) + Kit.check("正向修正直加", DamageMathScript.counter_component(floor_v, floor_v, floor_v) == floor_v * 2) + Kit.check("负修正触底保落地伤害下限", DamageMathScript.counter_component(floor_v, -floor_v * 10, floor_v) == floor_v) + Kit.check("下限参数缺省 = 0(无下限语义面)", DamageMathScript.counter_component(floor_v, -floor_v * 10) == 0) # ---- 掷点折扣查档(纯函数):满阈值恒全额;档位边界/末档语义与表数组一致 ---- @@ -71,22 +56,22 @@ func _test_roll_discount_percent() -> void: var threshold := 10 # 查档探针阈值(夹具值——档位参数本身全部来自表) var bounds: Array = db.settlement.roll_tier_bounds var discounts: Array = db.settlement.roll_tier_discounts - _check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0) - _check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100 + Kit.check("前置:档位数 = 边界数 + 1(查档契约——档数 = 边界数 + 1)", discounts.size() == bounds.size() + 1 and discounts.size() > 0) + Kit.check("掷点 ≥ 阈值恒全额 100", DamageMathScript.roll_discount_percent(threshold, threshold, bounds, discounts) == 100 and DamageMathScript.roll_discount_percent(threshold + 5, threshold, bounds, discounts) == 100) # 缺口 = 1:落首档(缺口 ∈ [1, bounds[0]] → discounts[0]) var tier0_expected: int = int(discounts[0]) if int(bounds[0]) >= 1: - _check("缺口 1 落首档", DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected) + Kit.check("缺口 1 落首档", DamageMathScript.roll_discount_percent(threshold - 1, threshold, bounds, discounts) == tier0_expected) # 缺口超末边界:落末档 var last_expected: int = int(discounts[-1]) - _check("缺口超末边界落末档", DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected) + Kit.check("缺口超末边界落末档", DamageMathScript.roll_discount_percent(threshold - int(bounds[-1]) - 1, threshold, bounds, discounts) == last_expected) # 档位值降序(重档折扣更深)——表自检 var descending := true for i in range(discounts.size() - 1): if int(discounts[i]) < int(discounts[i + 1]): descending = false - _check("档位值随档位加深不升(折扣单调)", descending) + Kit.check("档位值随档位加深不升(折扣单调)", descending) # ---- 吃掷点分量算术:折扣→(暴击)→floor→+克制修正(单一取整链)---- @@ -94,9 +79,9 @@ func _test_roll_discount_percent() -> void: func _test_rolled_component() -> void: var base := float(_floor_val() + 7) # 表值推导探针基线 var crit_mult: int = int(db.settlement.crit_multiplier) - _check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base))) - _check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0))) - _check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0)) - _check("克制修正加在 floor 之后(加法核)", DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val()) - _check("复合序 = 基线×折扣%×暴击%(floor 只在链末)", + Kit.check("满额折扣无暴击 = floor(基线) + 修正 0", DamageMathScript.rolled_component(base, 100, false, crit_mult, 0) == int(floor(base))) + Kit.check("满额折扣暴击按表倍率放大", DamageMathScript.rolled_component(base, 100, true, crit_mult, 0) == int(floor(base * float(crit_mult) / 100.0))) + Kit.check("折扣降低不高于满额", DamageMathScript.rolled_component(base, 50, false, crit_mult, 0) <= DamageMathScript.rolled_component(base, 100, false, crit_mult, 0)) + Kit.check("克制修正加在 floor 之后(加法核)", DamageMathScript.rolled_component(base, 100, false, crit_mult, _floor_val()) == int(floor(base)) + _floor_val()) + Kit.check("复合序 = 基线×折扣%×暴击%(floor 只在链末)", DamageMathScript.rolled_component(base, 50, true, crit_mult, 0) == int(floor(base * 50.0 / 100.0 * float(crit_mult) / 100.0))) diff --git a/game/tests/unit/contracts/system_contracts_test.gd b/game/tests/unit/contracts/system_contracts_test.gd index 6f332bf1..36901a8d 100644 --- a/game/tests/unit/contracts/system_contracts_test.gd +++ b/game/tests/unit/contracts/system_contracts_test.gd @@ -12,35 +12,19 @@ const ConfigDBScript := preload("res://core/config_db.gd") const RngServiceScript := preload("res://core/rng_service.gd") const StatusContainerScript := preload("res://core/status_container.gd") const DamageControllerScript := preload("res://core/damage_controller.gd") - -var _failures := 0 -var _checks := 0 +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 func _init() -> void: var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) - _check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) + Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) if load_ok: _test_damage_controller_vocabularies() _test_rng_tag_closure(db) _test_ticket_nine_columns() _test_status_boolean_acceptance(db) - if _failures > 0: - push_error("[system_contracts_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[system_contracts_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + quit(Kit.finish("system_contracts_test")) # ---- B1 兑付配方词表:五配方/三目标规则/双结账政策——常量非空且互异(封闭枚举负空间) ---- @@ -55,11 +39,11 @@ func _test_damage_controller_vocabularies() -> void: for k in kinds: if String(k).is_empty(): nonempty = false - _check("五配方常量非空(开单面唯一分派键)", nonempty) + Kit.check("五配方常量非空(开单面唯一分派键)", nonempty) var unique_kinds: Dictionary = {} for k in kinds: unique_kinds[String(k)] = true - _check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size()) + Kit.check("五配方常量互异(词表封闭无别名)", unique_kinds.size() == kinds.size()) var targets: Array = [ DamageControllerScript.TARGET_FRONT_ALIVE, DamageControllerScript.TARGET_HERO, DamageControllerScript.TARGET_ALL_ALIVE, @@ -67,8 +51,8 @@ func _test_damage_controller_vocabularies() -> void: var unique_targets: Dictionary = {} for t in targets: unique_targets[String(t)] = true - _check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size()) - _check("双结账政策常量互异(A 即结 / B 攒批回切位)", + Kit.check("三目标规则常量互异(结账时刻现场解析词表)", unique_targets.size() == targets.size()) + Kit.check("双结账政策常量互异(A 即结 / B 攒批回切位)", String(DamageControllerScript.POLICY_IMMEDIATE) != String(DamageControllerScript.POLICY_SEGMENT)) @@ -76,9 +60,9 @@ func _test_damage_controller_vocabularies() -> void: func _test_rng_tag_closure(db) -> void: var stream_label: String = String(db.settlement.armor_roll_stream) - _check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)", + Kit.check("settlement.armor_roll_stream ∈ RngService 封闭标签词表(EC12)", RngServiceScript.PURPOSE_TAGS.has(stream_label)) - _check("RngService 用途标签词表非空且互异(封闭语义由流改标签)", + Kit.check("RngService 用途标签词表非空且互异(封闭语义由流改标签)", RngServiceScript.PURPOSE_TAGS.size() > 0) @@ -95,9 +79,9 @@ func _test_ticket_nine_columns() -> void: for c in columns: if not t.has(c): all_present = false - _check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present) - _check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "") - _check("缺省无前置依赖(requires = -1)", int(t.requires) == -1) + Kit.check("单据九栏 + 可选两栏缺省齐备(开单面只写差异栏前提)", all_present) + Kit.check("缺省无类型轴(attack_type 空 = 恒不查克制矩阵)", String(t.attack_type) == "") + Kit.check("缺省无前置依赖(requires = -1)", int(t.requires) == -1) func _bare_controller(): @@ -129,18 +113,18 @@ func _test_status_boolean_acceptance(db) -> void: var any_row: Dictionary = {} if db.status_effects.rows.size() > 0: any_row = db.status_effects.rows[0] - _check("前置:状态表非空", not any_row.is_empty()) + Kit.check("前置:状态表非空", not any_row.is_empty()) if any_row.is_empty(): return var probe_id: String = String(any_row.effect_id) var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0} - _check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false) - _check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false) + Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false) + Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false) effects.notify_battle_start() - _check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true) + Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true) var active: Array = effects.query_active(String(any_row.target_side)) var projected := false for a in active: if String(a.effect_id) == probe_id: projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude) - _check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected) + Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected) diff --git a/game/tests/unit/core/aux_items_test.gd b/game/tests/unit/core/aux_items_test.gd index 2dff7fea..f78c1626 100644 --- a/game/tests/unit/core/aux_items_test.gd +++ b/game/tests/unit/core/aux_items_test.gd @@ -6,13 +6,7 @@ extends SceneTree const AuxItemsScript := preload("res://core/aux_items.gd") - -var _problems: Array = [] - - -func check(cond: bool, msg: String) -> void: - if not cond: - _problems.append(msg) +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道) class StubTable: @@ -59,39 +53,39 @@ func _init() -> void: var inv = _new_inv() var r1: Dictionary = inv.add_item("sapper_order", "safe_cell", Vector2i(3, 4), 2) var r2: Dictionary = inv.add_item("nest_pendulum", "flag_hit", Vector2i(5, 6), 3) - check(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)") - check(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2, + Kit.check_quiet(int(r1.handle) == 1 and int(r2.handle) == 2, "句柄受理序单调(1→2——零随机)") + Kit.check_quiet(String(r1.source_class) == "safe_cell" and r1.origin_cell == Vector2i(3, 4) and int(r1.round_index) == 2, "授予来源可归因(source/cell/round)") var inv_proj: Array = inv.query_inventory(Callable()) - check(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用") - check(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执") - check(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)") - check(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)") + Kit.check_quiet(inv_proj.size() == 2 and not bool(inv_proj[0].used), "库存投影两件未用") + Kit.check_quiet(bool(inv.consume(int(r1.handle)).get("used", false)), "consume 返回已用快照回执") + Kit.check_quiet(inv.consume(int(r1.handle)).is_empty(), "Used 不可逆——二次 consume 拒绝(空回执)") + Kit.check_quiet(int(inv.inventory_count()) == 2, "Used 保留只读(库存计数不变)") # ② 真视恒可用+边缘窗口∩板(3×3 中心贴角——角 4 格) var v_ok: Dictionary = inv.validate_use(int(r2.handle), Vector2i(0, 0), Vector2i(10, 10), Callable()) - check(bool(v_ok.ok) and v_ok.resolved.size() == 4, + Kit.check_quiet(bool(v_ok.ok) and v_ok.resolved.size() == 4, "真视边缘窗口=窗口∩板(角 4 格——护栏只缩不扩)") # 预读点击级零信息:中心板内但回调恒 false(全已开)→拒绝零消耗 var inv2 = _new_inv() var rp: Dictionary = inv2.add_item("loot_compass", "safe_cell", Vector2i(1, 1), 1) var v_zero: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(5, 5), Vector2i(10, 10), Callable()) - check(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1, + Kit.check_quiet(not bool(v_zero.ok) and int(inv2.inventory_count()) == 1, "预读点击级零信息拒绝(不消耗——库存不变)") # 目标越板 var v_oob: Dictionary = inv2.validate_use(int(rp.handle), Vector2i(12, 5), Vector2i(10, 10), Callable()) - check(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝") + Kit.check_quiet(not bool(v_zero.ok) and not bool(v_oob.ok), "越板目标拒绝") # ③ 叠加层+三清(retire 不动库存;reset 全清) inv2.apply_overlay("mine_reveal", v_ok.resolved) inv2.apply_overlay("reward_preview", [Vector2i(5, 5)]) var ov: Dictionary = inv2.query_overlays() - check(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)") + Kit.check_quiet(ov.mine_reveal.size() == 4 and ov.reward_preview.size() == 1, "叠加层两集落位(真视 4/预读 1)") inv2.retire_overlays() - check(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1, + Kit.check_quiet(inv2.query_overlays().mine_reveal.is_empty() and int(inv2.inventory_count()) == 1, "换图三清:叠加层消亡+库存不动(关卡域)") inv2.reset() - check(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)") + Kit.check_quiet(inv2.inventory_count() == 0 and inv2.probe().used == 0, "关卡末 reset 全清(幂等)") # ④ 零随机重放:同操作序列两遍,投影逐位相同 var replay_a: Array = [] @@ -106,13 +100,7 @@ func _init() -> void: replay_a = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()] else: replay_b = [inv3.query_inventory(Callable()), inv3.query_overlays(), inv3.probe()] - check(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同") + Kit.check_quiet(str(replay_a) == str(replay_b), "零随机重放:库存+叠加层+探针投影逐位相同") # ---- 汇总 ---- - if _problems.is_empty(): - print("[aux_items_test] PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过") - quit(0) - else: - for p in _problems: - push_error("[aux_items_test] FAIL — " + p) - quit(1) + quit(Kit.finish("aux_items_test", "PASS — 授予归因/Used 不可逆/真视恒可用/零信息拒绝/边缘窗口/三清/零随机重放 全过")) diff --git a/game/tests/unit/core/breakpoint_resume_test.gd b/game/tests/unit/core/breakpoint_resume_test.gd index 4270efb1..5edf6cdd 100644 --- a/game/tests/unit/core/breakpoint_resume_test.gd +++ b/game/tests/unit/core/breakpoint_resume_test.gd @@ -8,16 +8,11 @@ extends SceneTree const SaveManagerScript := preload("res://core/save_manager.gd") const AdventureRunnerScript := preload("res://core/adventure_runner.gd") +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点(静默收集通道) -var _problems: Array = [] var _bp_path := "user://test_breakpoint_resume.json" -func check(cond: bool, msg: String) -> void: - if not cond: - _problems.append(msg) - - func _init() -> void: var db = load("res://core/config_db.gd").new() if not db.load_tables("res://data/tables", false): @@ -32,64 +27,64 @@ func _init() -> void: var adr_a = AdventureRunnerScript.new(db, rng_a) adr_a.set_save_manager(sm_a) var params := {"hero_override": {}, "aux_unlocked": {}} - check(adr_a.start_adventure(1234, params), "冒险启动(入口停点)") - check(sm_a.has_breakpoint(), "入口停点断点在档") - check(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1") - check(adr_a.abandon(), "入口停点放弃受理(非战斗期)") - check(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)") + Kit.check_quiet(adr_a.start_adventure(1234, params), "冒险启动(入口停点)") + Kit.check_quiet(sm_a.has_breakpoint(), "入口停点断点在档") + Kit.check_quiet(int((sm_a.breakpoint_snapshot() as Dictionary).get("level_index", -1)) == 1, "断点关卡序号 = 1") + Kit.check_quiet(adr_a.abandon(), "入口停点放弃受理(非战斗期)") + Kit.check_quiet(not sm_a.has_breakpoint() and sm_a.write_to_disk(), "放弃清断点+写盘(退出=清除断点)") var sm_reload = SaveManagerScript.new(_bp_path) sm_reload.load_from_disk() - check(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)") + Kit.check_quiet(not sm_reload.has_breakpoint(), "放弃后重读档无断点(「继续上局」隐藏口径)") # ---- Part 2:中断基线——入口停点后选路进战,记板指纹与计数器 ---- var rng_b = load("res://core/rng_service.gd").new() var sm_b = SaveManagerScript.new(_bp_path) var adr_b = AdventureRunnerScript.new(db, rng_b) adr_b.set_save_manager(sm_b) - check(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)") + Kit.check_quiet(adr_b.start_adventure(1234, params), "基线冒险启动(同种子 1234)") var runner_b = adr_b.runner() var c0: int = rng_b.counter() var cand_b: Array = runner_b.next_candidate_ids() var hp0: int = runner_b.hero_hp var bp_disk: Dictionary = sm_b.breakpoint_snapshot() - check(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)") - check(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战") + Kit.check_quiet(int(bp_disk.get("rng_counter", -1)) == c0, "断点计数器 = 当前取数号位(快照契约)") + Kit.check_quiet(runner_b.pick_next_node(int(cand_b[0])), "基线选路进首战") var battle_b = runner_b.current_battle() - check(battle_b != null, "基线首战开打") + Kit.check_quiet(battle_b != null, "基线首战开打") var fp_b := _board_fingerprint(battle_b.minesweeper.current_board()) var c1: int = rng_b.counter() # ---- Part 3:续玩确定性——新进程对象自盘上断点重建,同种子续玩板逐位一致 ---- var sm_c = SaveManagerScript.new(_bp_path) - check(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档") + Kit.check_quiet(sm_c.load_from_disk() and sm_c.has_breakpoint(), "断点自盘读回在档") var bp_c: Dictionary = sm_c.breakpoint_snapshot() var rng_c = load("res://core/rng_service.gd").new() var adr_c = AdventureRunnerScript.new(db, rng_c) - check(adr_c.resume_adventure(bp_c, params), "续玩受理") + Kit.check_quiet(adr_c.resume_adventure(bp_c, params), "续玩受理") var runner_c = adr_c.runner() - check(int(adr_c.master_seed) == 1234, "续玩主种子回填") - check(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)") - check(runner_c.hero_hp == hp0, "续玩生命账本回填") + Kit.check_quiet(int(adr_c.master_seed) == 1234, "续玩主种子回填") + Kit.check_quiet(int(rng_c.counter()) == c0, "续玩取数号位 = 断点号位(零重耗零跳号)") + Kit.check_quiet(runner_c.hero_hp == hp0, "续玩生命账本回填") var cand_c: Array = runner_c.next_candidate_ids() - check(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)") - check(runner_c.awaiting_choice(), "续玩处选路停点态") - check(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战") + Kit.check_quiet(cand_c.size() == cand_b.size() and int(cand_c[0]) == int(cand_b[0]), "续玩候选一致(停点重建)") + Kit.check_quiet(runner_c.awaiting_choice(), "续玩处选路停点态") + Kit.check_quiet(runner_c.pick_next_node(int(cand_c[0])), "续玩选路进首战") var battle_c = runner_c.current_battle() var fp_c := _board_fingerprint(battle_c.minesweeper.current_board()) - check(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)") - check(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致") + Kit.check_quiet(fp_c == fp_b, "续玩首战棋盘与中断前时间线逐位一致(同种子同板)") + Kit.check_quiet(int(rng_c.counter()) == c1, "续玩开板后号位与基线一致") # ---- Part 4:续玩幂等——同一断点二次续玩(新进程对象)结果逐位同 ---- var sm_d = SaveManagerScript.new(_bp_path) sm_d.load_from_disk() var rng_d = load("res://core/rng_service.gd").new() var adr_d = AdventureRunnerScript.new(db, rng_d) - check(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理") + Kit.check_quiet(adr_d.resume_adventure(sm_d.breakpoint_snapshot(), params), "二次续玩受理") var runner_d = adr_d.runner() - check(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)") - check(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路") + Kit.check_quiet(int(rng_d.counter()) == c0 and runner_d.hero_hp == hp0, "二次续玩状态逐位同(幂等)") + Kit.check_quiet(runner_d.pick_next_node(int(runner_d.next_candidate_ids()[0])), "二次续玩选路") var battle_d = runner_d.current_battle() - check(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)") + Kit.check_quiet(_board_fingerprint(battle_d.minesweeper.current_board()) == fp_b, "二次续玩板逐位同(幂等)") # ---- Part 5:三选一停点中断→续玩(迭代 10 奖励重构——候选不重抽、已兑付不重付、次数计数一致)---- var rng_e = load("res://core/rng_service.gd").new() @@ -97,50 +92,43 @@ func _init() -> void: var adr_e = AdventureRunnerScript.new(db, rng_e) adr_e.set_save_manager(sm_e) var params_e := {"hero_override": {}, "aux_unlocked": {"blast_kit": true}} - check(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)") + Kit.check_quiet(adr_e.start_adventure(4321, params_e), "P5 冒险启动(seed 4321)") adr_e._bonus_taken["attack"] = 1 # 白盒预置计数(断点附随件往返的判据面——计数不可由加算区反推) adr_e._on_level_finished({"result": "victory", "hp": 30, "node_index": 1, "node_id": 1, "node_type": "shop"}) var cands_e: Array = (adr_e._bonus_pending.get("candidates", []) as Array).duplicate() - check(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)") + Kit.check_quiet(not cands_e.is_empty(), "P5 非末关通关三选一停点在案(直调 victory——L1 非末关)") var sm_f = SaveManagerScript.new(_bp_path) - check(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)") + Kit.check_quiet(sm_f.load_from_disk() and sm_f.has_breakpoint(), "P5 选择停点断点在档(选择停点可中断契约)") var bp_f: Dictionary = sm_f.breakpoint_snapshot() var bp_f_cands: Array = (bp_f.get("bonus_pending", {}) as Dictionary).get("candidates", []) as Array - check(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)") - check(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)") + Kit.check_quiet(bp_f_cands == cands_e, "P5 断点附随件含候选(bonus_pending 往返)") + Kit.check_quiet(int((bp_f.get("bonus_taken", {}) as Dictionary).get("attack", -1)) == 1, "P5 断点附随件含次数计数(bonus_taken 往返)") var got_cands := {"cands": [] as Array} var adr_f = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) adr_f.set_save_manager(sm_f) # 兑付后停点快照落盘承载(与真机组合根同构) adr_f.bonus_choice_presented.connect(func(p): got_cands["cands"] = (p.get("candidates", []) as Array).duplicate()) - check(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)") - check(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)") - check(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)") - check(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)") - check(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)") - check(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理") + Kit.check_quiet(adr_f.resume_adventure(bp_f, params_e), "P5 续玩受理(选择态分支)") + Kit.check_quiet(adr_f.runner() == null, "P5 续玩不重跑关卡(三选一停点直回,_start_current_level 未走)") + Kit.check_quiet(got_cands["cands"] == cands_e, "P5 续玩候选不重抽(逐位一致)") + Kit.check_quiet(int(adr_f._bonus_taken["attack"]) == 1, "P5 次数计数恢复一致(牌库剔除态续接)") + Kit.check_quiet(not adr_f.continue_to_next_level(), "P5 未兑付推进拒绝(续玩态零自动推进同契约)") + Kit.check_quiet(adr_f.pick_bonus(String(cands_e[0])), "P5 点选兑付受理") var picked_e: String = String(adr_f._bonus_result.get("picked", "")) if adr_f._bonus_taken.has(picked_e): - check(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1), + Kit.check_quiet(int(adr_f._bonus_taken[picked_e]) == (2 if picked_e == "attack" else 1), "P5 兑付计数一致(%s——预置 1 + 本次兑付)" % picked_e) - check(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)") + Kit.check_quiet(sm_f.has_breakpoint(), "P5 兑付后停点快照在档(已兑付待推进态可中断)") var bp_g: Dictionary = sm_f.breakpoint_snapshot() var adr_g = AdventureRunnerScript.new(db, load("res://core/rng_service.gd").new()) adr_g.set_save_manager(sm_f) # 已兑付态续玩的后续快照同承载 var got_result := {"r": {}} adr_g.bonus_resolved.connect(func(r): got_result["r"] = r.duplicate()) - check(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理") - check(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)") - check(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)") + Kit.check_quiet(adr_g.resume_adventure(bp_g, params_e), "P5 已兑付态续玩受理") + Kit.check_quiet(String(got_result["r"].get("picked", "")) == picked_e, "P5 已兑付不重付(resolved 重发同件)") + Kit.check_quiet(adr_g.continue_to_next_level(), "P5 已兑付续玩后推进受理(下一关开始)") _clear_disk() - if _problems.is_empty(): - print("[breakpoint_resume_test] PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿") - quit(0) - else: - for p in _problems: - push_error("[breakpoint_resume_test] FAIL — " + p) - print("[breakpoint_resume_test] FAIL — " + p) - quit(1) + quit(Kit.finish("breakpoint_resume_test", "PASS — 断点往返/续玩确定性/幂等/退出清断点 全绿")) func _board_fingerprint(b) -> String: diff --git a/game/tests/unit/data_config/aux_target_violation_test.gd b/game/tests/unit/data_config/aux_target_violation_test.gd index f06c0251..4f4fa08d 100644 --- a/game/tests/unit/data_config/aux_target_violation_test.gd +++ b/game/tests/unit/data_config/aux_target_violation_test.gd @@ -7,27 +7,12 @@ extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:BAD 表目录夹具共享(copy_tables) const SRC_DIR := "res://data/tables" const BAD_DIR := "user://aux_fixture_target_violation" -func _copy_tables() -> bool: - DirAccess.make_dir_recursive_absolute(BAD_DIR) - var dir := DirAccess.open(SRC_DIR) - if dir == null: - return false - dir.list_dir_begin() - var f := dir.get_next() - while f != "": - if not dir.current_is_dir(): - var err := dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f) - if err != OK: - return false - f = dir.get_next() - return true - - func _inject_violation() -> bool: var path := BAD_DIR + "/aux_items.tres" var f := FileAccess.open(path, FileAccess.READ) @@ -58,7 +43,7 @@ func _inject_violation() -> bool: func _init() -> void: - if not _copy_tables() or not _inject_violation(): + if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation(): push_error("[aux_target_violation_test] 夹具目录准备失败") quit(2) return diff --git a/game/tests/unit/data_config/config_db_test.gd b/game/tests/unit/data_config/config_db_test.gd index 5362bd42..bdf4c2bc 100644 --- a/game/tests/unit/data_config/config_db_test.gd +++ b/game/tests/unit/data_config/config_db_test.gd @@ -5,16 +5,14 @@ extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") - -var _failures := 0 -var _checks := 0 +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 func _init() -> void: var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) - _check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0) - _check("is_ready 通道随加载置位", db.is_ready) + Kit.check("全部表加载 + 校验通过(含跨表护栏零 error 诊断)", load_ok and db.diagnostics_count("error") == 0) + Kit.check("is_ready 通道随加载置位", db.is_ready) if not load_ok: for d in db.diagnostics(): if d.severity == "error": @@ -25,21 +23,7 @@ func _init() -> void: _test_shop_accessors(db) _test_meta_accessors(db) _test_manifest_dir_sync(db) - if _failures > 0: - push_error("[config_db_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[config_db_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + quit(Kit.finish("config_db_test")) # ---- 进度坐标 ceil 解析(R1:向上下取整保证偏难;越最大行 clamp 取最大值 + 警告去重)---- @@ -67,34 +51,34 @@ func _test_progress_resolution(db) -> void: # 精确命中:请求 = 表内现存 (L,D) → 原行返回(字段逐位相等) var probe_row: Dictionary = bg_rows[0] var got: Dictionary = db.resolve_board_gen(int(probe_row.level_index), int(probe_row.node_depth)) - _check("进度解析精确命中返回原行(board_gen 首行)", + Kit.check("进度解析精确命中返回原行(board_gen 首行)", int(got.get("level_index", -99)) == int(probe_row.level_index) and int(got.get("node_depth", -99)) == int(probe_row.node_depth) and String(got.get("board_type", "")) == String(probe_row.get("board_type", ""))) # enemy 行集同通道:同 (L,D) 精确行存在(关卡节点地图 前置——battle 节点双表精确行契约) var en_got: Dictionary = db.resolve_enemy_rowset(int(probe_row.level_index), int(probe_row.node_depth)) - _check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty()) + Kit.check("enemy 行集同坐标解析非空(双表精确行契约)", not en_got.is_empty()) # 深度 clamp:请求深度超最大行 → 取该关最大深度行 + 警告诊断一次(去重) db.reset_diagnostics() var deep: Dictionary = db.resolve_board_gen(min_l, int(min_depths[-1]) + 1) - _check("深度越最大行 clamp 取该关最大深度行", + Kit.check("深度越最大行 clamp 取该关最大深度行", int(deep.get("level_index", -99)) == min_l and int(deep.get("node_depth", -99)) == int(min_depths[-1])) - _check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1) + Kit.check("clamp 解析发警告诊断(resolve_depth_clamp)", db.diagnostics_count("warning") >= 1) var found_clamp := false for d in db.diagnostics(): if d.category == "resolve_depth_clamp": found_clamp = true - _check("警告类别 = resolve_depth_clamp", found_clamp) + Kit.check("警告类别 = resolve_depth_clamp", found_clamp) var warn_before: int = db.diagnostics_count("warning") db.resolve_board_gen(min_l, int(min_depths[-1]) + 1) - _check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before) + Kit.check("同键重复请求警告去重(四元组注册表)", db.diagnostics_count("warning") == warn_before) # 关卡 clamp:请求关卡超最大行 → 取最大关卡行 db.reset_diagnostics() var high: Dictionary = db.resolve_board_gen(max_l + 1, 0) - _check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l) + Kit.check("关卡越最大行 clamp 取最大关卡行", int(high.get("level_index", -99)) == max_l) # ---- 变异池门控(unlock_level 闸 + ∅ board_types 通配)---- @@ -114,12 +98,12 @@ func _test_variant_pool_gate(db) -> void: var tag: String = String(gated_row.board_types[0]) var before: Array = db.variant_pool(u - 1, tag) var after: Array = db.variant_pool(u, tag) - _check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row)) - _check("变异池 unlock 当级含该行", _rows_contain(after, gated_row)) + Kit.check("变异池 unlock 前不含未解锁行(level=unlock−1)", not _rows_contain(before, gated_row)) + Kit.check("变异池 unlock 当级含该行", _rows_contain(after, gated_row)) if not wildcard_row.is_empty(): var u2: int = int(wildcard_row.unlock_level) var any_pool: Array = db.variant_pool(u2, "nonexistent_board_type_tag") - _check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row)) + Kit.check("∅ board_types 行通配(任一 board_type 命中)", _rows_contain(any_pool, wildcard_row)) func _rows_contain(pool: Array, row: Dictionary) -> bool: @@ -134,26 +118,26 @@ func _rows_contain(pool: Array, row: Dictionary) -> bool: func _test_shop_accessors(db) -> void: var products: Array = db.shop_products() if products.is_empty(): - _check("ShopTable products 行族非空(表已落籍前提)", false) + Kit.check("ShopTable products 行族非空(表已落籍前提)", false) return - _check("ShopTable products 行族非空", true) + Kit.check("ShopTable products 行族非空", true) var first: Dictionary = products[0] var got: Dictionary = db.shop_product_row(String(first.product_id)) - _check("shop_product_row 按 product_id 精确命中首行", got == first) - _check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty()) + Kit.check("shop_product_row 按 product_id 精确命中首行", got == first) + Kit.check("shop_product_row 未知 id 返回空字典(容错通道)", db.shop_product_row("no_such_product_id__").is_empty()) var tier_rows: Array = db._shop_rows("gp_drops") if not tier_rows.is_empty(): var tier0: String = String(tier_rows[0].tier) - _check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty()) + Kit.check("shop_gp_drop_row 按 tier 命中", not db.shop_gp_drop_row(tier0).is_empty()) var bonus_rows: Array = db._shop_rows("level_gp_bonus") if not bonus_rows.is_empty(): var blv: int = int(bonus_rows[0].level_index) - _check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus)) - _check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0) + Kit.check("shop_level_gp_bonus 按关卡序号命中行值", db.shop_level_gp_bonus(blv) == int(bonus_rows[0].bonus)) + Kit.check("shop_level_gp_bonus 未登记关卡返回 0(兜底通道)", db.shop_level_gp_bonus(-1) == 0) var price_rows: Array = db._shop_rows("price_level_bonus") if not price_rows.is_empty(): var plv: int = int(price_rows[0].level_index) - _check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus)) + Kit.check("shop_price_level_bonus 按关卡序号命中行值", db.shop_price_level_bonus(plv) == int(price_rows[0].bonus)) # ---- MetaTable 消费面 ---- @@ -161,19 +145,19 @@ func _test_shop_accessors(db) -> void: func _test_meta_accessors(db) -> void: var forge_rows: Array = db._meta_rows("forge_products") if forge_rows.is_empty(): - _check("MetaTable forge_products 行族非空", false) + Kit.check("MetaTable forge_products 行族非空", false) else: var attr0: String = String(forge_rows[0].attr) - _check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0]) - _check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty()) + Kit.check("meta_forge_row 按 attr 命中首行", db.meta_forge_row(attr0) == forge_rows[0]) + Kit.check("meta_forge_row 未知 attr 返回空字典", db.meta_forge_row("no_such_attr__").is_empty()) var key_rows: Array = db._meta_rows("boss_keystone") if not key_rows.is_empty(): var klv: int = int(key_rows[0].level_index) - _check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0]) + Kit.check("meta_boss_keystone_row 按关卡序号命中", db.meta_boss_keystone_row(klv) == key_rows[0]) var conv_rows: Array = db._meta_rows("conversion") if not conv_rows.is_empty(): var cattr: String = String(conv_rows[0].attr) - _check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0]) + Kit.check("meta_conversion_row 按 attr 命中", db.meta_conversion_row(cattr) == conv_rows[0]) # ---- TABLE_MANIFEST ↔ data/tables 目录同步(各系统→D2 单点契约的登记面)---- @@ -195,4 +179,4 @@ func _test_manifest_dir_sync(db) -> void: for key in manifest.keys(): if not disk_files.has(String(manifest[key])): synced = false - _check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced) + Kit.check("TABLE_MANIFEST 与 data/tables 目录逐文件同步(无缺登/无悬登)", synced) diff --git a/game/tests/unit/data_config/reward_dangling_aux_test.gd b/game/tests/unit/data_config/reward_dangling_aux_test.gd index 7490d229..7d110512 100644 --- a/game/tests/unit/data_config/reward_dangling_aux_test.gd +++ b/game/tests/unit/data_config/reward_dangling_aux_test.gd @@ -7,26 +7,12 @@ extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:BAD 表目录夹具共享(copy_tables) const SRC_DIR := "res://data/tables" const BAD_DIR := "user://aux_fixture_dangling_aux" -func _copy_tables() -> bool: - DirAccess.make_dir_recursive_absolute(BAD_DIR) - var dir := DirAccess.open(SRC_DIR) - if dir == null: - return false - dir.list_dir_begin() - var f := dir.get_next() - while f != "": - if not dir.current_is_dir(): - if dir.copy(SRC_DIR + "/" + f, BAD_DIR + "/" + f) != OK: - return false - f = dir.get_next() - return true - - func _inject_violation() -> bool: var path := BAD_DIR + "/reward.tres" var f := FileAccess.open(path, FileAccess.READ) @@ -50,7 +36,7 @@ func _inject_violation() -> bool: func _init() -> void: - if not _copy_tables() or not _inject_violation(): + if not Kit.copy_tables(SRC_DIR, BAD_DIR) or not _inject_violation(): push_error("[reward_dangling_aux_test] 夹具目录准备失败") quit(2) return diff --git a/game/tests/unit/data_config/rng_service_test.gd b/game/tests/unit/data_config/rng_service_test.gd index 14861c97..f04939a5 100644 --- a/game/tests/unit/data_config/rng_service_test.gd +++ b/game/tests/unit/data_config/rng_service_test.gd @@ -6,33 +6,17 @@ extends SceneTree const RngServiceScript := preload("res://core/rng_service.gd") +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 const TEST_SEED := 20260928 # 夹具种子(任意固定值;语义 = 可复现性,非玩法数值) -var _failures := 0 -var _checks := 0 - func _init() -> void: _test_derive_determinism() _test_counter_ledger() _test_stream_draw_determinism() _test_negative_seed_normalization() - if _failures > 0: - push_error("[rng_service_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[rng_service_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + quit(Kit.finish("rng_service_test")) func _fresh_service(): @@ -49,11 +33,11 @@ func _test_derive_determinism() -> void: for scope in range(8): if a.derive_at(scope) != b.derive_at(scope): same = false - _check("同种子两实例 derive_at(0..7) 逐位一致", same) + Kit.check("同种子两实例 derive_at(0..7) 逐位一致", same) var a2 = _fresh_service() var before: int = a2.counter() a2.derive_at(0) - _check("derive_at 为纯函数不推计数器", a2.counter() == before) + Kit.check("derive_at 为纯函数不推计数器", a2.counter() == before) # 全局取数计数器:唯一账本——open_stream 逐次 +1(scope 单调) @@ -64,13 +48,13 @@ func _test_counter_ledger() -> void: var c1: int = rng.counter() rng.open_stream("armor_roll") var c2: int = rng.counter() - _check("open_stream 逐次推进全局计数器(+1/次)", c1 == c0 + 1 and c2 == c0 + 2) + Kit.check("open_stream 逐次推进全局计数器(+1/次)", c1 == c0 + 1 and c2 == c0 + 2) # 用途标签只进事件日志不参与派生——同 scope 同派生种子(ADR-0007 裁丙口径) var rng_x = _fresh_service() var rng_y = _fresh_service() var s1: int = int(rng_x.open_stream("board_gen").seed) var s2: int = int(rng_y.open_stream("reward_roll").seed) - _check("同 scope 异用途标签派生种子一致(标签不参与派生)", s1 == s2) + Kit.check("同 scope 异用途标签派生种子一致(标签不参与派生)", s1 == s2) # 同种子两实例同标签流的实际取数序列逐位一致(回放确定性契约——存档快照=主种子+计数器的前提) @@ -84,7 +68,7 @@ func _test_stream_draw_determinism() -> void: var rb: int = b.open_stream("armor_roll").randi_range(1, faces) if ra != rb: same = false - _check("同种子两实例同标签流取数 12 次逐位一致", same) + Kit.check("同种子两实例同标签流取数 12 次逐位一致", same) # 负输入 abs 归一化(警告通道)——|−s| 与 s 同派生 @@ -92,4 +76,4 @@ func _test_negative_seed_normalization() -> void: var pos = _fresh_service() var neg = RngServiceScript.new() neg.set_master_seed(-TEST_SEED) - _check("负种子 abs 归一化后派生与正种子一致", pos.derive_at(0) == neg.derive_at(0)) + Kit.check("负种子 abs 归一化后派生与正种子一致", pos.derive_at(0) == neg.derive_at(0)) diff --git a/game/tests/unit/example_test.gd b/game/tests/unit/example_test.gd index df553db7..455d059a 100644 --- a/game/tests/unit/example_test.gd +++ b/game/tests/unit/example_test.gd @@ -7,23 +7,12 @@ # 第一个真实测试:复制本文件为 tests/unit//__test.gd 并替换占位断言 extends SceneTree -func _init() -> void: - var failures := 0 - failures += _check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol()) - failures += _check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed()) - if failures > 0: - push_error("[example_test] %d 项失败" % failures) - quit(1) - return - print("[example_test] all green") - quit(0) +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:check/汇总退出样板单点 -func _check(desc: String, ok: bool) -> int: - if not ok: - push_error("[example_test] FAIL: " + desc) - return 1 - print("[example_test] PASS: " + desc) - return 0 +func _init() -> void: + Kit.check("headless SceneTree 协议可用(quit(code) 通道)", _test_headless_protocol()) + Kit.check("命名规范可执行:test_[scenario]_[expected]", _test_naming_convention_followed()) + quit(Kit.finish("example_test")) func _test_headless_protocol() -> bool: # 占位断言:验证脚本本身在 SceneTree 上下文可执行。 diff --git a/game/tests/unit/presentation/reward_overlay_test.gd b/game/tests/unit/presentation/reward_overlay_test.gd index e4fc85f7..a9960934 100644 --- a/game/tests/unit/presentation/reward_overlay_test.gd +++ b/game/tests/unit/presentation/reward_overlay_test.gd @@ -3,6 +3,17 @@ extends SceneTree # 原守 node_overlay.show_reward_items;奖励节点三选一与通关三选一合并共用 BonusPickOverlay 后, # 本套件改守共用面的「功能存在 ≠ 功能可达」盲区:选择外发/强制确认两段式/替换子面板两路/结果态。 # 运行:godot --headless --path game --script res://tests/unit/presentation/reward_overlay_test.gd(expect 0) +# T6 收口批:断言走 test_kit 计数(Kit.check)+ 失败即停早退协议保留(顺序状态机——坏态下后继断言无效)。 +const Kit := preload("res://tests/unit/test_kit.gd") + +# 断言 + 失败即停(原直写 print FAIL + quit(1) + return 样板单点化——早退语义逐位保持) +func _req(desc: String, ok: bool) -> bool: + Kit.check(desc, ok) + if not ok: + quit(1) + return false + return true + func _initialize() -> void: call_deferred("_run") @@ -25,17 +36,15 @@ func _run() -> void: for c in ov._choice_box.get_children(): if c is Button: btns += 1 - if ov.visible != true or btns != 3 or ov._confirm_btn.visible or ov._continue_btn.visible: - print("[reward_verify] FAIL — 选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns) - quit(1) + if not _req("选择态应三卡呈现且确认/继续隐藏(btns=%d)" % btns, + ov.visible and btns == 3 and not ov._confirm_btn.visible and not ov._continue_btn.visible): return # ② 点选→高亮→确认外发(真实按钮路径);确认前改选以最后点选为准 ov.press_card(0) ov.press_card(1) ov.press_confirm() - if ov._confirm_btn.visible != true or picked.size() != 1 or String(picked[0]) != "gp": - print("[reward_verify] FAIL — 点选确认未外发 pick_confirmed(gp)(%s)" % str(picked)) - quit(1) + if not _req("点选确认未外发 pick_confirmed(gp)(%s)" % str(picked), + ov._confirm_btn.visible and picked.size() == 1 and String(picked[0]) == "gp"): return # ③ 替换子面板:丢旧收新与放弃新件两路外发(通关渠道=携带序号 / 节点渠道=库存句柄——tag 直通) ov.open_replace([{"index": 0, "item_id": "probe_dust", "display_name": "显影尘"}], "爆破工兵包", 1, 1) @@ -43,33 +52,26 @@ func _run() -> void: for c in ov._replace_box.get_children(): if c is Button: replace_btns += 1 - if replace_btns != 2 or ov._choice_box.visible: - print("[reward_verify] FAIL — 替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns) - quit(1) + if not _req("替换面板应 2 钮(N+1 选 N+放弃)且三卡隐藏(btns=%d)" % replace_btns, + replace_btns == 2 and not ov._choice_box.visible): return + var replaced_first := false for c in ov._replace_box.get_children(): if c is Button: c.pressed.emit() + replaced_first = true break - if replaced.size() != 1 or int(replaced[0]) != 0: - print("[reward_verify] FAIL — 丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced)) - quit(1) + if not _req("丢旧收新未外发 replace_chosen(0)(%s)" % str(replaced), + replaced_first and replaced.size() == 1 and int(replaced[0]) == 0): return # ④ 结果态:继续钮点亮、唯一出口 ov.show_result("已选择:攻击力提升 · 攻击 +1") - if ov._continue_btn.visible != true or ov._replace_box.visible: - print("[reward_verify] FAIL — 结果态应点亮继续且收替换面板") - quit(1) + if not _req("结果态应点亮继续且收替换面板", ov._continue_btn.visible and not ov._replace_box.visible): return ov.press_continue() - if continued.size() != 1: - print("[reward_verify] FAIL — 继续未外发 continue_pressed") - quit(1) + if not _req("继续未外发 continue_pressed", continued.size() == 1): return ov.close_pick() - if ov.visible: - print("[reward_verify] FAIL — close_pick 应收屏") - quit(1) + if not _req("close_pick 应收屏", not ov.visible): return - print("[reward_verify] PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过") - quit(0) + quit(Kit.finish("reward_verify", "PASS — 三卡两段式/确认外发/替换两路/结果态/收屏 全过")) diff --git a/game/tests/unit/presentation/terrain_tile_sets_test.gd b/game/tests/unit/presentation/terrain_tile_sets_test.gd index ba6f4c81..e4ac400b 100644 --- a/game/tests/unit/presentation/terrain_tile_sets_test.gd +++ b/game/tests/unit/presentation/terrain_tile_sets_test.gd @@ -9,9 +9,7 @@ extends SceneTree const ConfigDBScript := preload("res://core/config_db.gd") const BoardViewScript := preload("res://presentation/views/board_view.gd") - -var _failures := 0 -var _checks := 0 +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 func _init() -> void: @@ -23,7 +21,7 @@ func _init() -> void: return var db = ConfigDBScript.new() var load_ok: bool = db.load_tables("res://data/tables", true) - _check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) + Kit.check("前置:表加载零 error", load_ok and db.diagnostics_count("error") == 0) if not load_ok: quit(1) return @@ -33,25 +31,11 @@ func _init() -> void: var bt := String(row.get("board_type", "")) if bt != "" and not types.has(bt): types.append(bt) - _check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0) + Kit.check("前置:board_gen 词表非空(%d 型)" % types.size(), types.size() > 0) _test_set_completeness(board_dir, types) _test_hue_domains(board_dir) _test_digit_ink_modes(board_dir, types) - if _failures > 0: - push_error("[terrain_tile_sets_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[terrain_tile_sets_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + quit(Kit.finish("terrain_tile_sets_test")) # 套完整性:词表每个 board_type 的 16 墙 + 5 地面变体全部在盘(缺位 → BoardView 整批回落 dungeon 的 @@ -69,7 +53,7 @@ func _test_set_completeness(board_dir: String, types: Array) -> void: var pf := board_dir.path_join("terrain_floor_%s_%d.png" % [String(bt), i]) if not FileAccess.file_exists(pf): missing.append("floor_%d" % i) - _check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty()) + Kit.check("图型 %s 瓦片套完整(16 墙 + %d 地面,缺=%s)" % [String(bt), variants, str(missing)], missing.is_empty()) # 色相域抽检:wall_center 上半幅(墙顶面)与 floor_1 全幅的均值色相/明度 @@ -85,17 +69,17 @@ func _test_hue_domains(board_dir: String) -> void: var wall := _load_stats(board_dir, "terrain_wall_center_%s.png" % String(bt), true) var floor_img := _load_stats(board_dir, "terrain_floor_%s_1.png" % String(bt), false) if wall.is_empty() or floor_img.is_empty(): - _check("图型 %s 抽检样本可读" % String(bt), false) + Kit.check("图型 %s 抽检样本可读" % String(bt), false) continue wall_v[bt] = float(wall["v"]) floor_v[bt] = float(floor_img["v"]) var band: Array = bands[bt] - _check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])], + Kit.check("图型 %s 墙顶均值色相在带 %s(实测 %.3f)" % [String(bt), str(band), float(wall["h"])], float(wall["h"]) >= float(band[0]) and float(wall["h"]) <= float(band[1])) - _check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]], + Kit.check("图型 %s 图底分离(墙顶明度 %.3f > 地面 %.3f)" % [String(bt), wall_v[bt], floor_v[bt]], wall_v[bt] > floor_v[bt]) if wall_v.has("underdark") and wall_v.has("deep_underdark"): - _check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"])) + Kit.check("deep_underdark 墙顶更暗于 underdark(更深蓝黑方向)", float(wall_v["deep_underdark"]) < float(wall_v["underdark"])) # 均值读数:hue 取饱和度 > 0.05 的像素(墨线/灰黑不影响色相均值);v 取全幅均值 @@ -150,26 +134,26 @@ func _test_digit_ink_modes(board_dir: String, types: Array) -> void: var tex: Texture2D = ImageTexture.create_from_image(Image.load_from_file(p)) BoardViewScript._normalize_floor_luma(tex, 0.10, lumas) if not readable: - _check("图型 %s 数字墨族断言样本可读" % String(bt), false) + Kit.check("图型 %s 数字墨族断言样本可读" % String(bt), false) continue var mode: Dictionary = BoardViewScript._digit_ink_mode(lumas) var plate: bool = bool(mode["plate"]) var dark: bool = bool(mode["dark"]) var mode_name := "色牌垫底" if plate else ("深墨族直画" if dark else "亮墨族直画") if plate: - _check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [ + Kit.check("图型 %s 双峰纹理落牌(亮族 p90 %.2f/均值 %.2f,深族 p90 %.2f/均值 %.2f 均未过门)" % [ String(bt), float(mode["cr_p90_bright"]), float(mode["cr_mean_bright"]), float(mode["cr_p90_dark"]), float(mode["cr_mean_dark"])], float(mode["cr_mean_dark"]) < float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK)) else: var cr_p90 := float(mode["cr_p90_bright"]) if not dark else float(mode["cr_p90_dark"]) var cr_mean := float(mode["cr_mean_bright"]) if not dark else float(mode["cr_mean_dark"]) - _check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, + Kit.check("图型 %s 所选%s 对 p90 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, float(BoardViewScript.DIGIT_INK_CR_P90), cr_p90], cr_p90 >= float(BoardViewScript.DIGIT_INK_CR_P90)) var mean_gate := float(BoardViewScript.DIGIT_INK_CR_MEAN_BRIGHT) if not dark else float(BoardViewScript.DIGIT_INK_CR_MEAN_DARK) - _check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean], + Kit.check("图型 %s 所选%s 对均值 ≥%.1f(实测 %.2f)" % [String(bt), mode_name, mean_gate, cr_mean], cr_mean >= mean_gate) if expect_plate.has(bt): - _check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt), + Kit.check("图型 %s 呈现模式期望=%s(实测=%s)" % [String(bt), "色牌" if bool(expect_plate[bt]) else "直画", mode_name], plate == bool(expect_plate[bt])) diff --git a/game/tests/unit/presentation/ui_font_asset_test.gd b/game/tests/unit/presentation/ui_font_asset_test.gd index eba1ec4c..102cc88e 100644 --- a/game/tests/unit/presentation/ui_font_asset_test.gd +++ b/game/tests/unit/presentation/ui_font_asset_test.gd @@ -7,38 +7,22 @@ extends SceneTree const UiKitScript := preload("res://presentation/views/ui_kit.gd") - -var _failures := 0 -var _checks := 0 +const Kit := preload("res://tests/unit/test_kit.gd") # T6 收口:_check/汇总退出样板单点 func _init() -> void: var path: String = UiKitScript.FONT_UI var f: Font = UiKitScript.load_font(path) - _check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null) + Kit.check("随包字体守卫装载非空(res://assets/fonts/ui_font_fusion_pixel_zh_hans.ttf)", f != null) if f != null: - _check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \ + Kit.check("中文字形在集(攻/雷/旗/封)", f.has_char("攻".unicode_at(0)) and f.has_char("雷".unicode_at(0)) \ and f.has_char("旗".unicode_at(0)) and f.has_char("封".unicode_at(0))) - _check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0))) + Kit.check("ASCII/数字字形在集(A/0/%)", f.has_char("A".unicode_at(0)) and f.has_char("0".unicode_at(0)) and f.has_char("%".unicode_at(0))) var sz := 18 var w_cn: float = f.get_string_size("战斗胜利", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x var w_label: float = f.get_string_size("攻击 2", HORIZONTAL_ALIGNMENT_LEFT, -1, sz).x - _check("中文采样文本宽度 > 0(C6 headless 判据)", w_cn > 0.0 and w_label > 0.0) + Kit.check("中文采样文本宽度 > 0(C6 headless 判据)", w_cn > 0.0 and w_label > 0.0) # 注:不设「私用区 has_char=false」负探针——fusion pixel 实测覆盖 PUA 码位,负向覆盖非契约面(宁缺勿错) else: - _check("字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)", false) - if _failures > 0: - push_error("[ui_font_asset_test] %d/%d 项失败" % [_failures, _checks]) - quit(1) - return - print("[ui_font_asset_test] all green (%d checks)" % _checks) - quit(0) - - -func _check(desc: String, ok: bool) -> void: - _checks += 1 - if ok: - print(" [PASS] " + desc) - else: - _failures += 1 - push_error(" [FAIL] " + desc) + Kit.check("字体缺失时后续断言跳过(缺位=回归,此行仅占位计数)", false) + quit(Kit.finish("ui_font_asset_test")) diff --git a/game/tests/unit/test_kit.gd b/game/tests/unit/test_kit.gd new file mode 100644 index 00000000..e8b81bb8 --- /dev/null +++ b/game/tests/unit/test_kit.gd @@ -0,0 +1,72 @@ +# tests/unit/test_kit.gd — 单元测试协议样板单点(T6 样板收口批 2026-10-02) +# 收口范围:11 份套件的 _check/_problems 样板(计数器 + PASS/FAIL 行 + 汇总退出)+ +# reward_dangling_aux/aux_target_violation 两致命类用例各自复制的 BAD 表目录夹具。 +# 用法(--script 模式无 autoload/全局标识符——禁 class_name 裸引用,使用方 preload): +# const Kit := preload("res://tests/unit/test_kit.gd") +# Kit.check("断言描述", cond) # 计数 + 即时 PASS/FAIL 行 +# Kit.check_quiet(cond, "描述") # 计数 + 失败收集(finish 时统一汇总——原 _problems 风格,原参序) +# quit(Kit.finish("suite_name")) # 汇总输出 + 返回退出码(0 全绿 / 1 有败) +# 计数器为 static(套件 = 独立 Godot 子进程恰一份生命周期,见 run_all.gd 执行形态); +# 单进程跑多套件须先 Kit.reset()。 +extends RefCounted + +static var checks: int = 0 +static var failures: int = 0 +static var problems: Array = [] + + +# 主通道:计数 + 即时 PASS/FAIL 行(config_db/rng_service/damage_math/system_contracts/ +# damage_controller/ui_font_asset/terrain_tile_sets 等套件原 _check 同款输出,描述在前) +static func check(desc: String, ok: bool) -> void: + checks += 1 + if ok: + print(" [PASS] " + desc) + else: + failures += 1 + push_error(" [FAIL] " + desc) + + +# 静默收集通道:失败只记入 problems(即时零刷屏),finish 统一汇总。 +# 参序沿原函数式套件 aux_items/breakpoint_resume 的 check(cond, msg)——条件在前,与主通道相反。 +static func check_quiet(ok: bool, desc: String) -> void: + checks += 1 + if not ok: + failures += 1 + problems.append(desc) + + +# 汇总退出:返回退出码(调用方 `quit(Kit.finish(...))`)。pass_line = 成功尾行覆盖 +#(保留原套件 PASS 摘要文案;缺省 = "all green (N checks)" 计数口径)。 +# 失败路径:push_error 计数汇总 + problems 逐条 FAIL 行(print + push_error 双通道——原两风格并档)。 +static func finish(suite: String, pass_line: String = "") -> int: + if failures > 0: + push_error("[%s] %d/%d 项失败" % [suite, failures, checks]) + for p in problems: + print("[%s] FAIL — %s" % [suite, p]) + push_error("[%s] FAIL — %s" % [suite, p]) + return 1 + print("[%s] %s" % [suite, pass_line if pass_line != "" else "all green (%d checks)" % checks]) + return 0 + + +# BAD 表夹具共享(原 reward_dangling_aux/aux_target_violation 各自复制的 _copy_tables): +# 整目录拷贝 src_dir → bad_dir(目录已建)。BAD_DIR 常量仍由各用例自持(名字互异——独立进程并行不互踩)。 +static func copy_tables(src_dir: String, bad_dir: String) -> bool: + DirAccess.make_dir_recursive_absolute(bad_dir) + var dir := DirAccess.open(src_dir) + if dir == null: + return false + dir.list_dir_begin() + var f := dir.get_next() + while f != "": + if not dir.current_is_dir(): + if dir.copy(src_dir + "/" + f, bad_dir + "/" + f) != OK: + return false + f = dir.get_next() + return true + + +static func reset() -> void: + checks = 0 + failures = 0 + problems = [] diff --git a/game/tests/unit/test_kit.gd.uid b/game/tests/unit/test_kit.gd.uid new file mode 100644 index 00000000..27e686e6 --- /dev/null +++ b/game/tests/unit/test_kit.gd.uid @@ -0,0 +1 @@ +uid://dtestkit123456 From 5584feb6e5beedd2d2694b4c844bce16adb148d0 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 09:07:06 +0800 Subject: [PATCH 6/8] =?UTF-8?q?T4=20UiKit=E8=89=B2=E5=BD=A9=E9=AA=A8?= =?UTF-8?q?=E6=9E=B6=E6=94=B6=E5=8F=A3=E6=AE=B5:=20modal=5Fshell/modal=5Fd?= =?UTF-8?q?im=E6=94=B610=E5=A4=84overlay=E9=AA=A8=E6=9E=B6=EF=BC=88dim?= =?UTF-8?q?=E8=89=B2/min=5Fsize/panel=E5=8F=82=E6=95=B0=E5=8C=96=E5=AF=B9?= =?UTF-8?q?=E7=85=A7=E8=A1=A8=E5=9C=A8=E6=8A=A5=E5=91=8A=EF=BC=8C=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E7=A7=81=E6=9C=89=E4=BB=B6=E5=90=8C=E5=90=8D=E5=90=8C?= =?UTF-8?q?=E5=B1=82=E7=BA=A7=E4=BF=9D=E7=95=99=EF=BC=89+label=E5=8A=A0col?= =?UTF-8?q?or=E5=8F=82=E6=94=B6=E5=85=A8=E5=B1=82=E4=B8=A4=E8=A1=8C?= =?UTF-8?q?=E5=BC=8F+=E8=A3=B8=E8=89=B2=E6=94=B6=E5=8F=A3=EF=BC=88standee/?= =?UTF-8?q?camp/stage/level=5Fmap/board=5Fworld=20Color8=E2=86=92UiKit?= =?UTF-8?q?=E6=A1=A313=E8=89=B2+HEX=5FLOG=5F*/HEX=5FROUTE/SEED=2013?= =?UTF-8?q?=E6=A1=A3=E2=80=94=E2=80=94=E5=80=BC=E9=80=90=E5=AD=97=E8=8A=82?= =?UTF-8?q?=E4=B8=8D=E5=8F=98=EF=BC=89+=E6=9D=82=E5=B9=B6=E4=B8=83?= =?UTF-8?q?=E4=BB=B6=EF=BC=88UiKit.scaled=E6=94=B6=E5=8F=8C=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0/=E5=88=B0=E8=BE=BE=E6=8B=8D=E5=9D=97=E5=90=88?= =?UTF-8?q?=E5=B9=B6/=E7=82=B9=E9=98=B5=E5=90=88=E5=B9=B6/=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=B8=B8=E6=95=B0=E5=8D=95=E7=82=B9/=E7=93=A6?= =?UTF-8?q?=E7=89=87=E8=AF=8D=E8=A1=A8=E5=8D=95=E7=82=B9=E5=8C=96/?= =?UTF-8?q?=E5=8D=A0=E4=BD=8D=E6=96=87=E6=A1=88const/with=5Fbar=E5=88=A4?= =?UTF-8?q?=E6=8D=AE/bonus=E5=86=85=E8=81=94lambda=E6=94=B9=E5=85=B7?= =?UTF-8?q?=E5=90=8D=EF=BC=89=EF=BC=9BArmedConfirm=E4=B8=89=E5=A4=84?= =?UTF-8?q?=E8=AF=AD=E4=B9=89=E5=BC=82=E6=9E=84=E9=99=8D=E7=BA=A7=E8=AE=B0?= =?UTF-8?q?06=EF=BC=88=E4=B8=8D=E5=81=9A=E4=BA=86=E5=8D=8A=E5=A5=97?= =?UTF-8?q?=EF=BC=89=EF=BC=9B=E9=97=B8=3D=E5=86=B2=E7=83=9F636/0+run=5Fall?= =?UTF-8?q?=2015=E5=A5=97+=E5=85=AB=E6=8E=A2=E9=92=88=E5=85=A8=E7=BB=BF+?= =?UTF-8?q?=E7=86=B5=E5=A2=9E=E9=97=B8PASS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/presentation/battle_narrator.gd | 16 ++-- game/presentation/hud_presenter.gd | 2 +- game/presentation/main.gd | 14 ++-- game/presentation/ui_assembler.gd | 19 ++--- game/presentation/views/ap_bar_view.gd | 34 ++++----- game/presentation/views/battle_end_overlay.gd | 29 +++----- game/presentation/views/battle_log_view.gd | 30 ++++---- game/presentation/views/battle_stage_view.gd | 54 +++++++------- game/presentation/views/board_fx_view.gd | 3 +- game/presentation/views/board_view.gd | 23 ++---- game/presentation/views/board_world.gd | 2 +- game/presentation/views/bonus_pick_overlay.gd | 30 ++++---- game/presentation/views/camp_overlay_view.gd | 53 +++++-------- game/presentation/views/confirm_dialog.gd | 19 +---- .../views/hero_info_panel_view.gd | 7 +- game/presentation/views/legend_overlay.gd | 31 +++----- game/presentation/views/level_map_screen.gd | 42 +++++------ game/presentation/views/node_overlay.gd | 14 +--- game/presentation/views/settings_overlay.gd | 18 +---- game/presentation/views/settlement_fx_view.gd | 9 +-- .../views/settlement_review_overlay.gd | 17 +---- game/presentation/views/shop_overlay_view.gd | 23 ++---- game/presentation/views/standee_body.gd | 13 ++-- game/presentation/views/status_slot_row.gd | 3 +- game/presentation/views/toast_stack.gd | 3 +- game/presentation/views/tune_panel.gd | 26 ++----- game/presentation/views/ui_kit.gd | 74 ++++++++++++++++++- 27 files changed, 263 insertions(+), 345 deletions(-) diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index 13040dce..2bac84f1 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -6,24 +6,24 @@ extends RefCounted -# 节点进入横幅(棋盘日志——battle/boss 用;节点编号 = 行进器编号空间读面) +# 节点进入横幅(棋盘日志——battle/boss 用;节点编号 = 行进器编号空间读面;行色 = UiKit.HEX_* 语义档单点) static func node_banner(runner, node: Dictionary, t: String) -> String: - return "[color=#e0b860]── 节点 %d/%d · %s(编号 %d)──[/color]" % [ + return "[color=#%s]── 节点 %d/%d · %s(编号 %d)──[/color]" % [UiKit.HEX_LOG_GAIN, runner.node_index + 1, runner.nodes().size(), "Boss 房" if t == "boss" else "战斗", runner.node_id_of(node)] static func reward_line() -> String: - return "[color=#d0c090]关卡奖励点(节点内容待定——直通)[/color]" + return "[color=#%s]关卡奖励点(节点内容待定——直通)[/color]" % UiKit.HEX_ROUTE_NOTE static func rest_line(healed: int, hp: int, max_hp: int) -> String: - return "[color=#7cc47c]恢复点:回复 %d HP(→ %d/%d)[/color]" % [healed, hp, max_hp] + return "[color=#%s]恢复点:回复 %d HP(→ %d/%d)[/color]" % [UiKit.HEX_LOG_HEAL, healed, hp, max_hp] # 商店房行(GP+商店房实施批——到达即开店;购买/离开归玩家意图) static func shop_line(gp: int) -> String: - return "[color=#e0b860]商店房:金币 %d——购买即时生效,离开后选下一跳[/color]" % gp + return "[color=#%s]商店房:金币 %d——购买即时生效,离开后选下一跳[/color]" % [UiKit.HEX_LOG_GAIN, gp] # 选路口行(零自动推进停点——战斗胜利/兑付确认后出现) @@ -31,18 +31,18 @@ static func route_line(candidates: Array, runner) -> String: var names: Array[String] = [] for cand in candidates: names.append("节点 %d" % runner.node_id_of(cand)) - return "[color=#e0b860]── 岔路口:可选下一跳 %s ──[/color]" % " / ".join(names) + return "[color=#%s]── 岔路口:可选下一跳 %s ──[/color]" % [UiKit.HEX_LOG_GAIN, " / ".join(names)] # 关卡开场行(种子/加成口径注记——面板加成是否生效由调用方传入) static func level_open_line(level_display: String, seed_val: int, atk: int, max_hp: int, boosted: bool) -> String: - return "[color=#c8a860]%s · 种子 %d · 攻击 %d / 生命 %d%s[/color]" % [ + return "[color=#%s]%s · 种子 %d · 攻击 %d / 生命 %d%s[/color]" % [UiKit.HEX_SEED_NOTE, level_display, seed_val, atk, max_hp, "(含面板加成)" if boosted else ""] # 调试开场行 static func debug_open_line(seed_val: int, level: int, level_mode: bool) -> String: - return "[color=#c8a860]种子 %d · 遭遇 %d 层%s[/color]" % [ + return "[color=#%s]种子 %d · 遭遇 %d 层%s[/color]" % [UiKit.HEX_SEED_NOTE, seed_val, level, "(关卡模式 · HP 延续)" if level_mode else ""] diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index 6b926a05..6333a3eb 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -151,7 +151,7 @@ static func outcome_text(outcome: Dictionary) -> String: static func tune_sim_report(n: int, atk_pct: int, hp_pct: int, wins: int, dmg_all: Array, hp_left_win: Array, hp_boss: Array, rounds_all: Array, death_nodes: Dictionary, base_hp: int) -> String: var lines: Array[String] = [] lines.append("[b]模拟 N=%d(atk+%d%% / hp+%d%%)[/b]" % [n, atk_pct, hp_pct]) - lines.append("通关率 [color=#80d080]%.0f%%[/color](%d/%d)" % [100.0 * float(wins) / float(maxi(1, n)), wins, n]) + lines.append("通关率 [color=#%s]%.0f%%[/color](%d/%d)" % [UiKit.HEX_LOG_WIN, 100.0 * float(wins) / float(maxi(1, n)), wins, n]) lines.append("整关承伤:均 %.1f · p90 %.0f(基础 HP %d)" % [_mean(dmg_all), _p90(dmg_all), base_hp]) if not hp_left_win.is_empty(): lines.append("通关剩余 HP 均 %.1f · 进 Boss HP 均 %.1f" % [_mean(hp_left_win), _mean(hp_boss)]) diff --git a/game/presentation/main.gd b/game/presentation/main.gd index fc156228..87af7d65 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -268,8 +268,7 @@ func _build_ui() -> void: func _fatal_screen(msg: String) -> void: - var l := UiKit.label(msg, 20, true) - l.add_theme_color_override("font_color", UiKit.RED_ALERT) + var l := UiKit.label(msg, 20, true, UiKit.RED_ALERT) l.set_anchors_preset(Control.PRESET_CENTER) add_child(l) @@ -689,14 +688,11 @@ func _on_inter_next_pressed() -> void: # E4:加成生效值供给(debug_session 注入 Callable——每次从面板百分比现算,经 encounter 传参零表写; -# 原表写路径 _apply_hero_override/_scaled_override 已废除,双写叠加 6→18→54 整类隐患随之移除) +# 原表写路径 _apply_hero_override/_scaled_override 已废除,双写叠加 6→18→54 整类隐患随之移除; +# 合成式单点在 UiKit.scaled——与 TunePanel 生效值预览共用,本类不再持有同式私有实现) func _debug_hero_override() -> Dictionary: - return {"atk": _scaled(int(db.hero.atk), tune_panel.atk_pct()), - "max_hp": _scaled(int(db.hero.max_hp), tune_panel.hp_pct())} - - -func _scaled(base: int, pct: int) -> int: - return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0))) + return {"atk": UiKit.scaled(int(db.hero.atk), tune_panel.atk_pct()), + "max_hp": UiKit.scaled(int(db.hero.max_hp), tune_panel.hp_pct())} # 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index cc9a81e8..a790725f 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -114,18 +114,17 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary: bd.mouse_filter = Control.MOUSE_FILTER_IGNORE overlay.add_child(bd) var box := overlay_box(overlay) - var title := UiKit.label("Dungeon Sweeper", 34, true) - title.add_theme_color_override("font_color", UiKit.WARM) + var title := UiKit.label("Dungeon Sweeper", 34, true, UiKit.WARM) box.add_child(title) - var sub := UiKit.label("战斗屏战斗屏改造批 —— 扫雷的因,逐行兑现的果", 16, false) - sub.add_theme_color_override("font_color", UiKit.WARM) # 对比度修复(v3.5 批 B3——WARM_SOFT 对深石底实测 3.54:1→暖白 ≥6:1) + # 对比度修复(v3.5 批 B3——WARM_SOFT 对深石底实测 3.54:1→暖白 ≥6:1) + var sub := UiKit.label("战斗屏战斗屏改造批 —— 扫雷的因,逐行兑现的果", 16, false, UiKit.WARM) box.add_child(sub) box.add_child(HSeparator.new()) var seed_row := HBoxContainer.new() box.add_child(seed_row) - var seed_label := UiKit.label("随机种子:", 16, false) # 字号下限(v3.5 批 B3——正文 ≥16)+暗底显式暖白 - seed_label.add_theme_color_override("font_color", UiKit.WARM) + # 字号下限(v3.5 批 B3——正文 ≥16)+暗底显式暖白 + var seed_label := UiKit.label("随机种子:", 16, false, UiKit.WARM) seed_row.add_child(seed_label) var seed_edit := LineEdit.new() seed_edit.text = "42" @@ -153,9 +152,8 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary: # 玩法帮助文案(表值驱动——2026-09-16 AP 压速批遗留硬编码 8 随手修正;护甲等级掷点批同步"减伤"→"护甲强化") var help_text := "玩法:每轮 %d AP——挖开(1AP)/插旗(1AP)/移动(免费)/连锁(免费)。\n每个非封锁格都藏一种格面资源(增伤/护甲强化/回血/爆裂/削弱)。开安全格、标中雷 = 正向兑付;\n开雷格、标错安全格 = 负向兑付——全部在轮末结算逐行兑现;吃掷点的基础攻击按护甲等级判定,无 miss、最低保底 1 点。\n停手:剩余 AP 转存 RAP;弃图刷新:RAP−%d 当场换图继续挖(每轮剩 %d 次)。\n敌人全灭胜、主角倒下负。悬停可看三态光标与可挖域微光。" % [ int(db.action_economy.ap_per_round), int(db.action_economy.rap_refresh_cost), int(db.action_economy.refresh_cap_per_round)] - var help := UiKit.label(help_text, 16, false) # 字号下限(v3.5 批 B3——正文 ≥16) - # 对比度修复(v3.5 批 B3——WARM_FAINT 对深石底实测 2.58:1→暖白 ≥6:1;暗底双色墨制) - help.add_theme_color_override("font_color", UiKit.WARM) + # 字号下限(v3.5 批 B3——正文 ≥16);对比度修复(v3.5 批 B3——WARM_FAINT 对深石底实测 2.58:1→暖白 ≥6:1;暗底双色墨制) + var help := UiKit.label(help_text, 16, false, UiKit.WARM) box.add_child(help) return {"overlay": overlay, "box": box, "seed_edit": seed_edit, @@ -167,8 +165,7 @@ static func build_start_overlay(host: Control, db = null) -> Dictionary: static func build_inter_overlay(host: Control) -> Dictionary: var overlay := overlay_bg(host, Color(0, 0, 0, 0.6)) var box := overlay_box(overlay) - var label := UiKit.label("", 24, true) - label.add_theme_color_override("font_color", UiKit.WARM) + var label := UiKit.label("", 24, true, UiKit.WARM) box.add_child(label) var next_btn := UiKit.button("进入下一层") box.add_child(next_btn) diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index f6ff1a42..c4afab2c 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -71,8 +71,7 @@ func _init() -> void: _rap_label = UiKit.label("RAP 0", 14, true) _rap_label.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(_rap_label) - _rap_feedback = UiKit.label("", 11, false) - _rap_feedback.add_theme_color_override("font_color", UiKit.STONE_FAINT) + _rap_feedback = UiKit.label("", 11, false, UiKit.STONE_FAINT) _rap_feedback.size_flags_vertical = Control.SIZE_SHRINK_CENTER row.add_child(_rap_feedback) @@ -81,8 +80,7 @@ func _init() -> void: _confirm_row.add_theme_constant_override("separation", 4) _confirm_row.visible = false row.add_child(_confirm_row) - _confirm_lab = UiKit.label("RAP−%d · 旧板旗作废" % refresh_cost, 14, true) - _confirm_lab.add_theme_color_override("font_color", COL_INK) + _confirm_lab = UiKit.label("RAP−%d · 旧板旗作废" % refresh_cost, 14, true, COL_INK) _confirm_row.add_child(_confirm_lab) var confirm_btn := UiKit.button("确认弃图") confirm_btn.pressed.connect(_disarm_and_fire) @@ -153,21 +151,21 @@ func set_rap(rap: int) -> void: _rap_feedback.text = "" -# 资源面拒绝反馈(pattern 1 resource_face 腿):RAP 数字脉冲(RAP<8 弃图刷新被拒) -func pulse_rap() -> void: +# 资源读数拒绝脉冲单点(pattern 1 resource_face 腿——RAP/AP 两份逐字重复合并:朱红脉冲一拍回白) +func _pulse(label: Label) -> void: var tw := create_tween() - tw.tween_property(_rap_label, "modulate", Color(1.0, 0.4, 0.3), UiKit.motion(0.1)) - tw.tween_property(_rap_label, "modulate", Color(1, 1, 1), UiKit.motion(0.25)) + tw.tween_property(label, "modulate", Color(1.0, 0.4, 0.3), UiKit.motion(0.1)) + tw.tween_property(label, "modulate", Color(1, 1, 1), UiKit.motion(0.25)) + + +func pulse_rap() -> void: + _pulse(_rap_label) func _render_rap_dots(rap: int) -> void: var shown := mini(rap, 16) while _rap_dots.get_child_count() < shown: - var d := ColorRect.new() - d.custom_minimum_size = Vector2(8, 8) - d.size_flags_vertical = Control.SIZE_SHRINK_CENTER - d.mouse_filter = Control.MOUSE_FILTER_IGNORE - _rap_dots.add_child(d) + _rap_dots.add_child(_make_dot(8.0, true)) # RAP 点阵 8px 窄档(随读数字号)——建点单点共用 for i in _rap_dots.get_child_count(): var d: ColorRect = _rap_dots.get_child(i) d.color = COL_GOLD if i < shown else COL_DOT_OFF @@ -226,14 +224,14 @@ func _disarm() -> void: # 资源面拒绝反馈(pattern 1 resource_face 腿):AP 数字脉冲 func pulse_ap() -> void: - var tw := create_tween() - tw.tween_property(_ap_label, "modulate", Color(1.0, 0.4, 0.3), UiKit.motion(0.1)) - tw.tween_property(_ap_label, "modulate", Color(1, 1, 1), UiKit.motion(0.25)) + _pulse(_ap_label) -func _make_dot() -> ColorRect: +# 点阵建点单点(AP 10px / RAP 8px 两套建点参数化合并;初始色=熄灭档——投影循环当拍覆写) +func _make_dot(px := 10.0, shrink_center := false) -> ColorRect: var d := ColorRect.new() - d.custom_minimum_size = Vector2(10, 10) + d.custom_minimum_size = Vector2(px, px) + d.size_flags_vertical = Control.SIZE_SHRINK_CENTER if shrink_center else Control.SIZE_FILL d.color = COL_DOT_OFF d.mouse_filter = Control.MOUSE_FILTER_IGNORE return d diff --git a/game/presentation/views/battle_end_overlay.gd b/game/presentation/views/battle_end_overlay.gd index d66f6e2e..d985efff 100644 --- a/game/presentation/views/battle_end_overlay.gd +++ b/game/presentation/views/battle_end_overlay.gd @@ -24,22 +24,13 @@ var _board_rect := Rect2() func _init() -> void: # 二轮修改批 B(2026-09-29 用户目检裁决):半透明浮层文字不可读 → 实底石板皮面板形态 - #(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝),信息与按钮全部上板; - # 金蜡封 = 面板头部静态徽记(原全屏坠落演出退役——落点被面板遮挡)。 - _dim = ColorRect.new() - _dim.color = Color(COL_WARM, 0.0) - _dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(_dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - _dim.add_child(center) - var panel := PanelContainer.new() - panel.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(16, 14, 16, 14))) - center.add_child(panel) - var box := VBoxContainer.new() - box.custom_minimum_size = Vector2(520, 0) - box.add_theme_constant_override("separation", 12) - panel.add_child(box) + #(与营地/设置/图例同族)。半透明氛围 dim 保留(胜利暖纱/失败暮蓝——open_* 就地换色),信息与 + # 按钮全部上板;金蜡封 = 面板头部静态徽记(原全屏坠落演出退役——落点被面板遮挡)。 + # 骨架经 UiKit.modal_shell(T4 收口批);_dim 引用摘出供 open_* 换氛围色。 + var shell := UiKit.modal_shell(self, Color(COL_WARM, 0.0), Vector2(520, 0), 12, true, + UiKit.panel_skinned(Vector4(16, 14, 16, 14))) + _dim = shell.dim + var box: VBoxContainer = shell.box var header := HBoxContainer.new() header.add_theme_constant_override("separation", 12) header.alignment = BoxContainer.ALIGNMENT_CENTER @@ -51,13 +42,11 @@ func _init() -> void: _seal.custom_minimum_size = Vector2(34, 34) _seal.size_flags_vertical = Control.SIZE_SHRINK_CENTER header.add_child(_seal) - _verdict = UiKit.label("", 24, true) - _verdict.add_theme_color_override("font_color", UiKit.INK_TITLE) + _verdict = UiKit.label("", 24, true, UiKit.INK_TITLE) _verdict.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _verdict.size_flags_vertical = Control.SIZE_SHRINK_CENTER header.add_child(_verdict) - _gp_line = UiKit.label("", 16, true) - _gp_line.add_theme_color_override("font_color", UiKit.INK_LABEL) + _gp_line = UiKit.label("", 16, true, UiKit.INK_LABEL) _gp_line.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER _gp_line.visible = false box.add_child(_gp_line) diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index 8fcbf463..009cb04e 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -40,15 +40,13 @@ func _init(db_ref) -> void: add_child(vb) var head := HBoxContainer.new() vb.add_child(head) - _summary = UiKit.label("", 16, false) # 对比度修复(v3.5 批 B3 2026-09-30——明细条灰字对深灰底实测 2.63:1):暗底双色墨制=暖白族 - _summary.add_theme_color_override("font_color", UiKit.WARM) + _summary = UiKit.label("", 16, false, UiKit.WARM) _summary.custom_minimum_size = Vector2(0, BAND_H_DIG - 4) _summary.clip_text = true _summary.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS head.add_child(_summary) - _hint = UiKit.label("结算明细 ▸ 点击展开回看", 14, false) - _hint.add_theme_color_override("font_color", UiKit.WARM) # 辅助提示(≥14 辅助档)随上暖白 + _hint = UiKit.label("结算明细 ▸ 点击展开回看", 14, false, UiKit.WARM) # 辅助提示(≥14 辅助档)随上暖白 _hint.size_flags_horizontal = Control.SIZE_SHRINK_END head.add_child(_hint) _log = RichTextLabel.new() @@ -162,7 +160,7 @@ func _gold_frame(text: String) -> String: func _format_row(row: Dictionary, agg_count := -1) -> String: var src := _source_label(String(row.get("source_class", ""))) var round_no: int = int(row.get("round_index", 0)) - var prefix := " [color=#9a8a72]R%d%s[/color] " % [round_no, ("·" + src) if src != "" else ""] + var prefix := " [color=#%s]R%d%s[/color] " % [UiKit.HEX_LOG_ROUND, round_no, ("·" + src) if src != "" else ""] var hl := String(row.get("highlight", "")) var v: int = int(row.get("value", 0)) var name_: String = String(row.get("display_name", "")) @@ -174,12 +172,12 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: # GP 型效果行(GP+商店房实施批 2026-09-21):「金币袋 +3」兑付反馈——value = 本行 ±power, # escrow = 战斗内缓冲现值(入钱包在战斗末一次上抛——呈现语义与增益池分账) if String(row.get("effect_id", "")) in ["gp_gain", "gp_loss"]: - var gp_col := "#e0b860" if v > 0 else "#c47c7c" + var gp_col := UiKit.HEX_LOG_GAIN if v > 0 else UiKit.HEX_LOG_LOSS var gp_body := "%s %+d(战斗暂存 %+d)%s" % [name_, v, int(row.get("escrow", 0)), suffix] return prefix + (_gold_frame(gp_body) if hl != "" else "[color=%s]%s[/color]" % [gp_col, gp_body]) # 效果行 value = 单次贡献值,pool_value = 池合成值——禁相加呈现(战斗结算 呈现细则) var pool: int = int(row.get("pool_value", 0)) - var col := "#7cc47c" if v > 0 else "#c47c7c" + var col := UiKit.HEX_LOG_HEAL if v > 0 else UiKit.HEX_LOG_LOSS var body := "%s 单次%+d(池 %+d)%s" % [name_, v, pool, suffix] return prefix + (_gold_frame(body) if hl != "" else "[color=%s]%s[/color]" % [col, body]) "damage": @@ -197,29 +195,29 @@ func _format_row(row: Dictionary, agg_count := -1) -> String: var kill := " ⚔击杀!" if row.get("killed", false) else "" # 二轮修改批 C(2026-09-29 用户目检——「每次结算标出的伤害数字不明显」): # 伤害值放大加色独立于行文(朱红大一号——数字即信息主角,非同字号色温微差) - var body := "%s [color=#d9402a][size=17]-%d[/size][/color]%s%s%s%s" % [name_, v, roll_txt, tail, kill, suffix] - var body_col := "#e07050" if row.get("killed", false) else ("#9a8a72" if v == 0 else "#e0a050") + var body := "%s [color=#%s][size=17]-%d[/size][/color]%s%s%s%s" % [name_, UiKit.HEX_LOG_DMG_NUM, v, roll_txt, tail, kill, suffix] + var body_col := UiKit.HEX_LOG_DMG_KILL if row.get("killed", false) else (UiKit.HEX_LOG_ROUND if v == 0 else UiKit.HEX_LOG_DMG_BODY) if bool(row.get("crit", false)): return prefix + _gold_frame(body) # nat 最大面暴击 = 金框高亮行 return prefix + "[color=%s]%s[/color]" % [body_col, body] "heal": - return prefix + "[color=#7cc47c]%s +%d%s%s[/color]" % [name_, v, tail, suffix] + return prefix + "[color=#%s]%s +%d%s%s[/color]" % [UiKit.HEX_LOG_HEAL, name_, v, tail, suffix] "reward_ineffective": # 未生效类行:⚠ icon + 划线形状冗余(非色相独载)+ 聚合计数 var n := (" ×%d" % agg_count) if agg_count > 1 else "" - return prefix + "[s][color=#b08060]⚠ %s%s[/color][/s]%s" % [name_, n, suffix] + return prefix + "[s][color=#%s]⚠ %s%s[/color][/s]%s" % [UiKit.HEX_LOG_VOIDED, name_, n, suffix] "flag_miss": var body := "✗ %s%s" % [name_, "(本板完美排雷已破——归因此旗)" if hl == "perfect_broken" else ""] - return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#9a8a72]%s[/color]" % body) + return prefix + (_gold_frame(body) if hl == "perfect_broken" else "[color=#%s]%s[/color]" % [UiKit.HEX_LOG_ROUND, body]) "cast": # 施放行(敌人技能池批——蓄力行 charge 改型):增益技 0 伤害施放,具名技能 - return prefix + "[color=#b09a72]✦ %s%s[/color]" % [name_, suffix] + return prefix + "[color=#%s]✦ %s%s[/color]" % [UiKit.HEX_LOG_CAST, name_, suffix] "decay": - return prefix + "[color=#8a9ab0]状态衰减 ×%d[/color]" % v + return prefix + "[color=#%s]状态衰减 ×%d[/color]" % [UiKit.HEX_LOG_DECAY, v] "verdict": if int(row.value) == 1: - return "[color=#80d080][b]◆ %s[/b][/color]" % name_ - return "[color=#e0b860][b]◆ %s[/b][/color]" % name_ + return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_WIN, name_] + return "[color=#%s][b]◆ %s[/b][/color]" % [UiKit.HEX_LOG_GAIN, name_] "highlight": return prefix + _gold_frame("★ %s" % name_) return prefix + name_ + suffix diff --git a/game/presentation/views/battle_stage_view.gd b/game/presentation/views/battle_stage_view.gd index daf77336..abd2e62b 100644 --- a/game/presentation/views/battle_stage_view.gd +++ b/game/presentation/views/battle_stage_view.gd @@ -25,6 +25,7 @@ const _STANDEE_BODY := preload("res://presentation/views/standee_body.gd") const _CONTENT_MARGINS := Rect2(6, 4, 6, 4) # 原 PanelContainer stylebox 内容边距(子面几何零漂移) const _STANDEE_W := 150.0 # 站桩盒宽(旧 StandeeView custom_minimum_size) const _STANDEE_GAP := 8.0 # 敌站桩间距(旧 enemy_box HBox separation) +const STAGE_CENTER_HINT := "兑现演出区\n(结算期:光带 → 弹道 → 受击)" # 演出区占位文案(set_settle_focus 恢复态同源——字面量双写退役) var hero_standee: Node2D # StandeeBody——主角站桩本体(世界内;slot_row = 壳层伴随件引用) var round_label: Label @@ -42,17 +43,18 @@ var world: Node2D # StageWorld(preload 脚本实例——Node2D 世 func _init(db_ref) -> void: custom_minimum_size = Vector2(0, 252) # 面板底(圣经 v3.5 批 B4 2026-09-30——§6.1 舞台正面目标落码:B 区=地窟看台,世界底层暗背景 - # Color8(62,52,42)=方向稿 render_battle_screen 取值——替换原浅石板皮〔实测 L0.027 旧深底与 - # 上半米黄割裂的 opposite 端——v3.5 定暖暗中档〕;描边=PANEL_BORDER 同方向稿) + # =方向稿 render_battle_screen 取值(值单点收 UiKit.STAGE_BG——T4 裸色收口)——替换原浅石板皮 + # 〔实测 L0.027 旧深底与上半米黄割裂的 opposite 端——v3.5 定暖暗中档〕;描边=PANEL_BORDER 同方向稿) var bg := Panel.new() - var sb := UiKit.panel_flat(Color8(62, 52, 42), UiKit.PANEL_BORDER, 0, 1) + var sb := UiKit.panel_flat(UiKit.STAGE_BG, UiKit.PANEL_BORDER, 0, 1) bg.add_theme_stylebox_override("panel", sb) bg.set_anchors_preset(Control.PRESET_FULL_RECT) bg.mouse_filter = Control.MOUSE_FILTER_IGNORE add_child(bg) - # 顶光柱氛围层(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光 (255,236,200) - # 自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/世界站桩之下——层序=bg < 光柱 < 世界) + # 顶光柱氛围层(§6.1 光源氛围——方向稿四柱:x 基准 110/900/1055/1210@1280,暖光(值单点收 + # UiKit.BEAM_LIGHT——T4 裸色收口)自顶向下 60 段渐宽窄渐隐〔顶 α26→0〕;绘制在暗底之上/ + # 世界站桩之下——层序=bg < 光柱 < 世界) var beams := Control.new() beams.set_anchors_preset(Control.PRESET_FULL_RECT) beams.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -86,9 +88,8 @@ func _init(db_ref) -> void: vb.offset_bottom = -_CONTENT_MARGINS.size.y add_child(vb) - round_label = UiKit.label("第 — 轮 · —", 15, true) + round_label = UiKit.label("第 — 轮 · —", 15, true, UiKit.WARM) # 暗底双色墨制(v3.5 批 B4——原深墨对暗底不可读) round_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER - round_label.add_theme_color_override("font_color", UiKit.WARM) # 暗底双色墨制(v3.5 批 B4——原深墨对暗底不可读) vb.add_child(round_label) _row = HBoxContainer.new() @@ -103,8 +104,7 @@ func _init(db_ref) -> void: var center_col := VBoxContainer.new() center_col.alignment = BoxContainer.ALIGNMENT_CENTER center.add_child(center_col) - stage_center = UiKit.label("兑现演出区\n(结算期:光带 → 弹道 → 受击)", 13, false) - stage_center.add_theme_color_override("font_color", UiKit.WARM_SOFT) # 暗底双色墨制(原 STONE_FAINT 对暗底不可读) + stage_center = UiKit.label(STAGE_CENTER_HINT, 13, false, UiKit.WARM_SOFT) # 暗底双色墨制(原 STONE_FAINT 对暗底不可读) stage_center.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER center_col.add_child(stage_center) _row.add_child(center) @@ -146,7 +146,7 @@ func _draw_light_beams(c: Control) -> void: var a := int(26.0 * (1.0 - float(i) / 60.0)) var half := 46.0 - float(i) / 3.0 c.draw_rect(Rect2(x0 - half, 0.0, half * 2.0, 40.0 + float(i) * 3.0), - Color8(255, 236, 200, a), true) + Color(UiKit.BEAM_LIGHT, float(a) / 255.0), true) func _make_slot_companion(parent: Control) -> StatusSlotRow: @@ -314,31 +314,27 @@ func on_step_row_fx(row: Dictionary) -> void: # 主角受击演出(迭代 6 T5 拆早退——旧「敌→主角零反馈」):弹道到达拍对齐, # 受击态+闪白+打击火花+击杀塌陷与敌方路径同密度(演出对等——计划书 §1.4) hero_standee.set_hero_state("hit") # 受击态(短驻自回 idle) - var hero_arrived := func(): - hero_standee.play_hit_impact() - hero_standee.flash_hit() - if bool(row.get("killed", false)): - hero_standee.mark_dead() - if get_tree() != null: - get_tree().create_timer(UiKit.motion(0.33)).timeout.connect(hero_arrived) - else: - hero_arrived.call() + _schedule_arrival_fx(hero_standee, bool(row.get("killed", false))) return hero_standee.set_hero_state("attack") # 我方攻击行——attack 态(短驻自回) var card: Node2D = _card_by_name(target) if card == null: return - # 弹道到达拍对齐(迭代 6 T4 终稿):闪白/打击火花/击杀塌陷延迟 ≈弹道飞行时长 - # (play_projectile 0.16+0.19)——呈现链序=弹道→闪白+火花→血条(投影刷新)→灰化塌陷。 - # 行突发同拍多定时器互不干扰(flash 重启语义/死亡幂等守卫);卡片提前释放=守卫跳过; - # 树外(防御)=即时执行不调度。 + _schedule_arrival_fx(card, bool(row.get("killed", false))) + + +# 弹道到达拍调度单点(主角受击/敌方受击两份「0.33s 定时器+到达 lambda」同构块合并——T4 杂并批): +# 闪白/打击火花/击杀塌陷延迟 ≈弹道飞行时长(play_projectile 0.16+0.19)——呈现链序=弹道→闪白+火花 +# →血条(投影刷新)→灰化塌陷。行突发同拍多定时器互不干扰(flash 重启语义/死亡幂等守卫); +# 本体提前释放=is_instance_valid 守卫跳过;树外(防御)=即时执行不调度。 +func _schedule_arrival_fx(body: Node2D, killed: bool) -> void: var arrived := func(): - if not is_instance_valid(card): + if not is_instance_valid(body): return - card.play_hit_impact() - card.flash_hit() - if bool(row.get("killed", false)): - card.mark_dead() + body.play_hit_impact() + body.flash_hit() + if killed: + body.mark_dead() if get_tree() != null: get_tree().create_timer(UiKit.motion(0.33)).timeout.connect(arrived) else: @@ -381,4 +377,4 @@ func settle_hero_idle() -> void: # 结算期 B 区第一注视点切换(能量 7/10 清算调性):结算中占位文案退位——兑现演出由 # SettlementFxView 弹道/光带本体接管(演出链批);挖掘期恢复教学占位文案 func set_settle_focus(on: bool) -> void: - stage_center.text = "" if on else "兑现演出区\n(结算期:光带 → 弹道 → 受击)" + stage_center.text = "" if on else STAGE_CENTER_HINT diff --git a/game/presentation/views/board_fx_view.gd b/game/presentation/views/board_fx_view.gd index c852c5b5..84df2360 100644 --- a/game/presentation/views/board_fx_view.gd +++ b/game/presentation/views/board_fx_view.gd @@ -17,8 +17,7 @@ var _banner: Label func _init() -> void: mouse_filter = Control.MOUSE_FILTER_IGNORE set_anchors_preset(Control.PRESET_FULL_RECT) - _banner = UiKit.label("", 26, true) - _banner.add_theme_color_override("font_color", COL_GOLD) + _banner = UiKit.label("", 26, true, COL_GOLD) _banner.set_anchors_preset(Control.PRESET_CENTER_TOP) _banner.grow_horizontal = Control.GROW_DIRECTION_BOTH _banner.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 18fc7b89..3ddaee05 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -858,24 +858,11 @@ func _reset_breath() -> void: func _load_board_tiles(board_type: String = "dungeon") -> void: if board_type == "": board_type = "dungeon" - var t := { - "wall_center": UiKit.load_tex(TILE_WALL_FMT % ["wall_center", board_type]), - "wall_center_crack": UiKit.load_tex(TILE_WALL_FMT % ["wall_center_crack", board_type]), - "wall_center_speckle": UiKit.load_tex(TILE_WALL_FMT % ["wall_center_speckle", board_type]), - "wall_n": UiKit.load_tex(TILE_WALL_FMT % ["wall_n", board_type]), - "wall_s": UiKit.load_tex(TILE_WALL_FMT % ["wall_s", board_type]), - "wall_w": UiKit.load_tex(TILE_WALL_FMT % ["wall_w", board_type]), - "wall_nw": UiKit.load_tex(TILE_WALL_FMT % ["wall_nw", board_type]), - "wall_sw": UiKit.load_tex(TILE_WALL_FMT % ["wall_sw", board_type]), - "wall_hrun": UiKit.load_tex(TILE_WALL_FMT % ["wall_hrun", board_type]), - "wall_vrun": UiKit.load_tex(TILE_WALL_FMT % ["wall_vrun", board_type]), - "wall_end_n": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_n", board_type]), - "wall_end_s": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_s", board_type]), - "wall_end_w": UiKit.load_tex(TILE_WALL_FMT % ["wall_end_w", board_type]), - "wall_lone": UiKit.load_tex(TILE_WALL_FMT % ["wall_lone", board_type]), - "wall_in_nw": UiKit.load_tex(TILE_WALL_FMT % ["wall_in_nw", board_type]), - "wall_in_sw": UiKit.load_tex(TILE_WALL_FMT % ["wall_in_sw", board_type]), - } + # 16 墙瓦片装载循环节(词表 TILE_WALL_NAMES 单点——T4 杂并:手写 16 条字典项重复退役; + # 词表本身被 terrain_tile_sets_test 直读,保留) + var t := {} + for wall_name in TILE_WALL_NAMES: + t[wall_name] = UiKit.load_tex(TILE_WALL_FMT % [wall_name, board_type]) var floors: Array[Texture2D] = [] for i in range(1, TILE_FLOOR_VARIANTS + 1): floors.append(UiKit.load_tex(TILE_FLOOR_FMT % [board_type, i])) diff --git a/game/presentation/views/board_world.gd b/game/presentation/views/board_world.gd index 85fda58a..71c9b922 100644 --- a/game/presentation/views/board_world.gd +++ b/game/presentation/views/board_world.gd @@ -76,7 +76,7 @@ var cols := 0: # 列数(正面悬崖竖缝数 = cols−1) set(v): cols = v queue_redraw() -var mat := Color8(201, 175, 131) # 材质锚色(掩尘 #C9AF83——侧面由此派生明暗;BoardView 传入) +var mat: Color = UiKit.BRICK # 材质锚色(掩尘 #C9AF83 与 UiKit.BRICK 同值——T4 裸色收口改引单点;侧面由此派生明暗,BoardView 传入可覆写) func _draw() -> void: diff --git a/game/presentation/views/bonus_pick_overlay.gd b/game/presentation/views/bonus_pick_overlay.gd index 4c4b47e3..e4742f6e 100644 --- a/game/presentation/views/bonus_pick_overlay.gd +++ b/game/presentation/views/bonus_pick_overlay.gd @@ -22,18 +22,8 @@ var _card_meta: Array = [] # open_pick 缓存 [{id, text}]— func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(0, 0, 0, 0.6) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var box := VBoxContainer.new() - box.add_theme_constant_override("separation", 12) - center.add_child(box) - _label = UiKit.label("", 20, true) - _label.add_theme_color_override("font_color", UiKit.WARM) + var box: VBoxContainer = UiKit.modal_shell(self, Color(0, 0, 0, 0.6)).box + _label = UiKit.label("", 20, true, UiKit.WARM) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_label) _choice_box = HBoxContainer.new() @@ -49,7 +39,7 @@ func _init() -> void: _replace_box.alignment = BoxContainer.ALIGNMENT_CENTER box.add_child(_replace_box) _continue_btn = UiKit.button("继续") - _continue_btn.pressed.connect(func(): continue_pressed.emit()) + _continue_btn.pressed.connect(_emit_continue_pressed) _continue_btn.visible = false box.add_child(_continue_btn) visible = false @@ -68,7 +58,7 @@ func open_pick(header: String, cards: Array) -> void: var cid := String(ch.get("id", "")) var btn := UiKit.button(String(ch.get("text", ""))) btn.custom_minimum_size = Vector2(170, 88) - btn.pressed.connect(func(): _select_card(cid)) + btn.pressed.connect(_select_card.bind(cid)) # 具名方法 bind(仓内惯例——内联 lambda 退役) _choice_box.add_child(btn) _choice_box.visible = true _confirm_btn.visible = false @@ -129,10 +119,10 @@ func open_replace(items: Array, incoming_name: String, holding: int, slots: int) for it in items: var tag: int = int(it.get("index", it.get("handle", -1))) # 通关渠道=携带序号 / 节点渠道=库存句柄(核心分发面裁决) var btn := UiKit.button("丢弃「%s」换新" % String(it.get("display_name", it.get("item_id", "")))) - btn.pressed.connect(func(): replace_chosen.emit(tag)) + btn.pressed.connect(_emit_replace_chosen.bind(tag)) _replace_box.add_child(btn) var decline := UiKit.button("放弃新件(不获取)") - decline.pressed.connect(func(): replace_chosen.emit(-1)) + decline.pressed.connect(_emit_replace_chosen.bind(-1)) _replace_box.add_child(decline) _choice_box.visible = false _confirm_btn.visible = false @@ -148,3 +138,11 @@ func close_pick() -> void: _continue_btn.visible = false _confirm_btn.visible = false visible = false + + +func _emit_continue_pressed() -> void: + continue_pressed.emit() + + +func _emit_replace_chosen(tag: int) -> void: + replace_chosen.emit(tag) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index 7f46d85a..b778be23 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -15,10 +15,10 @@ signal buy_requested(attr: String) signal closed const ATTR_LABELS := {"atk": "攻击", "max_hp": "生命上限", "armor_class": "护甲"} # 档位文案轴(数据契约 attr 词表,与 ShopOverlay 同款) -# 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨) -const HEADER_BAND_COL := Color8(122, 82, 50) -const ROW_PLATE_COL := Color8(240, 230, 206) -const BADGE_PANEL_COL := Color8(40, 32, 24) +# 方向稿取色(4-camp.png——柜台头带深棕/行板暖米;双色墨=亮板配深墨)——值单点收 UiKit(T4 裸色收口批) +const HEADER_BAND_COL := UiKit.CAMP_HEADER_BAND +const ROW_PLATE_COL := UiKit.CAMP_ROW_PLATE +const BADGE_PANEL_COL := UiKit.CAMP_BADGE_PANEL var _balance_label: Label var _forge_holder: VBoxContainer @@ -29,10 +29,7 @@ var _recent_list: Label # 最近冒险(逐行拼接 label——只读 func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(0, 0, 0, 0.45) # 方向稿=石纹底透出(浅 dim——石纹全屏底担当氛围) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) + var dim := UiKit.modal_dim(self, Color(0, 0, 0, 0.45)) # 方向稿=石纹底透出(浅 dim——石纹全屏底担当氛围) # 石纹底噪(样张 v2 冻结件——start overlay 同款;缺位不挂即回退平 dim) var bd_tex := UiKit.load_tex(UiKit.TEX_BACKDROP_STONE) if bd_tex != null: @@ -65,11 +62,9 @@ func _init() -> void: top.add_child(plaque) var plaque_col := VBoxContainer.new() plaque.add_child(plaque_col) - var title := UiKit.label("营地 · 永久强化", 22, true) - title.add_theme_color_override("font_color", UiKit.INK_TITLE) + var title := UiKit.label("营地 · 永久强化", 22, true, UiKit.INK_TITLE) plaque_col.add_child(title) - var sub := UiKit.label("跨局成长账本——钥石换永久属性", 13, false) - sub.add_theme_color_override("font_color", UiKit.STONE) + var sub := UiKit.label("跨局成长账本——钥石换永久属性", 13, false, UiKit.STONE) plaque_col.add_child(sub) var spacer := Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -84,8 +79,7 @@ func _init() -> void: var keystone_icon := UiKit.icon_rect(UiKit.TEX_ICON_KEYSTONE, 18) # 缺位 null → 纯文本回退 if keystone_icon != null: bal_row.add_child(keystone_icon) - _balance_label = UiKit.label("钥石 0", 18, true) - _balance_label.add_theme_color_override("font_color", UiKit.GOLD) + _balance_label = UiKit.label("钥石 0", 18, true, UiKit.GOLD) bal_row.add_child(_balance_label) # ── 三柜台列(方向稿同构:头带深棕+米色板身)── @@ -102,8 +96,7 @@ func _init() -> void: mid.add_child(_counter_panel("道具工坊", UiKit.TEX_ICON_WORKSHOP, _workshop_holder, 356)) var atlas_holder := VBoxContainer.new() mid.add_child(_counter_panel("奖励格图鉴", UiKit.TEX_ICON_ATLAS, atlas_holder, 356)) - var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false) - lock_l.add_theme_color_override("font_color", UiKit.STONE) + var lock_l := UiKit.label(" —— 柜台结构位预留:内容排 Alpha(地图战斗资源差异化批)", 13, false, UiKit.STONE) lock_l.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lock_l.custom_minimum_size = Vector2(300, 0) atlas_holder.add_child(lock_l) @@ -117,12 +110,10 @@ func _init() -> void: var spacer2 := Control.new() spacer2.size_flags_horizontal = Control.SIZE_EXPAND_FILL bottom.add_child(spacer2) - var caption := UiKit.label("每购得一件永久强化,对应柜台亮起一格暖光 · 价格为钥石", 13, false) - caption.add_theme_color_override("font_color", UiKit.WARM_SOFT) + var caption := UiKit.label("每购得一件永久强化,对应柜台亮起一格暖光 · 价格为钥石", 13, false, UiKit.WARM_SOFT) bottom.add_child(caption) col.add_child(bottom) - _recent_list = UiKit.label("", 13, false) - _recent_list.add_theme_color_override("font_color", UiKit.WARM_SOFT) + _recent_list = UiKit.label("", 13, false, UiKit.WARM_SOFT) _recent_list.clip_text = true _recent_list.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS col.add_child(_recent_list) @@ -148,8 +139,7 @@ func _counter_panel(title_text: String, icon_path: String, holder: VBoxContainer var ic := UiKit.icon_rect(icon_path, 16) # 缺位 null → 纯文本回退 if ic != null: head.add_child(ic) - var l := UiKit.label(title_text, 16, true) - l.add_theme_color_override("font_color", UiKit.WARM) + var l := UiKit.label(title_text, 16, true, UiKit.WARM) head.add_child(l) vbox.add_child(holder) return panel @@ -223,16 +213,13 @@ func _build_forge_row(holder: VBoxContainer, row: Dictionary) -> Dictionary: line.add_theme_constant_override("separation", 12) card.add_child(line) var attr := String(row.get("attr", "")) - var name_l := UiKit.label("%s %+d/级" % [String(row.get("display_name", "")), int(row.get("flat", 0))], 14, true) - name_l.add_theme_color_override("font_color", UiKit.INK) + var name_l := UiKit.label("%s %+d/级" % [String(row.get("display_name", "")), int(row.get("flat", 0))], 14, true, UiKit.INK) name_l.custom_minimum_size = Vector2(132, 0) line.add_child(name_l) - var level_l := UiKit.label("", 14, false) - level_l.add_theme_color_override("font_color", UiKit.INK) + var level_l := UiKit.label("", 14, false, UiKit.INK) level_l.custom_minimum_size = Vector2(44, 0) line.add_child(level_l) - var price_l := UiKit.label("", 14, false) - price_l.add_theme_color_override("font_color", UiKit.STONE) + var price_l := UiKit.label("", 14, false, UiKit.STONE) price_l.custom_minimum_size = Vector2(64, 0) line.add_child(price_l) var btn := UiKit.button("锻造 +1 级") @@ -252,16 +239,14 @@ func _rebuild_workshop(data: Dictionary) -> void: var line := HBoxContainer.new() line.add_theme_constant_override("separation", 12) card.add_child(line) - var name_l := UiKit.label(String(row.get("display_name", "")), 14, true) - name_l.add_theme_color_override("font_color", UiKit.INK) + var name_l := UiKit.label(String(row.get("display_name", "")), 14, true, UiKit.INK) name_l.custom_minimum_size = Vector2(132, 0) line.add_child(name_l) - var progress_l := UiKit.label("", 13, false) - progress_l.add_theme_color_override("font_color", UiKit.STONE) + var progress_l := UiKit.label("", 13, false, UiKit.STONE) progress_l.custom_minimum_size = Vector2(110, 0) line.add_child(progress_l) - var status_l := UiKit.label("", 13, true) - status_l.add_theme_color_override("font_color", UiKit.STONE) + # 状态色随卡⑭ 亮板双色墨制(深墨族替代暖白族)——色随刷新态条件赋值,非机械两行式(T4 收口保留) + var status_l := UiKit.label("", 13, true, UiKit.STONE) line.add_child(status_l) _workshop_holder.add_child(card) _workshop_rows.append({"item_id": String(row.get("item_id", "")), "progress_l": progress_l, "status_l": status_l}) diff --git a/game/presentation/views/confirm_dialog.gd b/game/presentation/views/confirm_dialog.gd index 68e72138..41f8364e 100644 --- a/game/presentation/views/confirm_dialog.gd +++ b/game/presentation/views/confirm_dialog.gd @@ -15,22 +15,9 @@ var _cancel_btn: Button func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(UiKit.DIM, 0.55) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var panel := PanelContainer.new() - panel.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(14, 12, 14, 12))) - center.add_child(panel) - var box := VBoxContainer.new() - box.custom_minimum_size = Vector2(460, 0) - box.add_theme_constant_override("separation", 12) - panel.add_child(box) - _text = UiKit.label("", 16, false) - _text.add_theme_color_override("font_color", UiKit.INK_TITLE) + var box: VBoxContainer = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(460, 0), 12, + true, UiKit.panel_skinned(Vector4(14, 12, 14, 12))).box + _text = UiKit.label("", 16, false, UiKit.INK_TITLE) _text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _text.custom_minimum_size = Vector2(430, 0) box.add_child(_text) diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 35e6bb99..0ae0f173 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -94,8 +94,7 @@ func set_level_bonuses(lines: Array) -> void: _bonus_list.remove_child(c) c.queue_free() for line in lines: - var l := UiKit.label(String(line), 16, false) # 字号下限(v3.5 批 B3——正文 ≥16) - l.add_theme_color_override("font_color", COL_INK) + var l := UiKit.label(String(line), 16, false, COL_INK) # 字号下限(v3.5 批 B3——正文 ≥16) _bonus_list.add_child(l) _bonus_section.visible = not lines.is_empty() @@ -115,10 +114,9 @@ func armor_text() -> String: func _data_line() -> Label: - var l := UiKit.label("", 16, false) # 对比度修复(v3.5 批 B3 2026-09-30——非粗体 label 默认浅灰对米色数据框仅 1.31:1, # 实测=ui-redesign 对比报告 §一):显式主墨(对 PANEL_DATA ≈10:1) - l.add_theme_color_override("font_color", UiKit.INK) + var l := UiKit.label("", 16, false, UiKit.INK) l.clip_text = true l.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS return l @@ -150,6 +148,7 @@ func set_aux_inventory(items: Array, selected_handle: int) -> void: glyph.vertical_alignment = VERTICAL_ALIGNMENT_CENTER glyph.set_anchors_preset(Control.PRESET_FULL_RECT) glyph.mouse_filter = Control.MOUSE_FILTER_IGNORE + # 墨色随 used/usable 条件赋值——非机械两行式,T4 收口保留原分支 if used: glyph.add_theme_color_override("font_color", UiKit.STONE_FAINT) elif not usable: diff --git a/game/presentation/views/legend_overlay.gd b/game/presentation/views/legend_overlay.gd index 16d6c57c..f0eb03b3 100644 --- a/game/presentation/views/legend_overlay.gd +++ b/game/presentation/views/legend_overlay.gd @@ -27,23 +27,12 @@ var _rap_cost := -1 # <0 = 不渲染 RAP 行(打磨包 2026-09-29——构 func _init(rap_cost: int = -1) -> void: _rap_cost = rap_cost - var dim := ColorRect.new() - dim.color = Color(UiKit.DIM, 0.55) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.gui_input.connect(_on_dim_gui_input) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - _panel = PanelContainer.new() - _panel.add_theme_stylebox_override("panel", UiKit.panel_skinned(Vector4(12, 10, 12, 10))) # 石板皮(T2;缺位回退 flat) - center.add_child(_panel) - var box := VBoxContainer.new() - box.custom_minimum_size = Vector2(520, 0) - box.add_theme_constant_override("separation", 6) - _panel.add_child(box) - var title := UiKit.label("图例 · 格态", 20, true) - title.add_theme_color_override("font_color", UiKit.INK_TITLE) # 亮底墨字(§7.2 数据素面) + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(520, 0), 6, true, + UiKit.panel_skinned(Vector4(12, 10, 12, 10))) # 石板皮(T2;缺位回退 flat) + _panel = shell.panel + (shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input) + var box: VBoxContainer = shell.box + var title := UiKit.label("图例 · 格态", 20, true, UiKit.INK_TITLE) # 亮底墨字(§7.2 数据素面) box.add_child(title) box.add_child(HSeparator.new()) for e in ENTRIES: @@ -53,8 +42,7 @@ func _init(rap_cost: int = -1) -> void: chip.custom_minimum_size = Vector2(18, 18) chip.color = Color(e[0]) row.add_child(chip) - var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false) - lab.add_theme_color_override("font_color", UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) + var lab := UiKit.label("%s —— %s" % [e[1], e[2]], 13, false, UiKit.INK_LABEL) # 亮底墨字(§7.2 数据素面) lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART lab.custom_minimum_size = Vector2(440, 0) row.add_child(lab) @@ -66,10 +54,9 @@ func _init(rap_cost: int = -1) -> void: rap_row.add_theme_constant_override("separation", 8) var rap_chip := ColorRect.new() rap_chip.custom_minimum_size = Vector2(18, 18) - rap_chip.color = Color("#F0A828") + rap_chip.color = UiKit.GOLD # 同 #F0A828——金徽章同源单点(T4 裸色收口) rap_row.add_child(rap_chip) - var rap_lab := UiKit.label("RAP 储备行动点 —— 停手时剩余 AP 转存;弃图刷新消耗 RAP−%d(旧板旗作废,AP 保留继续挖)" % _rap_cost, 13, false) - rap_lab.add_theme_color_override("font_color", UiKit.INK_LABEL) + var rap_lab := UiKit.label("RAP 储备行动点 —— 停手时剩余 AP 转存;弃图刷新消耗 RAP−%d(旧板旗作废,AP 保留继续挖)" % _rap_cost, 13, false, UiKit.INK_LABEL) rap_lab.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART rap_lab.custom_minimum_size = Vector2(440, 0) rap_row.add_child(rap_lab) diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index c11a005a..63330777 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -38,9 +38,13 @@ const TITLE_ENTRY := "出发——选择首个节点" # 入口停点 const TITLE_SINGLE := "继续前进" # 单链停点 const TITLE_FORK := "选择下一节点" # 岔路停点 const NODE_HALF := Vector2(26, 18) # 节点方块半宽高(52×36 占位画布参数——样张批终稿) +const LAYER_BASE_X := 70 # 首层节点原点 x(画布拟合与布局公式共用——T4 杂并:双写退役) +const LAYER_STEP_X := 122 # 层间横向步距(同上) +const SLOT_BASE_Y := 52 # 槽位原点 y(同上) +const SLOT_STEP_Y := 58 # 同层槽位纵向步距(同上) const HIT_PAD := 6.0 # 图上命中外扩(指宽容差) const CONFIRM_WINDOW_S := 3.0 # 放弃确认态窗口(秒——超时回落) -const COL_HP_BAR_BACK := Color8(40, 34, 28) # 迷你血条深底(样张 v1 冻结——对暗面板可辨) +const COL_HP_BAR_BACK := UiKit.HP_BAR_BACK # 迷你血条深底(样张 v1 冻结——对暗面板可辨;值单点收 UiKit) var _runner = null var _title: Label @@ -92,8 +96,7 @@ func _init() -> void: var top := HBoxContainer.new() top.add_theme_constant_override("separation", 12) box.add_child(top) - _level_label = UiKit.label("", 16, true) - _level_label.add_theme_color_override("font_color", UiKit.WARM) + _level_label = UiKit.label("", 16, true, UiKit.WARM) _level_label.custom_minimum_size.x = 420 top.add_child(_level_label) var gp_box := PanelContainer.new() @@ -102,11 +105,9 @@ func _init() -> void: var gp_row := HBoxContainer.new() gp_row.add_theme_constant_override("separation", 6) gp_box.add_child(gp_row) - _gp_value = UiKit.label("", 16, true) - _gp_value.add_theme_color_override("font_color", UiKit.GOLD) + _gp_value = UiKit.label("", 16, true, UiKit.GOLD) gp_row.add_child(_gp_value) - _title = UiKit.label(TITLE_FORK, 18, true) - _title.add_theme_color_override("font_color", UiKit.WARM) + _title = UiKit.label(TITLE_FORK, 18, true, UiKit.WARM) _title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_title) # 节点图画布(图上点击 = 唯一提交通道——布局决策 2;mouse_filter STOP 接悬停/点击) @@ -130,11 +131,9 @@ func _init() -> void: _bonus_row.add_child(bonus_box) var bonus_inner := VBoxContainer.new() bonus_box.add_child(bonus_inner) - var bonus_cap := UiKit.label("本关加成", 12, false) - bonus_cap.add_theme_color_override("font_color", UiKit.WARM_SOFT) + var bonus_cap := UiKit.label("本关加成", 12, false, UiKit.WARM_SOFT) bonus_inner.add_child(bonus_cap) - _bonus_value = UiKit.label("", 15, true) - _bonus_value.add_theme_color_override("font_color", UiKit.WARM) + _bonus_value = UiKit.label("", 15, true, UiKit.WARM) bonus_inner.add_child(_bonus_value) bottom.add_child(_bonus_row) var spacer := Control.new() @@ -148,25 +147,24 @@ func _init() -> void: visible = false -# 状态数据框工厂(直角细线数据框——pattern 5 族;hp_box = 带迷你血条。墨色按样张 v1 冻结: -# 暗底标签 WARM_SOFT/读数 WARM——§7.1 数据素面墨来自世界的墨在暗底侧的落法,L3 ≥4.5:1) +# 状态数据框工厂(直角细线数据框——pattern 5 族;with_bar = 带迷你血条(生命框),值读数随行挂回 +# _hp_value/_attr_value(T4 杂并:旧「caption == "生命"」文案做控制流退役——分组判据复用 with_bar)。 +# 墨色按样张 v1 冻结:暗底标签 WARM_SOFT/读数 WARM——§7.1 数据素面墨来自世界的墨在暗底侧的落法,L3 ≥4.5:1) func _status_box(caption: String, with_bar: bool) -> PanelContainer: var panel := PanelContainer.new() panel.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.DARK_UPCOMING, UiKit.PANEL_BORDER, 0, 1)) var inner := VBoxContainer.new() panel.add_child(inner) - var cap := UiKit.label(caption, 12, false) - cap.add_theme_color_override("font_color", UiKit.WARM_SOFT) + var cap := UiKit.label(caption, 12, false, UiKit.WARM_SOFT) inner.add_child(cap) if with_bar: _hp_bar = Control.new() _hp_bar.custom_minimum_size = Vector2(120, 6) _hp_bar.draw.connect(_draw_hp_bar.bind(_hp_bar)) inner.add_child(_hp_bar) - var value := UiKit.label("", 15, true) - value.add_theme_color_override("font_color", UiKit.WARM) + var value := UiKit.label("", 15, true, UiKit.WARM) inner.add_child(value) - if caption == "生命": + if with_bar: _hp_value = value else: _attr_value = value @@ -174,7 +172,7 @@ func _status_box(caption: String, with_bar: bool) -> PanelContainer: func _draw_hp_bar(bar: Control) -> void: - # 迷你血条(样张 v1 冻结:深底 Color8(40,34,28)(对暗面板可辨)+金填充——比例 = hero_hp / effective_max_hp) + # 迷你血条(样张 v1 冻结:深底 UiKit.HP_BAR_BACK #28221C(对暗面板可辨)+金填充——比例 = hero_hp / effective_max_hp) if _runner == null: return var max_hp := maxi(1, _runner.effective_max_hp()) @@ -268,7 +266,8 @@ func _fit_canvas(nodes: Array) -> void: maxl = maxi(maxl, int(layer[k])) for k in slot.keys(): maxslot = maxi(maxslot, int(slot[k])) - _content_size = Vector2(70 + maxl * 122 + NODE_HALF.x + 8.0, 52 + maxslot * 58 + NODE_HALF.y + 8.0) + _content_size = Vector2(LAYER_BASE_X + maxl * LAYER_STEP_X + NODE_HALF.x + 8.0, + SLOT_BASE_Y + maxslot * SLOT_STEP_Y + NODE_HALF.y + 8.0) _content_size = _content_size.max(Vector2(620, 240)) # 下限 = 旧定值画布(小图观感不变) _canvas.custom_minimum_size = _content_size @@ -450,7 +449,8 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary: var pos := {} var origin := (canvas.size - _content_size) / 2.0 for node in nodes: - pos[node] = origin + Vector2(70 + int(layer[node]) * 122, 52 + int(slot[node]) * 58) + pos[node] = origin + Vector2(LAYER_BASE_X + int(layer[node]) * LAYER_STEP_X, + SLOT_BASE_Y + int(slot[node]) * SLOT_STEP_Y) return pos diff --git a/game/presentation/views/node_overlay.gd b/game/presentation/views/node_overlay.gd index e80f9a8e..370f5a96 100644 --- a/game/presentation/views/node_overlay.gd +++ b/game/presentation/views/node_overlay.gd @@ -12,18 +12,8 @@ var _btn: Button func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(0, 0, 0, 0.6) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var box := VBoxContainer.new() - box.add_theme_constant_override("separation", 12) - center.add_child(box) - _label = UiKit.label("", 20, true) - _label.add_theme_color_override("font_color", UiKit.WARM) + var box: VBoxContainer = UiKit.modal_shell(self, Color(0, 0, 0, 0.6)).box + _label = UiKit.label("", 20, true, UiKit.WARM) _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_label) _btn = UiKit.button("继续") diff --git a/game/presentation/views/settings_overlay.gd b/game/presentation/views/settings_overlay.gd index c42d08f9..b5bf026b 100644 --- a/game/presentation/views/settings_overlay.gd +++ b/game/presentation/views/settings_overlay.gd @@ -30,23 +30,11 @@ func retarget(store) -> void: func _build() -> void: - var dim := ColorRect.new() - dim.color = UiKit.BG_OVERLAY - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var box := VBoxContainer.new() - box.custom_minimum_size = Vector2(560, 0) - box.add_theme_constant_override("separation", 8) - center.add_child(box) + var box: VBoxContainer = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(560, 0), 8).box - var title := UiKit.label("设置", 26, true) - title.add_theme_color_override("font_color", UiKit.WARM) + var title := UiKit.label("设置", 26, true, UiKit.WARM) box.add_child(title) - var hint := UiKit.label("系统级设置——独立存储不随存档(一安装一份,删游戏才消失)", 13, false) - hint.add_theme_color_override("font_color", UiKit.WARM_SOFT) + var hint := UiKit.label("系统级设置——独立存储不随存档(一安装一份,删游戏才消失)", 13, false, UiKit.WARM_SOFT) box.add_child(hint) box.add_child(HSeparator.new()) diff --git a/game/presentation/views/settlement_fx_view.gd b/game/presentation/views/settlement_fx_view.gd index da07d3a1..5221032a 100644 --- a/game/presentation/views/settlement_fx_view.gd +++ b/game/presentation/views/settlement_fx_view.gd @@ -47,8 +47,7 @@ func play_round_sweep(board_rect: Rect2) -> void: # 跨区弹道:glyph 字形自 from 飞向 to(0.35s + 轻微上抛弧线 + 终点淡出,自动回收)—— # 开格之因 → 站桩之果的因果可视承载(P2);crit 弹道由调用方传加重标记 func play_projectile(from: Vector2, to: Vector2, glyph: String, col: Color, crit: bool = false) -> void: - var shot := UiKit.label(glyph, 22 if crit else 16, true) - shot.add_theme_color_override("font_color", col) + var shot := UiKit.label(glyph, 22 if crit else 16, true, col) shot.mouse_filter = Control.MOUSE_FILTER_IGNORE shot.size = Vector2(24, 24) shot.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER @@ -92,8 +91,7 @@ func play_damage_number(pos: Vector2, text: String, col: Color, crit: bool = fal _fx_damage_marks += 1 # T17 修复(用户实机「完全没看到伤害数字」):26/32px 大字+1.1s 驻留——旧 20px/0.7s 在 # 结算排播节奏下视觉存在感不足;描边 4→6 同步加重(亮舞台底上保读数)。 - var lab := UiKit.label(text, 32 if crit else 26, true) - lab.add_theme_color_override("font_color", col) + var lab := UiKit.label(text, 32 if crit else 26, true, col) lab.add_theme_color_override("font_outline_color", Color(0.1, 0.08, 0.06, 0.9)) lab.add_theme_constant_override("outline_size", 6) lab.mouse_filter = Control.MOUSE_FILTER_IGNORE @@ -130,8 +128,7 @@ func play_round_banner(ri: int) -> void: var banner := Panel.new() banner.mouse_filter = Control.MOUSE_FILTER_IGNORE banner.add_theme_stylebox_override("panel", UiKit.panel_flat(Color(UiKit.BG_OVERLAY, 0.88), UiKit.GOLD_DEEP, 8, 2)) - var lab := UiKit.label("第 %d 轮" % ri, 22, true) - lab.add_theme_color_override("font_color", UiKit.GOLD) + var lab := UiKit.label("第 %d 轮" % ri, 22, true, UiKit.GOLD) lab.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) lab.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER lab.vertical_alignment = VERTICAL_ALIGNMENT_CENTER diff --git a/game/presentation/views/settlement_review_overlay.gd b/game/presentation/views/settlement_review_overlay.gd index 10e4ea3b..c9c0068d 100644 --- a/game/presentation/views/settlement_review_overlay.gd +++ b/game/presentation/views/settlement_review_overlay.gd @@ -11,20 +11,9 @@ var _log: RichTextLabel func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(UiKit.DIM, 0.62) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.gui_input.connect(_on_dim_gui_input) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var panel := PanelContainer.new() - center.add_child(panel) - var box := VBoxContainer.new() - box.custom_minimum_size = Vector2(760, 560) - box.add_theme_constant_override("separation", 6) - panel.add_child(box) + var shell := UiKit.modal_shell(self, Color(UiKit.DIM, 0.62), Vector2(760, 560), 6, true) + (shell.dim as ColorRect).gui_input.connect(_on_dim_gui_input) + var box: VBoxContainer = shell.box box.add_child(UiKit.label("结算明细回看(完整流水 · 逐行归因)", 18, true)) _log = RichTextLabel.new() _log.bbcode_enabled = true diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 7a515259..8f778a86 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -19,22 +19,11 @@ var _card_rows: Array[Dictionary] = [] # [{product_id, price, bought, btn}]— func _init() -> void: - var dim := ColorRect.new() - dim.color = Color(0, 0, 0, 0.6) - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var box := VBoxContainer.new() - box.add_theme_constant_override("separation", 12) - center.add_child(box) - var title := UiKit.label("商店", 26, true) - title.add_theme_color_override("font_color", UiKit.WARM) + var box: VBoxContainer = UiKit.modal_shell(self, Color(0, 0, 0, 0.6)).box + var title := UiKit.label("商店", 26, true, UiKit.WARM) title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(title) - _gp_label = UiKit.label("金币 0", 18, true) - _gp_label.add_theme_color_override("font_color", UiKit.GOLD) + _gp_label = UiKit.label("金币 0", 18, true, UiKit.GOLD) _gp_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER box.add_child(_gp_label) _cards_box = VBoxContainer.new() @@ -82,12 +71,10 @@ func _build_card(p: Dictionary) -> Dictionary: var name_l := UiKit.label(_card_title(p), 15, true) name_l.custom_minimum_size = Vector2(190, 0) row.add_child(name_l) - var price_l := UiKit.label("", 14, false) - price_l.add_theme_color_override("font_color", UiKit.WARM_SOFT) + var price_l := UiKit.label("", 14, false, UiKit.WARM_SOFT) price_l.custom_minimum_size = Vector2(110, 0) row.add_child(price_l) - var bought_l := UiKit.label("", 14, false) - bought_l.add_theme_color_override("font_color", UiKit.WARM_FAINT) + var bought_l := UiKit.label("", 14, false, UiKit.WARM_FAINT) row.add_child(bought_l) var btn := UiKit.button("购买") btn.pressed.connect(_emit_buy_requested.bind(p)) diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index e43c2ee4..47bfc9b2 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -143,8 +143,7 @@ func set_effects_cleared_feedback() -> void: return slot_row.visible = true slot_row.set_cleared_feedback() - var note := UiKit.label("状态随战斗结束消散", 11, false) - note.add_theme_color_override("font_color", UiKit.STONE_FAINT) + var note := UiKit.label("状态随战斗结束消散", 11, false, UiKit.STONE_FAINT) note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER note.mouse_filter = Control.MOUSE_FILTER_IGNORE var parent := slot_row.get_parent() @@ -418,11 +417,11 @@ func _draw() -> void: var ang := TAU * float(i) / 24.0 pts.append(sh_center + Vector2(cos(ang) * sh_rx, sin(ang) * sh_ry)) draw_colored_polygon(pts, Color(0, 0, 0, 0.27)) - # 石板地台(v3.5——方向稿取色:板身 (150,130,100)/描边 (90,76,58)/顶缘受光线 2px (196,172,132); - # 几何沿用现行底座位〔margin 26/高 10→方向稿 18/12——按方向稿对齐〕) - draw_rect(Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0), Color8(150, 130, 100), true) - draw_rect(Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0), Color8(90, 76, 58), false, 1.0) - draw_rect(Rect2(18.0, box.y - 18.0, box.x - 36.0, 2.0), Color8(196, 172, 132), true) + # 石板地台(v3.5——方向稿取色:板身/描边/顶缘受光线 2px——取色单点收 UiKit.STAND_STONE 族 + # (T4 裸色收口);几何沿用现行底座位〔margin 26/高 10→方向稿 18/12——按方向稿对齐〕) + draw_rect(Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0), UiKit.STAND_STONE, true) + draw_rect(Rect2(18.0, box.y - 18.0, box.x - 36.0, 12.0), UiKit.STAND_STONE_EDGE, false, 1.0) + draw_rect(Rect2(18.0, box.y - 18.0, box.x - 36.0, 2.0), UiKit.STAND_STONE_RIM, true) # 血条(直角递减):敌附顺位徽标(GOLD_DEEP ①②③)——旧 top VBox 几何等价(左 8 / 右 -8) var bar_x := 8.0 diff --git a/game/presentation/views/status_slot_row.gd b/game/presentation/views/status_slot_row.gd index 8557bb1a..b62e3911 100644 --- a/game/presentation/views/status_slot_row.gd +++ b/game/presentation/views/status_slot_row.gd @@ -64,8 +64,7 @@ func _init() -> void: _tip.visible = false var sb := UiKit.panel_flat(UiKit.TIP_BG, UiKit.GOLD, 2, 1, Vector4(6, 3, 6, 3)) _tip.add_theme_stylebox_override("panel", sb) - _tip_label = UiKit.label("", 13, false) - _tip_label.add_theme_color_override("font_color", UiKit.WARM) + _tip_label = UiKit.label("", 13, false, UiKit.WARM) _tip_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART _tip_label.custom_minimum_size = Vector2(210, 0) _tip.add_child(_tip_label) diff --git a/game/presentation/views/toast_stack.gd b/game/presentation/views/toast_stack.gd index 03f199a5..755a9798 100644 --- a/game/presentation/views/toast_stack.gd +++ b/game/presentation/views/toast_stack.gd @@ -43,8 +43,7 @@ func show_toast(text: String, tone: String = "info") -> void: col = COL_GOLD sb.border_color = col row.add_theme_stylebox_override("panel", sb) - var lab := UiKit.label(("⚠ " if tone == "warn" else "") + text, 14, tone != "info") - lab.add_theme_color_override("font_color", col) + var lab := UiKit.label(("⚠ " if tone == "warn" else "") + text, 14, tone != "info", col) lab.clip_text = true row.add_child(lab) _slot_box.add_child(row) diff --git a/game/presentation/views/tune_panel.gd b/game/presentation/views/tune_panel.gd index 54b7e619..01b9edfe 100644 --- a/game/presentation/views/tune_panel.gd +++ b/game/presentation/views/tune_panel.gd @@ -110,35 +110,23 @@ func _reset_btn_pressed() -> void: func effective_atk() -> int: - return _scaled(_base_atk, atk_pct()) + return UiKit.scaled(_base_atk, atk_pct()) func effective_max_hp() -> int: - return _scaled(_base_hp, hp_pct()) + return UiKit.scaled(_base_hp, hp_pct()) # ---- 内部构建 ---- func _build() -> void: - var dim := ColorRect.new() - dim.color = UiKit.BG_OVERLAY - dim.set_anchors_preset(Control.PRESET_FULL_RECT) - add_child(dim) - var center := CenterContainer.new() - center.set_anchors_preset(Control.PRESET_FULL_RECT) - dim.add_child(center) - var box := VBoxContainer.new() - box.custom_minimum_size = Vector2(640, 0) - box.add_theme_constant_override("separation", 8) - center.add_child(box) + var box: VBoxContainer = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(640, 0), 8).box - var title := UiKit.label("测试用 · 角色数值调整板", 26, true) - title.add_theme_color_override("font_color", UiKit.WARM) + var title := UiKit.label("测试用 · 角色数值调整板", 26, true, UiKit.WARM) box.add_child(title) var desc := UiKit.label( "模拟玩家在循环中解锁天赋获得的属性提升(仅测试工具,不落盘)。\n百分比叠加基础值:攻击 %d / 生命 %d。影响所有模式(手动与模拟)。" % [_base_atk, _base_hp], - 13, false) - desc.add_theme_color_override("font_color", UiKit.WARM_SOFT) + 13, false, UiKit.WARM_SOFT) box.add_child(desc) box.add_child(HSeparator.new()) @@ -210,10 +198,6 @@ func _sync_labels() -> void: _base_atk, effective_atk(), _base_hp, effective_max_hp()] -func _scaled(base: int, pct: int) -> int: - return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0))) - - func _emit_simulate_requested() -> void: simulate_requested.emit(int(_sim_count.value)) diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 09e339a9..14b81b8a 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -73,10 +73,36 @@ const DIM := Color(0.08, 0.06, 0.05) # 遮罩基色(用时乘 alpha const TOAST_BG := Color(0.94, 0.91, 0.84, 0.95) # toast 面底 const TIP_BG := Color(0.16, 0.12, 0.09, 0.96) # tooltip 深底 +# 呈现杂色档(T4 裸色收口批 2026-10-02——各视图裸 Color8 字面量单点化;值 = 收口时各来源行现值快照) +const STAGE_BG := Color8(62, 52, 42) # B 区舞台暗底(方向稿 render_battle_screen——battle_stage_view 面板底) +const BEAM_LIGHT := Color8(255, 236, 200) # B 区顶光柱暖光(用时乘 alpha 渐隐——battle_stage_view 光柱层) +const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色) +const STAND_STONE_EDGE := Color8(90, 76, 58) # 站桩石板地台·描边 +const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘受光线(与 board_cell_unit.FACADE_HIGHLIGHT_COL 异源同值——并档归清扫卡) +const CAMP_HEADER_BAND := Color8(122, 82, 50) # 营地柜台头带深棕(4-camp.png 方向稿取色) +const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上) +const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上) +const HP_BAR_BACK := Color8(40, 34, 28) # 迷你血条深底(关卡画面样张 v1 冻结——对暗面板可辨) + # BBCode 十六进制(RichTextLabel 用;与上同值) const HEX_GOLD := "f0a828" const HEX_INK := "33261b" const HEX_ON_GOLD := "3a2a08" +# 结算行/叙事行语义档(T4 收口批——battle_log_view 行格式化与 narrator 横幅行的裸 hex 单点化; +# 名随呈现语义、值 = 2026-09 起各行现值快照,色值逐字节不变) +const HEX_LOG_ROUND := "9a8a72" # 轮次前缀/零值行暖灰石 +const HEX_LOG_GAIN := "e0b860" # 收益金(GP 袋/商店行/败判定/节点横幅) +const HEX_LOG_LOSS := "c47c7c" # 损失红(GP 流失/负效果) +const HEX_LOG_HEAL := "7cc47c" # 治疗绿(效果正值/治疗行/恢复点行) +const HEX_LOG_DMG_NUM := "d9402a" # 伤害数字朱(放大加色——数字即信息主角) +const HEX_LOG_DMG_KILL := "e07050" # 击杀行正文橙红 +const HEX_LOG_DMG_BODY := "e0a050" # 伤害行正文橙 +const HEX_LOG_VOIDED := "b08060" # 未生效行暗橙(⚠ 划线) +const HEX_LOG_CAST := "b09a72" # 施放行暖石(✦ 具名技能) +const HEX_LOG_DECAY := "8a9ab0" # 状态衰减蓝灰 +const HEX_LOG_WIN := "80d080" # 胜利判定绿(模拟报告通关率同档) +const HEX_ROUTE_NOTE := "d0c090" # 奖励点预告暗金(narrator 行) +const HEX_SEED_NOTE := "c8a860" # 种子/开场行暗金(narrator 行) # 语义别名(同值异名——语义站点用别名,值仍单点) const DOT_OFF := PANEL_BORDER @@ -163,15 +189,59 @@ static func panel_flat(bg: Color, border: Color, corner: int = 0, border_w: int return sb -static func label(text: String, size: int, bold: bool) -> Label: +# color 缺省哨兵 = alpha<0(真实用色不取负 alpha)——传色即一次到位(旧「label() 后跟 +# add_theme_color_override」两行式收单行);省略时保持旧默认:bold=INK_LABEL / 非 bold=主题缺省 +static func label(text: String, size: int, bold: bool, color: Color = Color(-1, -1, -1, -1)) -> Label: var l := Label.new() l.text = text l.add_theme_font_size_override("font_size", size) - if bold: + if color.a >= 0.0: + l.add_theme_color_override("font_color", color) + elif bold: l.add_theme_color_override("font_color", INK_LABEL) return l +# ── 模态壳工厂(T4 收口批 2026-10-02——各 overlay 手搓 dim+Center+VBox/Panel 骨架单点化)── +# dim = 全屏半透明遮罩(全屏锚定纪律同既有各 overlay);center 全屏锚定居中;with_panel = 内容包 +# PanelContainer(panel_sb 缺省 = 主题默认板——settlement_review 同款);box = 内容列 +#(min_size/separation 参数化,min_size ZERO = 不设)。返回 Dictionary{dim, center, panel, box} +#——探针直读的私有件由调用方按名摘引,结构同名同层级不变。 +static func modal_dim(parent: Control, dim_color: Color) -> ColorRect: + var dim := ColorRect.new() + dim.color = dim_color + dim.set_anchors_preset(Control.PRESET_FULL_RECT) + parent.add_child(dim) + return dim + + +static func modal_shell(parent: Control, dim_color: Color, min_size := Vector2.ZERO, separation := 12, + with_panel := false, panel_sb: StyleBox = null) -> Dictionary: + var dim := modal_dim(parent, dim_color) + var center := CenterContainer.new() + center.set_anchors_preset(Control.PRESET_FULL_RECT) + dim.add_child(center) + var panel: PanelContainer = null + var host: Control = center + if with_panel: + panel = PanelContainer.new() + if panel_sb != null: + panel.add_theme_stylebox_override("panel", panel_sb) + center.add_child(panel) + host = panel + var box := VBoxContainer.new() + if min_size != Vector2.ZERO: + box.custom_minimum_size = min_size + box.add_theme_constant_override("separation", separation) + host.add_child(box) + return {"dim": dim, "center": center, "panel": panel, "box": box} + + +# 属性百分比合成(main._debug_hero_override 与 TunePanel 生效值预览共用单点——两处同式实现收口) +static func scaled(base: int, pct: int) -> int: + return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0))) + + static func button(text: String) -> Button: var b := Button.new() b.text = text From 54ac8759c9ad72e95bf008d41de3894d89100343 Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 09:25:23 +0800 Subject: [PATCH 7/8] =?UTF-8?q?T7=20=E6=AD=BB=E4=BB=A3=E7=A0=81=E4=B8=8E?= =?UTF-8?q?=E6=AE=8B=E7=95=99=E6=B8=85=E6=89=AB=E6=AE=B5:=2016=E9=A1=B9?= =?UTF-8?q?=E9=9B=B6=E4=BA=89=E8=AE=AE=E6=AD=BB=E4=BB=A3=E7=A0=81=E7=9B=B4?= =?UTF-8?q?=E5=88=A0=EF=BC=88equivalence=5Fsupply/pool=5Flayers/index=5Fof?= =?UTF-8?q?/has=5Fvariant/rows=5Fby=5Fpool/SUB=5FREASONS/ui=5Fkit=E6=AD=BB?= =?UTF-8?q?=E8=89=B2=E6=9D=BF=E4=BA=94=E5=B8=B8=E9=87=8F/reset=5Frequested?= =?UTF-8?q?/set=5Flegend=5Fenabled/is=5Flevel=5Fmode/legend.toggle/set=5Fi?= =?UTF-8?q?dle=C3=972/COL=5FPATH/COL=5FINK/rebuild=E6=AD=BB=E5=AE=88?= =?UTF-8?q?=E5=8D=AB/=5Fnormalize=E5=90=88=E5=B9=B6/FACADE=E5=B9=B6?= =?UTF-8?q?=E6=A1=A3=E2=80=94=E2=80=94=E5=88=A0=E5=89=8D=E9=80=90=E9=A1=B9?= =?UTF-8?q?=E5=85=A8=E4=BB=93grep=E9=9B=B6=E8=B0=83=E7=94=A8=E6=A0=B8?= =?UTF-8?q?=E5=AE=9E=EF=BC=8Cnext=5Fcandidates=E6=9C=89level=5Fautoplay?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E5=AD=98=E6=B4=BB=E6=9C=AA=E5=88=A0=3D?= =?UTF-8?q?=E5=AE=A1=E8=AE=A1=E8=AF=AF=E6=8A=A5=E7=BA=A0=E6=AD=A3=EF=BC=89?= =?UTF-8?q?+T17=E5=BD=92=E5=9B=A0=E4=BB=B6=E6=92=A4=E9=99=A4=EF=BC=88choic?= =?UTF-8?q?es=5Fdbg=E4=B8=89=E6=AE=B5+t17dump=E4=B8=A4=E6=AE=B5=EF=BC=8CT1?= =?UTF-8?q?7=5FLOCK=5FDEBUG=E5=85=A8=E4=BB=93=E5=BD=92=E9=9B=B6=EF=BC=89+?= =?UTF-8?q?=E5=BB=B6=E8=BF=9F=E4=B8=89=E4=BB=B6=EF=BC=88has=5Funrevealed?= =?UTF-8?q?=E4=B8=8B=E6=B2=89/is=5Fitem=5Funlocked=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E5=99=A8/rap=E6=AE=8B=E9=93=BEsnapshot=E9=9D=A2=EF=BC=89+?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=8D=81=E4=BB=B6=EF=BC=88=E8=BF=87=E6=9C=9F?= =?UTF-8?q?=E6=B3=A8=E9=87=8A/=E9=94=99=E6=8C=82=E6=B3=A8=E9=87=8A/4242?= =?UTF-8?q?=E6=96=87=E6=A1=88/README=E7=8E=B0=E8=A1=8C=E5=8C=96/critical-p?= =?UTF-8?q?aths=E9=81=93=E5=85=B7=E7=AA=97=E5=8F=A3=E9=80=80=E5=BD=B9?= =?UTF-8?q?=E6=94=B9=E5=86=99=EF=BC=89=EF=BC=9B=E9=97=B8=3D=E5=86=B2?= =?UTF-8?q?=E7=83=9F636/0+run=5Fall=2015=E5=A5=97+=E5=85=AB=E6=8E=A2?= =?UTF-8?q?=E9=92=88=E5=85=A8=E7=BB=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- game/README.md | 4 +++- game/core/board_data.gd | 8 -------- game/core/meta_progression.gd | 5 +++++ game/core/minesweeper_rules.gd | 13 ++++++++++--- game/core/reward_economy.gd | 18 ------------------ game/core/save_manager.gd | 6 +++--- game/core/status_container.gd | 8 -------- game/data/schemas/reward_table.gd | 11 ++--------- game/presentation/aux_interact_controller.gd | 8 ++------ game/presentation/debug_session.gd | 4 ---- game/presentation/main.gd | 10 +--------- game/presentation/views/ap_bar_view.gd | 4 ---- game/presentation/views/battle_log_view.gd | 1 - game/presentation/views/board_cell_unit.gd | 3 +-- game/presentation/views/board_view.gd | 3 --- .../presentation/views/hero_info_panel_view.gd | 6 ------ game/presentation/views/legend_overlay.gd | 9 +-------- game/presentation/views/shop_overlay_view.gd | 2 +- game/presentation/views/standee_body.gd | 5 ----- game/presentation/views/tune_panel.gd | 6 ++---- game/presentation/views/ui_kit.gd | 7 +------ game/smoke/level_autoplay.gd | 1 - game/smoke/smoke_all.gd | 2 +- game/smoke/ui_click_probe.gd | 9 --------- game/tests/README.md | 4 ++-- game/tests/smoke/critical-paths.md | 15 +++++++-------- game/tests/unit/README.md | 2 +- 27 files changed, 43 insertions(+), 131 deletions(-) diff --git a/game/README.md b/game/README.md index 0ec0e54b..e205732f 100644 --- a/game/README.md +++ b/game/README.md @@ -33,6 +33,8 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_probe.t Godot_v4.7.2-stable_win64_console.exe --headless --path . res://smoke/ui_tune_probe.tscn ``` +其余七场景探针(九探针全 roster 登记——ui_click_probe 行为链 / ui_input_probe 输入合成 / ui_layout_probe 布局 / shot_probe·fx_probe·fx_shot_probe·board_shot_probe 画面与特效采样,同上式运行)+ 板图校准工具 `smoke/calibrate_boards.gd`(board_calibration.csv 重跑入口,--script 运行): + 全冒险期望扫描(零加成三关连探批量——通关率/止步分布/败退节点;调参期难度读数一律走全冒险口径): ``` @@ -79,7 +81,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s - `B1BattleOrchestrator`:begin_battle 门序列增 `b4.notify_battle_start()`(编排唯一性不变)。 - 表/护栏:RewardTable `POOL_TAGS` + `active_inject`;RewardConfigTable + `active_p0/active_decay`、k_dmg 1→0(F5 休眠);ConfigDB 跨表护栏更新(clear_no_value 含即时类 / no_damage_row 扫描全池活行 / 新增 inject_pool 护栏:非可选类拒绝 + 空池致命)。 -**实测(N=200 板/深度,工具 reward_expectation.gd)**:注入 1.00 件/板(j=0,多件板 0)· 占比 小刀 59–65% / 精工 25–29% / 治疗包 7–17%(名义 60/30/10)· 格池零可选类泄漏 ✓。**实际获取期望**(level_monte_carlo「可选类/战」列,N=15):基线 0.88 → +200% 攻击 0.68 件/战(低于分配保证 1.0 的机理 = 注入格均匀随机,落在残余格/速杀未挖区时该件不获取;战斗末无兜底补发——**是否加"战斗末 j==0 补发 1 件"待用户拍板**)。每关合计 ≈ 4.5–6.5 件(原 24.8)✓ 拍板目标达成。 +**实测(N=200 板/深度,工具 reward_expectation.gd)**:注入 1.00 件/板(j=0,多件板 0)· 占比 小刀 59–65% / 精工 25–29% / 治疗包 7–17%(名义 60/30/10)· 格池零可选类泄漏 ✓。**实际获取期望**(level_monte_carlo「可选类/战」列,N=15——工具已退役,数据归档):基线 0.88 → +200% 攻击 0.68 件/战(低于分配保证 1.0 的机理 = 注入格均匀随机,落在残余格/速杀未挖区时该件不获取;战斗末无兜底补发——**是否加"战斗末 j==0 补发 1 件"待用户拍板**)。每关合计 ≈ 4.5–6.5 件(原 24.8)✓ 拍板目标达成。 **⚠️ 供给侧副作用(本轮扫描发现,待拍板)**:可选类退出格子池后,其 25% 概率质量被动流向即时类——攻防 buff 每格频率从 44% 升至 59%,基线通关率 55%→80%(N=15)、整关承伤 127→80:**难度不降反升方向反了**。即时类是"每格必有货"锁定设计下无法清空的(除非引入空白格 = 违反锁定事实)。候选杠杆:① 接受更快的 buff 节奏,敌 HP 曲线上调对冲(C3 主杠杆)② 下调 attack_up/defense_up 在即时池内的相对权重(压 buff 频率)③ buff 幅度下调(触碰 B3 表值)。 diff --git a/game/core/board_data.gd b/game/core/board_data.gd index 7548cb3f..a02ffacf 100644 --- a/game/core/board_data.gd +++ b/game/core/board_data.gd @@ -34,10 +34,6 @@ static func row_major_key(c: Vector2i) -> int: return c.y * 1000 + c.x -func index_of(c: Vector2i) -> int: - return c.y * width + c.x - - func is_mine(c: Vector2i) -> bool: return mines.has(c) @@ -54,10 +50,6 @@ func variant_ids(c: Vector2i) -> Array: return variants.get(c, []) -func has_variant(c: Vector2i, vid: String) -> bool: - return variant_ids(c).has(vid) - - func neighbors8(c: Vector2i) -> Array: var out: Array[Vector2i] = [] for dy in range(-1, 2): diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 4bf60bb4..96bc2968 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -81,6 +81,11 @@ func _workshop_progress(channel: String) -> int: return 0 # 未知渠道恒 0 = 永不解锁(fail-safe 非致命——渠道词表校验兜底,不静默误解锁) +# 道具解锁读数访问器(组合根/呈现层解锁门控查询面——禁再穿透 meta.save 直读 save_manager) +func is_item_unlocked(item_id: String) -> bool: + return save.item_unlocked(item_id) + + # 工坊六行只读投影(呈现层数据源):{item_id, display_name, threshold, unlocked, progress} func workshop_status() -> Array: var out: Array[Dictionary] = [] diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 6bb50563..548bf0ba 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -13,8 +13,6 @@ signal cell_revealed(evt: Dictionary) signal flag_judged(evt: Dictionary) signal round_end_board(payload: Dictionary) # 双发射源之棋盘路径(排雷玩法 外发;stopped 由 行动经济 直发) -const SUB_REASONS := ["not_adjacent", "target_revealed", "target_concealed", "flag_present", "no_path", "out_of_catalog"] # flag_absent 随 flag_pull 退役(2026-09-19) - var db: ConfigDb var action_economy: ActionEconomy # 棋盘状态私有(E2 封装收口 2026-09-13):对外只暴露 current_board()/reveal_record() 等查询面, @@ -75,6 +73,15 @@ func clear_map_cleared() -> bool: return safe_remaining() == 0 +# 未开格存在查询(aux 道具 usable 预判消费——全板扫描只读面) +func has_unrevealed() -> bool: + for y in _board.height: + for x in _board.width: + if not _revealed.has(Vector2i(x, y)): + return true + return false + + # ---- install(编排边界相位 = 行动经济 Idle/Waiting;Active 拒绝——防轮内换板)---- # during_round = 轮内刷新编排路径(2026-09-12 方案 A,战斗结算 唯一调用方):Active 相位合法—— @@ -107,11 +114,11 @@ func install_board(b: BoardData, during_round := false) -> Dictionary: return _outcome(true, "executed", "", "") -# during_round = 轮内刷新编排路径(2026-09-12 方案 A):Active 相位合法——换图前退役旧板 func flag_count() -> int: return _flags.size() +# during_round = 轮内刷新编排路径(2026-09-12 方案 A):Active 相位合法——换图前退役旧板 func retire_board(during_round := false) -> Dictionary: if _board == null: return _outcome(true, "executed", "", "") # 幂等受理 diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index fae67b5c..37052bca 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -172,24 +172,6 @@ func polarity_row(cell_type: String, polarity: String) -> Dictionary: return {} -# ---- F6 当量供给(行动经济 条款①对照实物输入——全活行投影 + tier_min_buff)---- - -func equivalence_supply() -> Dictionary: - var live: Array[Dictionary] = [] - var tier_min_buff := 99 - for row in db.reward.rows: - if int(row.weight) <= 0: - continue - live.append(row.duplicate()) # E6:表行值拷贝出口——消费方禁改配置源 - # tier_min_buff = 我侧增益(hero_offense/hero_defense 池的 percent 正向行——attack_up/defense_up)最低档 - if String(row.get("pool_tag", "")) == "cell" and String(row.get("polarity", "")) == "positive" \ - and String(row.get("effect_id", "")) != "": - var eff_row: Dictionary = db.status_effect_row(String(row.effect_id)) - if not eff_row.is_empty() and String(eff_row.get("target_side", "")) == "hero": - tier_min_buff = mini(tier_min_buff, int(row.tier)) - return {"live_rows": live, "tier_min_buff": tier_min_buff} - - # ---- F1 选行算子纯函数 ---- func weighted_pick(rows: Array, r: int) -> String: diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 22126da8..e9c91a3f 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -39,7 +39,7 @@ const FORGE_ATTRS := ["max_hp", "atk", "armor_class"] # 属性锻造三档合 var _path: String # 存档文件路径(默认 user://save.json;冒烟注入隔离路径) var _read_only := false # 高版本拒读置位——write_to_disk 一律拒绝(契约 4 禁写) var _data: Dictionary[String, Variant] = {} # 内存数据模型(_init 即内置默认;load 后为档案内容) -var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;生产链现为空——骨架先行) +var _migrations: Dictionary[int, Callable] = {} # from_version → Callable(data)->data(升一版;v1→v3 生产迁移链现行——_init 自注册) func _init(path: String = "user://save.json") -> void: @@ -155,7 +155,7 @@ func write_to_disk() -> bool: # 迁移注册口:from_version → from_version+1 的升级函数(读档时链式调用)。fn 契约: -# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移当前为空(v1 首版), +# Dictionary → Dictionary,须写回 version = from_version + 1。生产迁移链现行(v1→v3,_init 自注册), # 冒烟经本口验证链式升级。 func register_migration(from_version: int, fn: Callable) -> void: _migrations[from_version] = fn @@ -265,10 +265,10 @@ func _corrupt_fallback(reason: String) -> bool: # 迁移后/低版本档字段补齐(改版不弃档:缺新域按默认补齐,既有值保留) func _normalize(data: Dictionary) -> Dictionary[String, Variant]: var merged := _default_data() + var def_meta: Dictionary = merged["meta"] # 补齐源引用(先于 data 覆盖取出——data 自带 meta 时 merged["meta"] 被换走,此引用仍指默认值) for key in data: merged[key] = data[key] var meta: Dictionary = merged.get("meta", {}) - var def_meta: Dictionary = _default_data()["meta"] for key in def_meta: if not meta.has(key): meta[key] = def_meta[key] diff --git a/game/core/status_container.gd b/game/core/status_container.gd index 718d54a7..30397dc0 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -106,14 +106,6 @@ func query_active(side: String) -> Array: return out -# 池层数读值(战斗结算·战斗段伏击池 L 的消费面) -func pool_layers(effect_id: String) -> int: - for inst in _instances: - if inst.effect_id == effect_id: - return inst.layers - return 0 - - # 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载) func decay_round() -> int: if not _open: diff --git a/game/data/schemas/reward_table.gd b/game/data/schemas/reward_table.gd index d97c6712..f8ff8f76 100644 --- a/game/data/schemas/reward_table.gd +++ b/game/data/schemas/reward_table.gd @@ -44,7 +44,8 @@ func validate() -> Array: errs.append({"category": "enum", "message": "%s polarity 出域: %s" % [rid, pol]}) var atk_type: String = String(row.get("attack_type", "")) if atk_type != "" and not ["normal", "pierce", "magic"].has(atk_type): - errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]}) # aux 投放行专属校验(迭代 7 T1):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层 + errs.append({"category": "enum", "message": "%s attack_type 出域 {normal, pierce, magic}: %s" % [rid, atk_type]}) + # aux 投放行专属校验(迭代 7 T1):极性恒正/加权入池/effect_id=item_id 非空(悬空在 config_db 跨表层 # 对 aux_items 词表校验——reward_dangling_aux)/无 power 无区间(数值语义不复用,道具效果读 AuxItemTable) if ctype == AUX_ITEM_TYPE: # 负向行 = 雷格极性字典占位(雷安同池摇号下雷格可摇中 aux 身份——负路由零兑付兜底, @@ -118,14 +119,6 @@ func validate() -> Array: return errs -func rows_by_pool(pool_tag: String) -> Array: - var out: Array[Dictionary] = [] - for row in rows: - if row.get("pool_tag", "") == pool_tag: - out.append(row) - return out - - func row_by_id(rid: String) -> Dictionary: for row in rows: if row.get("reward_id", "") == rid: diff --git a/game/presentation/aux_interact_controller.gd b/game/presentation/aux_interact_controller.gd index 40580426..c9374082 100644 --- a/game/presentation/aux_interact_controller.gd +++ b/game/presentation/aux_interact_controller.gd @@ -41,7 +41,7 @@ func unlocked_set() -> Dictionary: return out for row in db.aux_active_rows(): var iid := String(row.get("item_id", "")) - if meta.save.item_unlocked(iid): + if meta.is_item_unlocked(iid): out[iid] = true return out @@ -108,11 +108,7 @@ func inventory_projection() -> Array: var cb := Callable() if ms != null: cb = func(_c: Vector2i) -> bool: - for y in ms.current_board().height: - for x in ms.current_board().width: - if not ms.is_revealed(Vector2i(x, y)): - return true - return false + return ms.has_unrevealed() # 只读查询面(排雷玩法 has_unrevealed——手搓全板双循环收口) var out: Array = [] for rec in runner.aux_inventory.query_inventory(cb): var arow: Dictionary = db.aux_item_row(String(rec.get("item_id", ""))) diff --git a/game/presentation/debug_session.gd b/game/presentation/debug_session.gd index d1c34fbf..4979cb51 100644 --- a/game/presentation/debug_session.gd +++ b/game/presentation/debug_session.gd @@ -33,10 +33,6 @@ func _init(db_ref, rng_ref, hero_override: Callable, seed_value: Callable) -> vo _seed_value = seed_value -func is_level_mode() -> bool: - return _level_mode - - # 单场遭遇原语(2026-09-13 用户裁决:单场用户入口已删——本方法仅 ui_probe 单场段使用; # 无延续、standalone 终局语义保留供探针战斗收尾) func start_single(level: int) -> bool: diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 87af7d65..959865d6 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -870,9 +870,6 @@ func _on_node_continue() -> void: func _on_choices_presented(candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(候选按钮退役 2026-09-28——战斗胜利/兑付确认统一到达此处) - if OS.get_environment("T17_LOCK_DEBUG") != "": - print("[choices_dbg] handler entered cands=%d runner_null=%s node=%s" % [ - candidates.size(), str(runner == null), str(runner.node_index) if runner != null else "-"]) if runner == null: return log_view.append_bb(Narrator.route_line(candidates, runner)) @@ -882,8 +879,6 @@ func _on_choices_presented(candidates: Array) -> void: # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 if _adventure_mode and runner.current_battle() == null \ and String(runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): - if OS.get_environment("T17_LOCK_DEBUG") != "": - print("[choices_dbg] -> battle stop overlay (node %s)" % str(runner.node_index)) _settlement_stop_pending = true _pending_candidates = candidates.duplicate() _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点", @@ -892,9 +887,6 @@ func _on_choices_presented(candidates: Array) -> void: _open_modal(_end_overlay) _refresh_presentations() return - if OS.get_environment("T17_LOCK_DEBUG") != "": - print("[choices_dbg] -> level_screen (node %s type %s cands %d)" % [ - str(runner.node_index), String(runner.current_node().get("type", "")), candidates.size()]) _enter_level_screen() level_screen.open_choices(candidates, runner) _refresh_presentations() @@ -1109,7 +1101,7 @@ func _on_refresh_confirmed() -> void: board_zone.toast.show_toast("RAP−%d · 旧板旗作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold") else: # 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断) - var rap: int = int(settlement.action_economy.rap) if settlement != null else 0 + var rap: int = int(settlement.presentation_snapshot().get("rap", 0)) if settlement != null else 0 var cost: int = int(db.action_economy.rap_refresh_cost) var result := "rejected_rap" if rap < cost else "rejected_cap" logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1}) diff --git a/game/presentation/views/ap_bar_view.gd b/game/presentation/views/ap_bar_view.gd index c4afab2c..5ec1450c 100644 --- a/game/presentation/views/ap_bar_view.gd +++ b/game/presentation/views/ap_bar_view.gd @@ -171,10 +171,6 @@ func _render_rap_dots(rap: int) -> void: d.color = COL_GOLD if i < shown else COL_DOT_OFF -func set_legend_enabled(on: bool) -> void: - _btn_legend.disabled = not on - - # 探针/装配复用:刷新按钮可用性(预判 = RAP 足额 + 上限未到) func is_refresh_ready() -> bool: return not _btn_refresh.disabled diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index 009cb04e..37ed2808 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -20,7 +20,6 @@ const BAND_H_DIG := 24 const BAND_H_SETTLE := 84 const AGG_TYPES := ["reward_ineffective"] const COL_GOLD := UiKit.HEX_GOLD -const COL_INK := UiKit.HEX_INK var _db var _log: RichTextLabel diff --git a/game/presentation/views/board_cell_unit.gd b/game/presentation/views/board_cell_unit.gd index 2134a0b9..5bc58028 100644 --- a/game/presentation/views/board_cell_unit.gd +++ b/game/presentation/views/board_cell_unit.gd @@ -67,7 +67,6 @@ var bevel := 0.0: # 顶面倒角强度(0=关——立体原 const FACADE_HIGHLIGHT_PX := 2.0 # 口沿高光线厚(像素) const FACADE_NEAR_FRAC := 0.16 # 立面近段高(格边比——研究件 10/64) const FACADE_FAR_FRAC := 0.25 # 立面远段高(格边比——研究件 16/64) -const FACADE_HIGHLIGHT_COL := Color8(196, 172, 132) # 口沿高光(研究件取色暖米白) const FACADE_NEAR_COL := Color(0, 0, 0, 0.60) # 立面近段黑罩(墙顶 132→52 实测 α≈0.61) const FACADE_FAR_COL := Color(0, 0, 0, 0.82) # 立面远段黑罩(墙顶 132→24 实测 α≈0.82) const PROJECTION_NEAR_COL := Color(0, 0, 0, 0.28) # 投影贴墙段(地面 66→46 实测 α≈0.28) @@ -251,7 +250,7 @@ func _draw_south_facade() -> void: var y_far := edge - far_h var y_near := y_far - near_h var y_hl := y_near - FACADE_HIGHLIGHT_PX - draw_rect(Rect2(0.0, y_hl, edge, FACADE_HIGHLIGHT_PX), FACADE_HIGHLIGHT_COL, true) + draw_rect(Rect2(0.0, y_hl, edge, FACADE_HIGHLIGHT_PX), UiKit.STAND_STONE_RIM, true) # 口沿高光(UiKit.STAND_STONE_RIM 并档——同值单点,清扫批 2026-10-02) draw_rect(Rect2(0.0, y_near, edge, near_h), FACADE_NEAR_COL, true) draw_rect(Rect2(0.0, y_far, edge, far_h), FACADE_FAR_COL, true) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 3ddaee05..2001c672 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -35,7 +35,6 @@ const COL_DIGIT := UiKit.INK # 数字——墨族 #33261B(对 const COL_HERO := UiKit.HERO_BLUE # 小人边框——小人蓝 #2E6E8E const COL_FLAG := UiKit.SEAL_BLACK # 封印黑 #000000(旗朱→封印紫→封印黑,2026-09-28 用户拍板) const COL_GOLD := UiKit.GOLD # 奖励金 #F0A828 -const COL_PATH := UiKit.HERO_BLUE # 路径预览点迹——小人蓝细线 const COL_CONTACT := UiKit.CONTACT_GLOW # 接触带微光基色(蓝族浅一档·近白蓝——§3.2 v2.8;旧绿相退役 2026-09-26) const COL_HL_TARGET := UiKit.GOLD # 悬停目标格边框(金) const REWARD_BADGE := { # 已开格格面资源身份 icon(cell_type 九型键;极性视觉归批 2) @@ -269,8 +268,6 @@ func rebuild(board: BoardData, minesweeper) -> void: _hover = Vector2i(-9, -9) for c in _board.compute_r0(): _r0[c] = true - if _board == null: - return # 瓦片套随板图型切换(T6 换色批——board_gen 词表 v1.1 dungeon/underdark/deep_underdark; # 同图型重复 rebuild 零重载,_tiles_board_type 守卫) var bt: String = _board.board_type if _board.board_type != "" else "dungeon" diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 0ae0f173..937eb64f 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -99,12 +99,6 @@ func set_level_bonuses(lines: Array) -> void: _bonus_section.visible = not lines.is_empty() -func set_idle() -> void: - _atk_label.text = "攻击 —" - _armor_label.text = "护甲 AC —" - set_level_bonuses([]) - - func attack_text() -> String: return _atk_label.text diff --git a/game/presentation/views/legend_overlay.gd b/game/presentation/views/legend_overlay.gd index f0eb03b3..c6cec4be 100644 --- a/game/presentation/views/legend_overlay.gd +++ b/game/presentation/views/legend_overlay.gd @@ -3,7 +3,7 @@ # 状态槽空态说明成文载入〔增益状态 D1×①〕)。 # Date: 2026-09-12 # pattern 6 模态 overlay 层级纪律:打开时由组合根隐藏上方遮挡层 + 锁棋盘输入; -# 本类自持开关(toggle),探针断言"打开后自身 visible"——遮挡物断言归场景探针。 +# 本类自持开关(open/close),探针断言"打开后自身 visible"——遮挡物断言归场景探针。 class_name LegendOverlay extends Control @@ -72,13 +72,6 @@ func is_open() -> bool: return visible -func toggle() -> void: - if visible: - close() - else: - open() - - func open() -> void: visible = true diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index 8f778a86..de80dd17 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -1,7 +1,7 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-21(GP+商店房实施批——商店房覆盖层呈现) # 商店覆盖层(模态):标题「商店」/ 金币余额 / 三商品卡全陈列(显示名·档位文案·现价·已购档数)/ -# 购买按钮(余额不足禁用置灰)/ 离开按钮。模态挂接方式仿既有覆盖层(route_overlay 现成参照—— +# 购买按钮(余额不足禁用置灰)/ 离开按钮。模态挂接方式仿既有覆盖层(legend_overlay 现成参照—— # dim + CenterContainer + box,组合根 _register_modal 收口层级)。 # 高内聚边界:纯视图 setter 面——商品快照与余额经组合根喂入(runner.shop_products() 只读投影, # 现价/已购档数由 runner 现算),购买/离开意图经信号外发——零核心写调用、零 runner 持有。 diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 47bfc9b2..8c58a569 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -124,11 +124,6 @@ func _set_hp(hp: int) -> void: _hp_tween.tween_property(self, "hp_value", target, UiKit.motion(0.15)) -func set_idle() -> void: - _set_hp(0) - set_effects([]) - - # 主角/敌方共用——状态效果唯一展示面(壳层伴随件转发;空态 = 图标行隐藏不占位) func set_effects(effects: Array) -> void: if slot_row == null: diff --git a/game/presentation/views/tune_panel.gd b/game/presentation/views/tune_panel.gd index 01b9edfe..599f5264 100644 --- a/game/presentation/views/tune_panel.gd +++ b/game/presentation/views/tune_panel.gd @@ -1,15 +1,14 @@ # VERTICAL SLICE - NOT FOR PRODUCTION # Date: 2026-09-11(2026-09-12 code-review 修复批重构) # 测试用角色数值调整板(模拟天赋树属性提升——仅测试,正式侧归元进度)。 -# 纯视图边界(规范 1.3):本类只承载滑杆 UI 与生效值预览;加成应用 / 重置落表 / 批量模拟编排 -# 全部经信号外发由组合根承接(apply_requested / reset_requested / simulate_requested / closed), +# 纯视图边界(规范 1.3):本类只承载滑杆 UI 与生效值预览;加成应用 / 批量模拟编排 +# 全部经信号外发由组合根承接(apply_requested / simulate_requested / closed); # 模拟进度与报告经 setter 注入——视图零核心写、零 smoke 脚本依赖、不拼报告文案(装配归 presenter)。 # 探针接缝(ADR-0004"合成输入走真实翻译出口"口径):press_apply_for_probe 等三键 = 按钮真实信号路径。 class_name TunePanel extends Control signal apply_requested(atk_pct: int, hp_pct: int) # 应用并关闭——加成落表归组合根单一应用路径 -signal reset_requested() # 重置为 0——视图自清滑杆,落表归组合根 signal simulate_requested(count: int) # 批量模拟编排归组合根(表基线复位 + 单路径施加) signal closed @@ -106,7 +105,6 @@ func _apply_btn_pressed() -> void: func _reset_btn_pressed() -> void: reset_sliders() - reset_requested.emit() func effective_atk() -> int: diff --git a/game/presentation/views/ui_kit.gd b/game/presentation/views/ui_kit.gd index 14b81b8a..73eb135f 100644 --- a/game/presentation/views/ui_kit.gd +++ b/game/presentation/views/ui_kit.gd @@ -21,10 +21,7 @@ const INK_TITLE := Color8(30, 26, 22) # 亮底标题墨 const INK_ROUTE := Color8(24, 20, 16) # 节点图最深墨 const INK_ON_GOLD := Color8(60, 44, 20) # 金底上的深墨 -const PANEL_HERO := Color8(232, 220, 196) # 面板底·主角侧(英雄卡/角色信息框) -const PANEL_ENEMY := Color8(226, 212, 190) # 面板底·敌方卡 const PANEL_DATA := Color8(226, 210, 182) # 面板底·数据框(AP 条/状态槽) -const PANEL_STAGE := Color8(216, 200, 172) # 面板底·战斗画面区 const PANEL_BORDER := Color8(120, 106, 88) # 面板细线框;兼熄灭点阵(DOT_OFF 别名) const GOLD := Color8(240, 168, 40) # 奖励金 @@ -50,12 +47,10 @@ const DUSK := Color8(90, 108, 140) # 暮蓝(全作唯一暮蓝档 const WARM := Color8(240, 226, 200) # 暖白(暗底正文) const WARM_SOFT := Color8(200, 188, 166) # 暖白次级(说明文) const WARM_FAINT := Color8(178, 166, 146) # 暖白弱化(帮助文) -const WARM_ROUTE_SUB := Color8(210, 196, 170) # 节点图编号暖白 const STONE := Color8(120, 110, 98) # 灰石(通道/箭头/失效) const STONE_FAINT := Color8(140, 124, 100) # 灰石弱化(占位提示文) const DARK_UPCOMING := Color8(64, 58, 52) # 未达暗字(底/文同值) -const DARK_TEXT_FAINT := Color8(160, 150, 138)# 未达节点文字 const BRICK := Color8(201, 175, 131) # 棋盘·未开掩尘地砖 #C9AF83 const FLOOR := Color8(179, 154, 120) # 棋盘·已开裸土 #B39A78 @@ -78,7 +73,7 @@ const STAGE_BG := Color8(62, 52, 42) # B 区舞台暗底(方向稿 re const BEAM_LIGHT := Color8(255, 236, 200) # B 区顶光柱暖光(用时乘 alpha 渐隐——battle_stage_view 光柱层) const STAND_STONE := Color8(150, 130, 100) # 站桩石板地台·板身(standee_body 方向稿取色) const STAND_STONE_EDGE := Color8(90, 76, 58) # 站桩石板地台·描边 -const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘受光线(与 board_cell_unit.FACADE_HIGHLIGHT_COL 异源同值——并档归清扫卡) +const STAND_STONE_RIM := Color8(196, 172, 132) # 站桩石板地台·顶缘受光线(board_cell_unit 口沿高光已并档至此——清扫批 2026-10-02) const CAMP_HEADER_BAND := Color8(122, 82, 50) # 营地柜台头带深棕(4-camp.png 方向稿取色) const CAMP_ROW_PLATE := Color8(240, 230, 206) # 营地行板暖米(同上) const CAMP_BADGE_PANEL := Color8(40, 32, 24) # 营地钥石徽暗底(同上) diff --git a/game/smoke/level_autoplay.gd b/game/smoke/level_autoplay.gd index aca66b9d..57b7699b 100644 --- a/game/smoke/level_autoplay.gd +++ b/game/smoke/level_autoplay.gd @@ -15,7 +15,6 @@ # 驱动循环按 route 策略显式 pick_next_node;route = "min"/"max"(候选节点编号最小/最大—— # 确定性路径轴,零随机流自开;默认 "min" 既有调用方签名兼容)。 # 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、 -# 2026-09-21 冒险结构实现批:run_adventure 三关连探驱动(AdventureRunner——主种子恰一次、 # 通关兑付停点确认、adventure_finished 置 result/summary;外层每关重取 runner 实例经 _drive_level # 复用单关驱动)。run_level 驱动段抽成 _drive_level——签名与返回结构零变化,行为逐位不变。 # 2026-10-01 迭代 10 奖励重构批:大奖励立即确认改三选一停点驱动(候选取首件/替换放弃新件)。 diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 46181d99..2cb95873 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -750,7 +750,7 @@ func _smoke_level() -> void: var expect: int = mini(int(floor(float(eff_max) * 0.3)), eff_max - rest.hp_before_rest) check(rest.rest_healed == expect, "恢复点回复 = floor(生效max_hp×30%%) 与缺口的较小值(%d/%d,生效 max %d)" % [rest.rest_healed, expect, eff_max]) else: - check(false, "恢复点未到达(seed 4242 hp+100 在恢复点前败北?%s)" % rest.result) + check(false, "恢复点未到达(seed 42 hp+100 在恢复点前败北?%s)" % rest.result) # ③ 裸装败北路径(atk1/hp10——Roguelike 基线应难以通关) var lose = autoplay.run_level(_db, 42, 1, -80, -80) check(lose.result == "defeat", "关卡·裸装(atk≈1/hp≈10)败北路径可达(实测:%s)" % lose.result) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 44a7a535..beeef8eb 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -542,12 +542,3 @@ func _dump_state() -> void: str(diag.battle_result), str(diag.settling)]) print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [ str(diag.minesweeper_board_installed), str(diag.facts_path), str(diag.refresh_used_round), str(_flags_placed)]) - # T17 硬锁 v3 归因面:收尾链全态 - print(" [t17dump] q=%d end_overlay=%s stop_pending=%s ref_same=%s level_screen=%s node_overlay=%s node=%s" % [ - main._row_queue.size(), str(main._end_overlay.is_open()), str(main._settlement_stop_pending), - str(main.settlement == main.runner.current_battle()), str(main.level_screen.visible), - str(main.node_overlay.visible), str(main.runner.node_index)]) - print(" [t17dump2] type=%s bonus_overlay=%s inter_overlay=%s shop=%s cands=%s" % [ - String(main.runner.current_node().get("type", "")), str(main.bonus_overlay.visible), - str(main._inter_overlay.visible), str(main.shop_overlay.visible), - str(main.runner.next_candidate_ids())]) diff --git a/game/tests/README.md b/game/tests/README.md index d639d919..d807138e 100644 --- a/game/tests/README.md +++ b/game/tests/README.md @@ -4,7 +4,7 @@ **Test Framework**: 双载体(ADR-0004)——`--headless --script` 逻辑冒烟 + headless 场景探针;gdUnit4 **deferred**(D2 OQ1 关闭形态,正式 CI 期再议) **CI**: `.github/workflows/tests.yml`(首次 push 到 GitHub 后自动生效;仓库 2026-09-12 尚未初始化 git) **Setup date**: 2026-09-12 -**参考实现**: `game/smoke/`(冒烟 96/96 + 三 UI 探针——正式工程的活样板) +**参考实现**: `game/smoke/`(冒烟 628+ 断言 + 九场景探针——正式工程的活样板) ## Directory Layout @@ -26,7 +26,7 @@ godot --headless --script tests/run_all.gd # 统一入口(CI 调用 godot --headless --script tests/unit/example_test.gd # 场景探针(双载体之二)——以探针场景为运行目标,走完整项目引导(autoload 在位) -godot --headless --path . res://tests/probes/.tscn +godot --headless --path . res://smoke/.tscn # CI 旁必须配外部 wall-clock 超时(ADR-0004:引擎无死锁探测) timeout 120s godot --headless --script tests/run_all.gd diff --git a/game/tests/smoke/critical-paths.md b/game/tests/smoke/critical-paths.md index d2ef2fa9..9fccca72 100644 --- a/game/tests/smoke/critical-paths.md +++ b/game/tests/smoke/critical-paths.md @@ -17,17 +17,16 @@ 5. 移动:已开格瞬移、**已引爆雷格可通行可读**(工程债呈现半边) 6. 停手:RAP 1:1 转存、下轮 AP 硬重置同图继续 7. 轮末结算:明细逐行呈现、来源标注可读(P2——"这一行是我哪步扫雷带来的"可答) -8. 道具使用窗口:授予段后弹出、至多选一件、满血治疗包 usable_now 禁选态正确 -9. 清图:清图奖励兑付 + 完美排雷高光(零误标时双份)+ 换新图;未刷新的停手轮不换图 -10. 轮内刷新(方案 A):Active 当场换图、AP 保留、RAP 扣减、每轮 ≤2 次、跨图事实结算完整 +8. 清图:清图奖励兑付 + 完美排雷高光(零误标时双份)+ 换新图;未刷新的停手轮不换图 +9. 轮内刷新(方案 A):Active 当场换图、AP 保留、RAP 扣减、每轮 ≤2 次、跨图事实结算完整 ## Battle & Level -11. 战斗段:主角先手攻击行(hero_attack 来源)、敌方行为环读怪(姿态↔下一拍)、死亡即不动手 -12. 胜负判定与 battle_over 清理全序;库存跨战斗延续、关卡末"剩余道具 ×N 清空"反馈 -13. 关卡推进:节点链兑付(奖励点/恢复点)、Boss 房(Dmax 行)、层间 HP 携带 +10. 战斗段:主角先手攻击行(hero_attack 来源)、敌方行为环读怪(姿态↔下一拍)、死亡即不动手 +11. 胜负判定与 battle_over 清理全序;aux 魔法道具关内跨战斗延续(三选一 BonusPickOverlay 入包、C 区槽位两步交互施用)、关卡末"剩余道具 ×N 清空"反馈(aux_cleared) +12. 关卡推进:节点链兑付(奖励点/恢复点)、Boss 房(Dmax 行)、层间 HP 携带 ## Data & Perf -14. 数值调整板:**打开可见**(覆盖层层级回归——缺陷⑤)、滑条即时生效、批量模拟可跑 -15. 无明显卡顿(60fps 目标);连续 5 分钟无内存异常增长;窗口/结算屏文案中文显示正常(CJK 字体核对) +13. 数值调整板:**打开可见**(覆盖层层级回归——缺陷⑤)、滑条即时生效、批量模拟可跑 +14. 无明显卡顿(60fps 目标);连续 5 分钟无内存异常增长;窗口/结算屏文案中文显示正常(CJK 字体核对) diff --git a/game/tests/unit/README.md b/game/tests/unit/README.md index 810dbd6e..d7d46e47 100644 --- a/game/tests/unit/README.md +++ b/game/tests/unit/README.md @@ -3,4 +3,4 @@ 每系统一个子目录(`reward_economy/`、`board_generator/`…)。协议与命名见 `tests/README.md`。 **活样板**:`example_test.gd`(本目录)——第一个真实测试请以它为模板。 -**参考实现**:`game/smoke/smoke_all.gd`(96 条断言的组织方式)。 +**参考实现**:`game/smoke/smoke_all.gd`(628+ 条断言的组织方式)。 From af755498cebe797bdec9fee1e11245d77ccd2b6f Mon Sep 17 00:00:00 2001 From: DKChan <524912269@qq.com> Date: Fri, 2 Oct 2026 09:35:12 +0800 Subject: [PATCH 8/8] =?UTF-8?q?T8=20=E6=94=B6=E5=8F=A3=E7=99=BB=E8=AE=B0:?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E6=89=B9=E9=99=8D=E7=BA=A7=E4=B8=8E?= =?UTF-8?q?=E5=90=8E=E7=BB=AD=E5=8D=A1=E5=85=A506=EF=BC=88ArmedConfirm?= =?UTF-8?q?=E9=99=8D=E7=BA=A7/main=E4=BA=8C=E6=9C=9F=E7=98=A6=E8=BA=AB/aux?= =?UTF-8?q?=E6=9B=BF=E6=8D=A2=E4=BA=8C=E6=9C=9F=E5=90=88=E5=B9=B6/?= =?UTF-8?q?=E6=8E=A2=E9=92=88=E7=99=BD=E7=9B=92=E5=87=BA=E5=8F=A3/?= =?UTF-8?q?=E6=9D=82=E9=A1=B9=E5=B0=BE=E5=B7=AE=E6=89=93=E5=8C=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- design/gdd/06-待办清单.md | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 818865a8..19226efa 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -523,3 +523,45 @@ ### 设置与本地化 + +## 代码质量修复批降级与后续卡(2026-10-02 修复批登记) + +#### 二次确认武装态公共控件(ArmedConfirm) + +**现状**:三处武装确认语义异构(AP 条 chips+4s tween 自动回落 / 选路页放弃钮 3s _process 倒计时+危险红呼吸+减动效静态变体 / 战斗总结结算框纯载荷暂存无计时无视觉)——保语义收口需可插拔视觉+双计时机制+可选脉冲,共享件本体 40-60 行而每处仅省约 6 行,且动 press_confirm_for_probe/confirm_visible 探针语义,修复批 T4 降级不做。来源:代码质量审计(ap_bar_view/level_map_screen/main 三份手抄) + +**归属**:表现层打磨批(M-4 窗口) + +**状态**:待办(2026-10-02 T4 降级登记,做了半套不如不做) + +#### main.gd 组合根二期瘦身(1237 → ~900) + +**现状**:T3 已迁排播泵/aux 两步交互/GP 影子账三件;到 ~900 还差三选一与替换子交互编排(约 115 行)、debug 模式接线、营地/商店投影 handler 等家族。来源:T3 执行报告 + +**归属**:下一表现层结构批 + +**状态**:待办(2026-10-02 登记,ADR-0001 接线留组合根前提不变) + +#### aux 替换子交互容器操作二期合并 + +**现状**:通关渠道(carry 序号寻址+hp 账本收尾)与节点渠道(库存 handle 寻址+node 收尾)已合共用前置谓词,丢弃/授予/收尾三步仍分轨——强合需适配层,收益低。来源:T5 执行报告保守注记 + +**归属**:随同调位实施批(attunement)顺路评估 + +**状态**:待办 + +#### 探针白盒私有域访问改 *_probe() 只读出口 + +**现状**:ui_input_probe 等直入 BoardView._cells/_diggable/_layout 等私有域(重构即碎),fx_probe 已有只读出口先例。来源:代码质量审计(登记债务,修复批边界外) + +**归属**:探针基建批 + +**状态**:待办 + +#### 杂项尾差(一批打包) + +adventure_table.tres 补 bonus_candidate_count 字段落档(T5 注:脚本默认 3=现行值,落档属结构改动);grant_aux_item 返回值接留痕同型件 level_runner :438 区(T5 注);README.md:402 提及已删 _autoplay_battle_perfect 旧名(T6 注);critical-paths.md:6「切片 96/96」过期字样(T7 注);hero_info_panel_view:29 散文提及已删 PANEL_HERO 色名(T7 注)。来源:T4-T7 执行报告遗留观察 + +**归属**:下一清扫批顺路 + +**状态**:待办