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] =?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