Files
gplan-2/game/presentation/main.gd
T

1296 lines
73 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# VERTICAL SLICE - NOT FOR PRODUCTION
# Validation Question: 线性第一关(战×2→道具→战×2→恢复→Boss)能否承载"数值提升→通关期望改善"的元进度验证
# 实现 GDD:design/ux/battle.md(战斗屏 A/B/C/D 四区 + 全屏 States + 遥测;E 区随可选类删除下线 2026-09-15)。
# Date: 2026-09-12(battle.md 战斗屏改造批;基础 = 2026-09-11 HUD框架-lite 组合根;code-review 修复批重构)
# 2026-09-13(封装审查修复批):核心状态读取收口至 战斗结算/runner 成文只读面(presentation_snapshot /
# current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离
# debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。
# 组合根(composition root):唯一的模式流转与 core↔view 接线处。
# 职责边界:① 构建四区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线)
# ② 模式管理(正式冒险 AdventureRunner——唯一试玩入口;调试两层编排归 debug_session.gd;单场原语仅探针使用)
# ③ 核心写调用只在此处发生(minesweeper.settle_batch / 刷新编排入口 / runner 推进;
# E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable
# 以 encounter 参数入战斗,db.hero 运行期只读)
# ④ 全屏 States 状态机(挖掘/结算/终局)与一次性提示去重。
# 2026-09-15 格面资源批:道具窗口与库存面板随可选类道具删除下线——E 区移除,窗口接线/选择序列删除。
# GP+商店房实施批(2026-09-21):商店覆盖层挂接与信号接线(装配器零信号连接纪律——句柄返回、
# 本类连接)+ runner.wallet_changed 转发试玩日志(wallet 行族)+ A 区顶栏金币余额。
# 渲染与文案归 views/,核心状态→文案装配归 hud_presenter.gd 与 battle_narrator.gd,节点构建归
# ui_assembler.gd,模态层级归 modal_stack.gd,批量模拟编排归 tune_sim_controller.gd,调试模式编排归
# debug_session.gd——本类不拼呈现字符串。
# T3 拆件批(2026-10-02,规范 1.1 单一职责):结算行排播泵迁 row_pacer.gd(本类只入队/排空/读队列,
# 探针/测试委托面 _row_queue/pace_delay_for/_flush_row_queue 原位保留);秘宝两步交互迁
# treasure_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/treasure_interact_controller 零连接,只收注入)。
extends Control
const UiAssembler := preload("res://presentation/ui_assembler.gd")
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 TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互(T3 拆件批)
const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批)
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互(T4/D4 拆件批)
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影(T4/D4 拆件批)
const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款)
const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律)
var db
var settlement = null # 当前战斗编排器(关卡与调试模式统一读它)
var runner = null # 关卡推进器(冒险模式每关重取 adventure.runner();单关 UI 直入口已退役——LevelRunner 仅存 headless 测试面)
var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接)
var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构)
var meta = null # 元进度账本服务(元进度实现批——余额/定价/购买/工坊解锁/Boss两档入账/注入计算;零信号纯被动)
var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派)
var _debug_entry := false # 调试会话门控(迭代 18 卡⑦——DS_DEBUG=1:启动即开浮窗+两屏角标点亮;_ready 赋值)
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _level_active := false
var _seed_edit: LineEdit
var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮)
var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化)
var _settlement_stop_timer := ArmedTimer.new() # 卡⑥ 结算框挂起计时(T3/D3 P4 乙——裸布尔暂存机制收口;窗口 0=挂起到显式回落)
var _settlement_stop_pending: bool: # 挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm)
get:
return _settlement_stop_timer.is_armed()
set(v):
if v:
_settlement_stop_timer.arm()
else:
_settlement_stop_timer.disarm()
var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷)
var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷)
var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用)
var _start_overlay: Control
var _end_overlay: BattleEndOverlay
var _inter_overlay: Control
var _inter_label: Label
var level_screen: LevelMapScreen # 关卡画面(独立屏——2026-09-28 形态升格批:激活期战斗屏宿主整体隐藏;零自动推进停点语义不变)
var _battle_root: Control # 战斗屏宿主(B/C/D 三区容器——独立屏激活期 visible=false 级互斥)
var _level_screen_active := false # 独立屏激活状态(覆盖兑付覆盖层在屏域内弹出期间——战斗屏保持隐藏)
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
var _debug = null
# 视图(views/)——battle.md 三区(A 区关卡进度条顶栏随 停点前移批 2026-09-23 退役——节点链进度
# 由关卡画面本体选路覆盖层承载;E 区道具面板随可选类删除下线)
var stage: BattleStageView # B 区(含 log_view 明细带)
var log_view: BattleLogView
var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区——2026-09-26 信息归属拍板精简)
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
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 _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图)
var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入)
var _camp_pres = null # 营地/商店投影(camp_shop_presenter.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 node_overlay: NodeOverlay
var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现)
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效)
var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面)
var pause_overlay: PauseOverlay # 局内暂停层·只读(迭代 17 卡③——钥石余额+营地三柜台进度查看;登记设置层同档)
var debug_overlay: DebugOverlay # 调试浮窗(迭代 14.5 调试整合批——种子/数值调整/批量模拟/两层调试入口收一)
var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体)
var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd)
var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent)
var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零)
var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名)
var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批)
var _locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放,玩家操作零丢失)
# 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理
#(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面,
# 排播泵本体已迁 row_pacer.gd——T3 结构段)。
var _deferred_modal: Control = null # 演播后开窗挂起(T17——胜利/Boss 总结/终局面等排播放完再开)
# 演播后排窗(T17 用户令「伤害过程演完才切结算窗口」):排播队列在播=挂起,演播完边沿
# (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出
# 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。
func _open_modal_after_playback(m: Control) -> void:
if _row_pacer.pending_rows().is_empty():
_open_modal(m)
return
_deferred_modal = m
m.visible = false # 挂起含视觉(open_* 已置 visible=true——不压掉则窗在排播期照会上屏,延迟形同虚设)
# 演播完边沿派发(row_pacer.pump_ended 注入口——原直连锁期点击重放,现统一派发):
# 先开挂起 modal(胜利窗接管呈现),后锁期点击重放——modal 开启后 _input_locked 吞掉
# 重放点击(战斗已收场,重放无意义)。
func _on_pump_ended() -> void:
if _deferred_modal != null:
var m: Control = _deferred_modal
_deferred_modal = null
_open_modal(m)
return
_replay_locked_click()
func _replay_locked_click() -> void:
if _locked_click.is_empty() or not _row_queue.is_empty():
return
var click: Dictionary = _locked_click
_locked_click = {}
_on_board_action(click["cell"], click["kind"])
func _ready() -> void:
db = get_node("/root/ConfigDB")
if not db.is_ready:
var ok: bool = db.load_tables("res://data/tables", false)
if not ok:
_fatal_screen("数值表加载失败——见输出错误清单(fail-fast)")
return
# 存档读盘(存档序列化批):拒读/损坏均已在 SaveManager 内部兜底,失败不阻断启动
# (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。
save_manager = SaveManagerScript.new()
save_manager.load_from_disk()
# 系统级设置(迭代 8 卡④——独立存储不随存档):启动即恢复音量/全屏/减动效引擎态
settings_store = SettingsStore.new()
settings_store.load_settings()
settings_store.apply()
# 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点
meta = MetaProgressionScript.new(db, save_manager)
_debug = DebugSessionScript.new(db, get_node("/root/RngService"),
Callable(self, "_debug_hero_override"), Callable(self, "_seed_value"))
_debug.battle_created.connect(_on_debug_battle_created)
_debug.battle_ready.connect(_on_debug_battle_ready)
_build_ui()
_refresh_resume_button() # 迭代 8 卡②——启动读盘后按断点在档态点亮「继续上局」
_start_overlay.visible = true
_debug_entry = OS.get_environment("DS_DEBUG") == "1" # 迭代 18 卡⑦ P3 甲——DS_DEBUG=1 调试会话门控
if _debug_entry:
_show_debug_overlay() # 启动即开(手动可关、两屏角标可再开——P3 甲)
# ==================== 视图接线(battle.md 四区布局 1280×800;节点构建归 UiAssembler) ====================
func _build_ui() -> void:
var root := VBoxContainer.new()
add_child(root)
_battle_root = root # 独立屏互斥面(2026-09-28 形态升格批)
var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db)
stage = battle_ui.stage
log_view = battle_ui.log_view
info_panel = battle_ui.info_panel
board_zone = battle_ui.board_zone
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
_row_pacer.pump_ended = _on_pump_ended # T17 v3.1+演播后排窗——边沿统一派发(挂起 modal 优先,其次锁期点击重放) # T17 v3.1——演播完边沿重放锁期点击(合并批接线)
add_child(_row_pacer)
# 秘宝两步交互(T3 拆件批——迁 treasure_interact_controller.gd;settlement/runner 随关卡更替,
# 经现值回调注入——仿 identity_resolver 只读回调先例)
_treasure_ui = TreasureInteractController.new(db, meta, info_panel, board_zone.toast, board_view,
func() -> Variant: return settlement, func() -> Variant: return runner)
board_zone.action_requested.connect(_on_board_action)
board_zone.pause_badge.pressed.connect(_on_pause_badge_pressed) # 暂停层入口(迭代 17 卡③——战斗屏 HUD 角标)
board_zone.debug_badge.pressed.connect(_on_debug_badge_pressed) # 调试浮窗入口(迭代 18 卡⑦ P2 甲——战斗屏 HUD 角标)
info_panel.treasure_slot_clicked.connect(_treasure_ui.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)
board_zone.hover_forbidden.connect(_on_hover_forbidden)
board_zone.chord_attempted.connect(_on_chord_attempted)
# 奖励徽章数据源:地图战斗资源 身份解析只读回调(已开格身份 icon——挖掘期 Active 相位内有效)
board_view.set_identity_resolver(_identity_lookup)
# 数字遮蔽数据源:地形变异 hide_number 解析只读回调(信息型呈现——board_view 渲染面读取)
board_view.set_variant_mask_resolver(_variant_mask_lookup)
board_view.set_variant_mark_resolver(_variant_mark_lookup) # 迭代 6 T6——flat_damage 未开格角标
logger = PlaytestLogger.new()
logger.ap_per_round = int(db.action_economy.ap_per_round)
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
# < 商店 < 营地 < 设置 < 暂停(只读) < 确认弹窗 < 调试浮窗)
var overlays: Dictionary = UiAssembler.build_overlays(self, db)
legend_overlay = overlays.legend_overlay
node_overlay = overlays.node_overlay
_end_overlay = overlays.end_overlay
shop_overlay = overlays.shop_overlay
camp_overlay = overlays.camp_overlay
level_screen = LevelMapScreen.new()
add_child(level_screen)
# 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——关卡画面须夹在节点层与商店层之间
#(兑付覆盖层在本屏域内弹出:模态栈打开低层时自动隐藏高层本屏、关闭自动恢复——零机制改动复用)
move_child(level_screen, node_overlay.get_index() + 1)
legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay))
node_overlay.continue_pressed.connect(_on_node_continue)
level_screen.route_chosen.connect(_on_route_chosen)
level_screen.pause_badge.pressed.connect(_on_pause_badge_pressed) # 暂停层入口(迭代 17 卡③——地图屏顶栏角标)
level_screen.debug_badge.pressed.connect(_on_debug_badge_pressed) # 调试浮窗入口(迭代 18 卡⑦ P2 甲——地图屏顶栏角标)
level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
settings_overlay = SettingsOverlay.new(settings_store) # 迭代 8 卡④——最小设置面(音量/全屏/减动效)
add_child(settings_overlay)
settings_overlay.closed.connect(_on_settings_closed)
# 局内暂停层·只读(迭代 17 卡③——P2 甲:登记设置层同档带 dim 遮罩,确认弹窗之前;
# 数据=组合根开层实时读 MetaProgression 访问器喂入(R4),本层零写通路)
pause_overlay = PauseOverlay.new()
add_child(pause_overlay)
pause_overlay.closed.connect(_on_pause_closed)
# 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进)
curio_overlay = CurioPickOverlay.new()
add_child(curio_overlay)
# 提升三选一与替换子交互(T4/D4 拆件批——迁 curio_interact_controller.gd;本类只接线与回调注入,
# ADR-0001——模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调)
_bonus = CurioInteractController.new(db, func() -> Variant: return meta, curio_overlay, _end_overlay, board_view,
board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return adventure,
func() -> Variant: return runner, func() -> Variant: return _adventure_mode,
_open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations,
_fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount}))
curio_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed)
curio_overlay.replace_chosen.connect(_bonus.on_replace_chosen)
curio_overlay.continue_pressed.connect(_bonus.on_continue)
curio_overlay.refresh_requested.connect(_bonus.on_refresh)
move_child(curio_overlay, _end_overlay.get_index())
var start: Dictionary = UiAssembler.build_start_overlay(self, db)
_start_overlay = start.overlay
_resume_btn = start.resumebtn
start.lvbtn.pressed.connect(_start_adventure_from_menu)
start.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口
# 调试角标入口退役(迭代 18 卡⑦ P2 甲——迁宿主:战斗屏 HUD+关卡地图屏各一枚,
# 接线见两屏装配段;开始层不再有调试入口,DS_DEBUG=1 会话=启动即开浮窗)
# 营地/商店投影(T4/D4 拆件批——迁 camp_shop_presenter.gd;本类只接线、探针缝保缝转发;
# 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入)
_camp_pres = CampShopPresenter.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay,
board_zone.toast, _start_overlay, logger, func() -> Variant: return runner,
_open_modal, _close_modal, _refresh_presentations, _refresh_resume_button)
shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy)
shop_overlay.refresh_requested.connect(_camp_pres.on_shop_refresh) # MC-1
shop_overlay.leave_pressed.connect(_camp_pres.on_shop_leave)
camp_overlay.buy_requested.connect(_camp_pres.on_camp_buy)
camp_overlay.attune_toggled.connect(_camp_pres.on_camp_attune_toggle)
camp_overlay.tree_buy_requested.connect(_camp_pres.on_camp_tree_buy)
camp_overlay.tree_fork_toggled.connect(_camp_pres.on_camp_fork_toggle)
camp_overlay.closed.connect(_camp_pres.on_camp_closed)
start.campbtn.pressed.connect(_camp_pres.open_camp)
start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口
start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单
var inter: Dictionary = UiAssembler.build_inter_overlay(self)
_inter_overlay = inter.overlay
_inter_label = inter.label
inter.next_btn.pressed.connect(_on_inter_next_pressed)
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)
# 调试浮窗(迭代 14.5 调试整合批——主菜单三散件收一:随机种子/数值调整/批量模拟/两层调试入口;
# 迭代 18 卡⑦——升级为全局可开浮窗:DS_DEBUG=1 启动即开〔_ready〕+两屏角标再开〔P2 甲〕+当局数据只读区块〔T3〕)。
# 树序在开始层与确认弹窗之后 = 浮于菜单之上的 Dialog(不隐藏开始层——与确认弹窗同款形态);
# 开窗唯一入口 = _show_debug_overlay(C6 单口——角标/探针/启动路径全收口)
debug_overlay = DebugOverlay.new(db, int(db.hero.atk), int(db.hero.max_hp))
add_child(debug_overlay)
_seed_edit = debug_overlay.seed_edit # 种子读取面随浮窗迁入(_seed_value 唯一消费点不变)
debug_overlay.closed.connect(_on_modal_closed.bind(debug_overlay))
debug_overlay.apply_requested.connect(_on_tune_apply)
debug_overlay.simulate_requested.connect(_on_tune_simulate)
debug_overlay.debug_levels_requested.connect(_start_debug_levels)
# 模态登记(缺陷⑤纪律——登记序 = 树序:含 move_child 调位后的关卡画面/三选一终位;
# 暂停层设置层同档、确认弹窗之前——P2 甲)
_modals = ModalStack.new()
for m: Control in [legend_overlay, node_overlay, level_screen, curio_overlay, _end_overlay, shop_overlay, camp_overlay, settings_overlay, pause_overlay, confirm_dialog, debug_overlay]:
_register_modal(m)
_tune_sim = TuneSimController.new(db, debug_overlay, int(db.hero.max_hp))
func _fatal_screen(msg: String) -> void:
var l := UiKit.label(msg,UiKit.FS_SUB, true, UiKit.RED_ALERT)
l.set_anchors_preset(Control.PRESET_CENTER)
add_child(l)
# ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ====================
func _register_modal(m: Control) -> void:
# 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后
# 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors
# ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律)
m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_modals.register(m)
func _open_modal(m: Control) -> void:
# 挂起兜底(T17 演播后排窗):其他路径(如回看)在播期排空队列会使演播完边沿不再触发——
# 此处先结挂起 modal 防永不兑现(m==挂起对象时跳过=本调用自身)。
if _deferred_modal != null and m != _deferred_modal:
var dm: Control = _deferred_modal
_deferred_modal = null
_open_modal(dm)
# 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈
_flush_row_queue() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行)
_modals.open(m)
_sync_pause_entry() # 迭代 17 卡③——暂停层入口角标可见性/置灰随开态同步
_sync_debug_overlay() # 迭代 18 卡⑦——浮窗当局数据/按钮态随开态同步(T3 刷新=模态开合信号驱动)
func _close_modal(m: Control) -> void:
_modals.close(m)
_sync_pause_entry() # 迭代 17 卡③——关闭恢复角标可点态(无残留——R3 同源口径)
_sync_debug_overlay() # 迭代 18 卡⑦——他层关闭复活浮窗时同步刷新(P1 甲关后自动恢复语义配套)
func _on_modal_closed(m: Control) -> void:
_close_modal(m)
func _input_locked() -> bool:
# 模态/非战斗期棋盘输入锁(T3 收口批——开态查询归 modal_stack.has_open(),登记册单一来源,
# 不再手工罗列各层;例外三件:开始层/过场层(非模态全屏态,不在登记册)与战斗缺位(棋盘输入无载体))
# 迭代 18 卡⑦ T2 防护(审计🔴):判据改排除式——调试浮窗对局中可开(P2 甲),其开态不计入锁
#(否则 DS_DEBUG 默认开×他层关闭复活浮窗×has_open 组合=棋盘永久锁死——S1+ 停机点防线)
return _modals.has_open_excluding(debug_overlay) or _start_overlay.visible or _inter_overlay.visible or settlement == null
# ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)====================
func _start_adventure_from_menu() -> void:
# 互斥确认弹窗化(二轮修改批 A——用户目检裁决「弹窗提示」):有断点时开新冒险弹实底确认框,
# 「开新冒险」提交(弃旧断点+写盘)/「取消」回菜单
if save_manager != null and save_manager.has_breakpoint():
var bp: Dictionary = save_manager.breakpoint_snapshot()
confirm_dialog.open("已有未完冒险(第 %d 关)——开新冒险将覆盖旧断点" % maxi(1, int(bp.get("level_index", 1))),
"开新冒险", "取消")
return
_start_overlay.visible = false
_start_adventure()
func _on_confirm_new_adventure() -> void:
confirm_dialog.close()
if save_manager != null:
save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效)
save_manager.write_to_disk()
_start_overlay.visible = false
_start_adventure()
func _on_confirm_cancelled() -> void:
confirm_dialog.close() # 回开始菜单(无状态变更)
# 退出游戏(二轮修改批 D1——一级菜单入口;设置即时落盘后退出)
func _quit_game() -> void:
if settings_store != null:
settings_store.save_settings()
get_tree().quit()
# 继续上局按钮可见性 = 断点在档态(迭代 8 卡②——启动读盘后与每次回开始菜单刷新)
func _refresh_resume_button() -> void:
if _resume_btn == null or save_manager == null:
return
var has: bool = save_manager.has_breakpoint()
_resume_btn.visible = has
if has:
var bp: Dictionary = save_manager.breakpoint_snapshot()
_resume_btn.text = "▶ 继续上局(第 %d 关 · 自最近停点恢复)" % maxi(1, int(bp.get("level_index", 1)))
# 继续上局(迭代 8 卡②——卡① resume_adventure 的消费入口):断点重建至最近停点
func _resume_from_menu() -> void:
if save_manager == null or not save_manager.has_breakpoint():
return
var bp: Dictionary = save_manager.breakpoint_snapshot()
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(迭代 18 卡⑦ T2 防护——续玩同口径)
_start_overlay.visible = false
_wire_adventure()
_level_active = true
_sync_pause_entry() # 迭代 17 卡③——续玩恢复点亮暂停入口角标
level_screen.set_abandon_available(true)
_reset_run_scope()
log_view.clear()
logger.start_session(int(bp.get("master_seed", 0)))
adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
if not adventure.resume_adventure(bp, {"hero_override": meta.hero_override() if meta != null else {}, "treasure_unlocked": _treasure_ui.unlocked_set()}):
_fatal_screen("续玩恢复失败——见输出日志")
return
# 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根,ADR-0001;断点注入同口)
func _wire_adventure() -> void:
adventure = AdventureRunner.new(db, get_node("/root/RngService"))
_adventure_mode = true
adventure.step_row.connect(_on_step_row)
adventure.round_started.connect(_on_round_started)
adventure.board_changed.connect(_on_board_changed)
adventure.node_entered.connect(_on_runner_node)
adventure.choices_presented.connect(_on_choices_presented)
adventure.wallet_changed.connect(_on_wallet_changed)
# 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线
adventure.curio_choice_presented.connect(_bonus.on_presented)
adventure.curio_resolved.connect(_bonus.on_resolved)
adventure.treasure_replace_presented.connect(_bonus.on_treasure_replace_presented)
adventure.adventure_finished.connect(_on_adventure_finished)
adventure.level_started.connect(_on_adventure_level_started)
adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径)
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
# 信号连接只在组合根——ADR-0001)
func _start_adventure() -> void:
_close_modal(debug_overlay) # 开冒险路径显式关浮窗(迭代 18 卡⑦ T2 防护——DS_DEBUG 默认开态下开局不带窗;对局中经角标再开)
_wire_adventure()
_level_active = true
_sync_pause_entry() # 迭代 17 卡③——进对局点亮暂停入口角标
level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板)
_reset_run_scope()
log_view.clear()
var seed_val := _seed_value()
logger.start_session(seed_val)
adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct())
# 永久强化注入(元进度实现批):冒险开始参数面——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, "treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"}):
_fatal_screen("冒险启动失败——见输出日志")
return
# 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达;
# 停点前移批 2026-09-23:本回调返回后才有 choices_presented 到达——入口选路层随即弹出)
func _on_adventure_level_started(level_index: int, display_name: String) -> void:
runner = adventure.runner()
log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed),
int(db.hero.atk), int(db.hero.max_hp),
debug_overlay.atk_pct() > 0 or debug_overlay.hp_pct() > 0))
func _reset_run_scope() -> void:
# 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号)
_hints_shown = {}
_refresh_used_round = 0
_boss_active = false
func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
# 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面
if _adventure_mode and adventure != null:
runner = adventure.runner()
var t := String(node.get("type", ""))
if t == "battle" or t == "boss":
settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线)
logger.attach_battle(settlement)
_boss_active = t == "boss"
_show_board_fx_for_node(t)
log_view.append_bb(Narrator.node_banner(runner, node, t))
elif t == "reward":
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。
# T4/D4 拆件:呈现编排归 curio_interact_controller.present_node_curio(_curio_context 随迁)。
_bonus.present_node_curio()
log_view.append_bb(Narrator.reward_line())
elif t == "rest":
var healed: int = int(info.get("healed", 0))
_open_modal(node_overlay)
node_overlay.show_rest(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0)))
log_view.append_bb(Narrator.rest_line(healed, int(info.get("hp", 0)), int(info.get("max_hp", 0))))
elif t == "shop":
# 商店房兑付(GP+商店房实施批):到达即开店——商品/货架快照与余额经 runner 只读投影喂入;
# 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。
# pool 面 = 共享池货架三选一同构卡(迭代 11 第②批 T4);legacy 面 = 固定三件(off 对照承载)
_open_modal(shop_overlay)
if runner.is_shop_pool():
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行
shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) # MC-1
else:
shop_overlay.open_shop(runner.shop_products(), int(runner.gp))
shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载)
log_view.append_bb(Narrator.shop_line(int(runner.gp)))
logger.log_node(node, info, runner.node_index)
_refresh_presentations()
# ---- 冒险模式终局面(三选一选择面 / 冒险结算屏——迭代 10 奖励重构批)----
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案
func _on_adventure_finished(summary: Dictionary) -> void:
_level_active = false
var result: String = String(summary.get("result", ""))
settlement = null
_exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口
_sync_pause_entry() # 迭代 17 卡③——离局熄灭暂停入口角标(防菜单误现)
_sync_settlement_visuals() # → 全关
HudPresenter.battle_end_feedback(stage, ap_bar, false) # 灰化/idle 回正/状态全清——HUD 单通道(T3 收口批)
# 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
var detail: Dictionary[String, Variant] = {}
if meta != null:
detail.assign(meta.settle_adventure(summary))
var text := Narrator.adventure_summary_text(summary)
if not detail.is_empty():
text += "\n" + Narrator.keystone_settle_text(detail, _bonus.item_name_lookup())
if result == "completed":
_end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(),
"本局 GP 入账 · 合计 %d" % _adventure_gp_final) # 卡⑩ GP 行;钥石明细已在 text(keystone_settle_text)——M9 三行账
else:
_end_overlay.open_defeat(text, "返回", board_view.get_global_rect())
logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷)
if runner != null:
runner.restore_hero_base()
_open_modal_after_playback(_end_overlay)
_refresh_presentations()
func _on_battle_end_continue() -> void:
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
_close_modal(_end_overlay)
_end_overlay.close()
# 迭代 10——三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
if _bonus.resume_if_pending(): # T4/D4 拆件——三选一停点在途恢复入口(curio_interact_controller)
return
# 卡⑥ 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
if _settlement_stop_pending:
_settlement_stop_pending = false
_enter_level_screen()
level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, runner.feats_held())))
level_screen.open_choices(_pending_candidates, runner)
_pending_candidates = []
_refresh_presentations()
return
_debug.reset()
if runner != null:
runner.restore_hero_base()
runner = null
settlement = null
adventure = null
_adventure_mode = false
debug_overlay.reset_sliders()
_start_overlay.visible = true
_refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新(收束后已清=隐藏)
_refresh_presentations()
func _start_debug_levels() -> void:
# 守卫(迭代 18 卡⑦ T2——浮窗对局中可开后,调试对局与活跃冒险/结算并存不开放:并发会清写
# settlement/编排态=状态机破坏;随开随收逻辑改向——对局中开窗只读浮窗,不再视为开局前置)
if _adventure_mode or settlement != null:
return
debug_overlay.reset_sliders()
_close_modal(debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay)
_start_overlay.visible = false
log_view.clear()
log_view.append_bb(Narrator.debug_open_line(_seed_value(), 1, true))
_reset_run_scope()
if not _debug.start_debug_levels():
_fatal_screen("战斗启动失败——见输出日志")
func _on_debug_battle_created(settlement_ref, _level: int, _carry: int) -> void:
# begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。settlement 必须先于 begin_battle
# 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务)
settlement = settlement_ref
_wire_battle(settlement_ref)
func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void:
settlement = settlement_ref
board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
_check_board_hints()
_refresh_presentations()
func _on_inter_next_pressed() -> void:
_inter_overlay.visible = false
if _adventure_mode:
# 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点)
if adventure != null and not adventure.continue_to_next_level():
_fatal_screen("关卡推进失败——见输出日志")
return
if not _debug.next_layer():
_fatal_screen("战斗启动失败——见输出日志")
# E4:加成生效值供给(debug_session 注入 Callable——每次从浮窗百分比现算,经 encounter 传参零表写;
# 原表写路径 _apply_hero_override/_scaled_override 已废除,双写叠加 6→18→54 整类隐患随之移除;
# 合成式单点在 UiKit.scaled——与调试浮窗生效值预览共用,本类不再持有同式私有实现)
func _debug_hero_override() -> Dictionary:
return {"atk": UiKit.scaled(int(db.hero.atk), debug_overlay.atk_pct()),
"max_hp": UiKit.scaled(int(db.hero.max_hp), debug_overlay.hp_pct())}
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音;
# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批)
func _identity_lookup(c: Vector2i):
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——格带该效果变异即遮蔽;读取方按格子变异数据行动。
# 变异表直读本根 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession
# 构造传参),免 settlement 内部链式穿透,T3 收口批)
func _variant_mask_lookup(c: Vector2i) -> bool:
if settlement == null:
return false
var b = settlement.minesweeper.current_board()
if b == null:
return false
for vid in b.variant_ids(c):
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 ""
var b = settlement.minesweeper.current_board()
if b == null:
return ""
for vid in b.variant_ids(c):
var vrow: Dictionary = db.variation.row_by_id(String(vid))
if String(vrow.get("effect", "")) == "flat_damage":
return "flat_damage"
return ""
func _show_board_fx_for_node(node_type: String = "") -> void:
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
if node_type == "boss":
board_zone.fx.play_boss_shadow()
func _on_battle_finished(payload: Dictionary) -> void:
# 仅调试模式接线(冒险的关卡结束归 AdventureRunner._on_level_finished——大奖励/冒险终局分派)
var result: String = String(payload.get("result", ""))
_sync_settlement_visuals() # → 全关(battle_over 反馈面)
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":
_inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
int(decision.layer), int(decision.carry_hp), int(db.hero.max_hp)]
_inter_overlay.visible = true
_refresh_presentations()
return
"level_end":
_end_overlay.open_victory(Narrator.battle_end_text("victory", true),
"返回", board_view.get_global_rect(), _gp_summary_text(payload),
"钥石 · 累计 %d" % runner.keystone, _bonus_summary_text(),
int(payload.get("gp_dropped", 0)), int(runner.gp))
"standalone_end":
if result == "victory":
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
"回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload),
"钥石 · 累计 %d" % runner.keystone, _bonus_summary_text(),
int(payload.get("gp_dropped", 0)), int(runner.gp))
else:
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
"返回", board_view.get_global_rect())
_open_modal_after_playback(_end_overlay)
_refresh_presentations()
# 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;账本语义零改动,纯呈现读数):
# 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账
# 连接先于组合根 handler——此读数即战后入账完毕的总量)。
# runner 缺位守卫(2026-10-06 用户实机 main.gd:717 崩值定位——续玩停点恢复分支:adventure_runner
# resume_adventure 的 bonus_pending 分支在 level_started 之前同步 curio_choice_presented.emit
# 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照
# 此刻必在档,语义=续玩恢复面的合计即存档合计;与 _bonus_summary_text 同款守卫式)
func _gp_summary_text(payload: Dictionary) -> String:
if runner == null:
var bp: Dictionary = save_manager.breakpoint_snapshot() if save_manager != null else {}
return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(bp.get("gp", 0))]
return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)]
# M9 结算三行账·本关加成行装配(呈现只读投影——runner.bonus 统一加算区同源;零加成=行隐藏)
func _bonus_summary_text() -> String:
if runner == null:
return ""
var b: Dictionary = runner.bonus
var parts: Array[String] = []
if int(b.get("atk", 0)) != 0:
parts.append("攻 %+d" % int(b.get("atk", 0)))
if int(b.get("max_hp", 0)) != 0:
parts.append("上限 %+d" % int(b.get("max_hp", 0)))
if int(b.get("armor_class", 0)) != 0:
parts.append("AC %+d" % int(b.get("armor_class", 0)))
return "本关加成 " + " · ".join(parts)
# ==================== core↔view 接线(战斗信号统一走这里)====================
func _wire_battle(settlement_ref) -> void:
settlement_ref.step_row.connect(_on_step_row)
settlement_ref.battle_finished.connect(_on_battle_finished)
settlement_ref.round_started.connect(_on_round_started)
settlement_ref.board_changed.connect(_on_board_changed)
# ==================== 结算行排播(泵本体归 row_pacer.gd——本节仅入口与探针/测试委托面)====================
func _on_step_row(row: Dictionary) -> void:
# 清图奖励可感知反馈(2026-10-04 用户反馈「挖完全图无任何奖励感」——clear_blast 兑付对敌伤害
# 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带)
if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective":
board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
# 卡⑦ 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放
if UiKit.motion_scale < 1.0:
_row_pacer.consume_now(row)
return
_row_pacer.enqueue(row)
# 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面)
func pace_delay_for(prev_seg: String, next_seg: String) -> float:
return _row_pacer.pace_delay_for(prev_seg, next_seg)
# 强制排空(模态开启前调用——明细回看/停点覆盖层/终局面所见明细带恒完整;pacing_queue_test 直读)
func _flush_row_queue() -> void:
_row_pacer.flush()
func _on_round_started(ri: int) -> void:
# 卡⑦:轮首分隔行随排播队列走(落在前轮各行之后——带序正确);轮横幅即轮切换时刻发射
#(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用
# _consume_row 轮首分隔行分支,与原直写逐字等价)。
if UiKit.motion_scale < 1.0:
_row_pacer.consume_now({"__round_line": ri})
else:
_row_pacer.enqueue({"__round_line": ri})
settlement_fx.play_round_banner(ri)
# 开战横幅——板级地形变异预告(全局作用域变异首发批 2026-10-04):首轮快照 board_variants
# 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。
if ri == 1 and settlement != null:
var bvs: Array = settlement.presentation_snapshot().get("board_variants", [])
if not bvs.is_empty():
var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs)
if not vlines.is_empty():
settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
_refresh_used_round = 0
_refresh_presentations()
func _on_board_changed() -> void:
# 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步
if settlement == null and _level_active and runner != null:
settlement = runner.current_battle()
if settlement == null:
return
board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper)
_check_board_hints()
_refresh_presentations()
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
func _check_board_hints() -> void:
var board = settlement.minesweeper.current_board()
if board == null:
return
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
if board.illegal_product:
board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
elif board.degraded_product:
board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
func _on_hover_forbidden(_cell: Vector2i) -> void:
if not _hints_shown.has("no_adjacent"):
_show_hint("需先走近(点击已开格移动,免费)", "no_adjacent")
func _on_chord_attempted(_cell: Vector2i) -> void:
if not _hints_shown.has("chord"):
_show_hint("本作无双击快开——插旗后轮末自动判定标中;点数字格可移动小人", "chord")
func _show_hint(text: String, hint_id: String) -> void:
_hints_shown[hint_id] = true
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
board_zone.toast.show_toast(text, "info")
# ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ----
func _on_node_continue() -> void:
_close_modal(node_overlay)
if runner != null:
# 泛化语义(接口漂移清偿):确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口
runner.continue_after_node()
func _on_choices_presented(candidates: Array) -> void:
# 零自动推进的停点呈现:整幅节点图 + 图上点击选路(候选按钮退役 2026-09-28——战斗胜利/兑付确认统一到达此处)
if runner == null:
return
log_view.append_bb(Narrator.route_line(candidates, runner))
# 卡⑥ 战斗胜利总结结算(battle.md OQ14 拍板面接线——欠账清偿):战斗胜利停点先弹 BattleEndOverlay
# 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点
# 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点
# 的当前节点非 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():
_settlement_stop_pending = true
_pending_candidates = candidates.duplicate()
_end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点",
board_view.get_global_rect(),
"战利品 GP +%d · 合计 %d" % [_last_gain_amount, int(runner.gp)])
_open_modal_after_playback(_end_overlay)
_refresh_presentations()
return
_enter_level_screen()
level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, runner.feats_held())))
level_screen.open_choices(candidates, runner)
_refresh_presentations()
func _on_route_chosen(node_id: int) -> void:
# 形态升格分支(2026-09-28):视图已按节点类型定去留——战斗/Boss 出屏(visible=false,此处收栈
# 记录一致性+回战斗屏);奖励/恢复/商店本屏保持激活(兑付覆盖层由节点事件域内弹出,模态栈
# 打开低层时自动隐藏本屏、关闭自动恢复)
if not level_screen.visible:
_close_modal(level_screen)
_exit_level_screen()
if runner != null and not runner.pick_next_node(node_id):
push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id)
# ---- 独立屏屏级互斥(形态升格批 2026-09-28——单一出口:进屏隐战斗屏 / 出屏显战斗屏)----
# 兑付覆盖层(节点/商店)在本屏激活域内弹出期间保持隐藏态不变——模态栈打开低层时隐藏本屏、
# 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。
func _enter_level_screen() -> void:
_level_screen_active = true
_battle_root.visible = false
_open_modal(level_screen)
_sync_pause_entry() # 迭代 17 卡③——屏级互斥切换同步(_open_modal 单口不识屏级状态)
func _exit_level_screen() -> void:
_level_screen_active = false
_battle_root.visible = true
if level_screen.visible:
_close_modal(level_screen)
_sync_pause_entry() # 迭代 17 卡③——同上
func _on_abandon_requested() -> void:
# 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——推翻 冒险结构.md「MVP 无界面入口」后置条款;
# 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型,
# 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层
if not _adventure_mode or adventure == null:
return
if not adventure.abandon():
logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"})
board_zone.toast.show_toast("战斗进行中——无法放弃", "warn")
# ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)----
# ---- 最小设置面(迭代 8 卡④——入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)----
func _open_settings() -> void:
_start_overlay.visible = false
_open_modal(settings_overlay) # 模态栈单口(T3 收口批)
func _on_settings_closed() -> void:
_close_modal(settings_overlay) # 模态栈单口(T3 收口批)
_start_overlay.visible = true
_refresh_resume_button()
# ---- 局内暂停层·只读(迭代 17 卡③——入口角标在两屏装配段〔组 B〕;开合与数据喂入归组合根)----
# 暂停层快照投影(R4 数据只读同源:每次开层实时读 MetaProgression 访问器现值拼装——
# 快照仅服务单次开层生命周期(open_pause 重建各行),跨开层零持有;零写调用)
func _pause_data() -> Dictionary:
var forge_rows: Array[Dictionary] = []
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = db.meta_forge_row(attr)
forge_rows.append({
"attr": attr,
"display_name": String(row.get("display_name", "")),
"level": meta.forge_level(attr),
"flat": int(row.get("flat_value", 0)),
})
return {
"balance": meta.keystone_balance(),
"forge": forge_rows,
"workshop": meta.workshop_status(),
"tree": meta.tree_camp_rows(),
"fork_groups": meta.tree_fork_groups(),
}
func _open_pause_layer() -> void:
_open_modal(pause_overlay) # 模态栈单口(开层前排空排播队列——既有层同款行为,目检确认项)
pause_overlay.open_pause(_pause_data())
func _on_pause_closed() -> void:
_close_modal(pause_overlay) # 模态栈单口
# 入口角标点击(两屏同款处理器):P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,
# 探针 pressed.emit 绕过 disabled 时由此兜住;地图屏自身的登记开态不计——has_open_excluding)
func _on_pause_badge_pressed() -> void:
var map_on: bool = level_screen != null and level_screen.visible
# P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住;
# 地图屏自身的登记开态不计;迭代 18 卡⑦ T2——判据排除调试浮窗:对局中浮窗开着时暂停角标仍可点)
if _modals == null or (map_on and _modals.has_open_excluding_any([level_screen, debug_overlay])) \
or (not map_on and _modals.has_open_excluding(debug_overlay)):
return
_open_pause_layer()
# 入口角标可见性/置灰同步(P1 丁+守卫——单一同步点):战斗屏角标=对局内且地图屏未激活;
# 地图屏角标=随屏;置灰=登记册上除地图屏自身外任一层开着(防「看得见点不着」态)。
# 调用面=模态开合单口与屏级互斥口(凡影响开态/屏态的路径必经其一)。
func _sync_pause_entry() -> void:
if pause_overlay == null or _modals == null or board_zone == null or level_screen == null:
return
var map_on: bool = level_screen.visible
var battle_live: bool = _level_active or settlement != null # 调试两层无 _level_active——settlement 判据补位(迭代 18 卡⑦)
board_zone.pause_badge.visible = _level_active and not map_on
level_screen.pause_badge.visible = map_on
# 置灰判据改排除式(迭代 18 卡⑦ T2——调试浮窗开态不计入:对局中浮窗开着时角标仍可点)
board_zone.pause_badge.disabled = _modals.has_open_excluding(debug_overlay)
level_screen.pause_badge.disabled = _modals.has_open_excluding_any([level_screen, debug_overlay])
# 调试浮窗角标(迭代 18 卡⑦ P2 甲——迁宿主两屏,DS_DEBUG 门控;17卡③暂停角标同形制同守卫):
# 战斗屏=对局内(冒险或调试两层)且地图屏未激活;地图屏=随屏;开始菜单零角标(DS_DEBUG=1 启动即开)
board_zone.debug_badge.visible = _debug_entry and not map_on and battle_live
level_screen.debug_badge.visible = _debug_entry and map_on
board_zone.debug_badge.disabled = board_zone.pause_badge.disabled
level_screen.debug_badge.disabled = level_screen.pause_badge.disabled
# ---- 探针缝保缝(T4/D4 拆件——click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)----
func _on_camp_closed() -> void:
_camp_pres.on_camp_closed()
func _camp_data() -> Dictionary:
return _camp_pres.camp_data()
func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void:
# wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷
logger.log_wallet(event, gp, extra)
_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:
if runner.is_shop_pool():
shop_overlay.refresh_shelf_state(int(runner.gp)) # pool 面余额联动(货架内容仅买后重抽变)
else:
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
_refresh_presentations()
# ==================== 玩家意图 → 核心写调用(唯一出口)====================
func _on_board_action(cell: Vector2i, kind: String) -> void:
# 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口)
if _input_locked():
return
# 结算演播期棋盘冻结(T17 用户明确令 v3——不可点/不可移/不可挖):
# 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。
# 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前
# _flush_row_queue 自行排空)。
# **点击不丢弃**(v3.1——用户实机「卡死」根因):锁期点击缓存最后一次,演播完边沿
# (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。
if not _row_queue.is_empty():
_locked_click = {"cell": cell, "kind": kind}
return
# 秘宝选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格;
# 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗)
var treasure_sel: int = _treasure_ui.active_selection()
if treasure_sel != 0 and kind == "dig":
_treasure_ui.attempt_use(treasure_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.treasure_inventory != null:
_treasure_ui.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 加固)
if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared():
board_zone.fx.play_clear_burst()
if accepted and kind == "dig" and settlement != null and settlement.minesweeper != null:
# 挖掘演出(棋盘小人贴图批 2026-09-25):挥铲三帧 + 角色弹跳——触发=格子点开那一刻
#(2026-09-25 用户裁决;settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态;
# 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位)
var hero_cell: Vector2i = settlement.minesweeper.hero_pos()
board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale())
board_view.play_hero_dig_bounce()
elif accepted and kind == "move":
board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
board_view.refresh()
_refresh_presentations()
func _on_stop_pressed() -> void:
if _input_locked():
return
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted))
if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided":
# voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗)
board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"})
board_view.refresh()
_refresh_presentations()
func _on_refresh_confirmed() -> void:
# 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板旗作废,每轮上限读表)
if _input_locked():
return
var ok: bool = false
if _level_active and runner != null:
ok = runner.player_request_refresh()
elif settlement != null:
ok = settlement.player_request_refresh()
logger.log_action("refresh", Vector2i(-1, -1), ok)
if ok:
_refresh_used_round += 1
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round})
board_zone.toast.show_toast("RAP−%d · 旧板旗作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold")
else:
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
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})
if result == "rejected_rap":
board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
ap_bar.pulse_rap()
else:
board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
board_view.refresh()
_refresh_presentations()
func _handle_outcome_feedback(outcome: Dictionary) -> void:
# 双源拒绝反馈(pattern 1):每次拒绝玩家可感知 + 来源可区分 + 返回值命令式驱动(无拒绝事件)
if outcome.is_empty() or bool(outcome.get("accepted", false)):
return
logger.log_ux("ux_reject", {"source": String(outcome.get("reject_reason", "")),
"sub_reason": String(outcome.get("sub_reason", ""))})
board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
if String(outcome.get("reject_reason", "")) == "resource_face":
ap_bar.pulse_ap()
# ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)====================
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round)
HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining)
_sync_settlement_visuals()
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()) # 结算轮光带(演出链批)——时序敏感演出触发,例外留在组合根
_was_settling = settling
HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, _phase_text())
func _is_settling() -> bool:
# T17 修复:结算视觉期跟随排播演播到放完。核心层同步走完(轮末结算后逻辑秒回 Active),
# 旧判定让演播期棋盘提前恢复可挖观感=用户「没意识到在结算演出」感知缺口的根源;
# 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹
# 窗口不可作演播中标志——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 String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING:
return true
return not _row_queue.is_empty()
func _phase_text() -> String:
# 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫;相位取数经
# presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批)
if settlement == null:
return "第 — 轮 · —"
return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result),
String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING,
_boss_active, Narrator.boss_name(runner, db))
# ==================== B 区明细带 / 图例(模态入口)====================
func _on_legend_toggled() -> void:
if legend_overlay.is_open():
_close_modal(legend_overlay)
else:
_open_modal(legend_overlay)
# ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)====================
func _on_diagnose_pressed() -> void:
# 迭代 18 卡⑦——本局种子现值(原 _seed_value() 重算冒充病灶改向 _current_master_seed 只读单点)
var ctx := {"mode": "level" if _level_active else "debug", "seed": _current_master_seed()}
if _level_active and runner != null:
ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result),
"hero_hp": runner.hero_hp, "settlement_is_null": runner.current_battle() == null}
if settlement != null:
ctx["settlement"] = settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12)
var path: String = logger.dump_diagnosis(ctx)
board_zone.toast.show_toast("已转储诊断: " + path, "info")
# ==================== 调试浮窗(迭代 14.5 整合批——原数值调整板节收编;开合/应用/模拟编排组合根承接)====================
func _show_debug_overlay() -> void:
# 门禁改向(迭代 18 卡⑦ P2/P3 甲——对局中可开;本函数仍是唯一开窗入口 C6,门禁逻辑收函数内):
# 旧「仅开始菜单可开」门随入口迁宿主退役——棋盘输入锁/暂停角标判据已排除浮窗(T2 防护),
# 遮挡语义=P1 甲 ModalStack 登记制(他模态开时暂隐、关后自动恢复),此处零场景限制
if debug_overlay == null:
return
_open_modal(debug_overlay)
_sync_debug_overlay()
# 调试浮窗角标点击(两屏同款处理器——17卡③暂停角标同形制守卫;C6:仍收口 _show_debug_overlay 单口)
func _on_debug_badge_pressed() -> void:
var map_on: bool = level_screen != null and level_screen.visible
if _modals == null or (map_on and _modals.has_open_excluding_any([level_screen, debug_overlay])) \
or (not map_on and _modals.has_open_excluding(debug_overlay)):
return
_show_debug_overlay()
# 浮窗上下文同步(迭代 18 卡⑦——当局数据刷新+对局中按钮禁用单点):调用面=开窗口与模态开合
# 单口(凡影响开态/对局态的路径必经其一——与 _sync_pause_entry 同款纪律)
func _sync_debug_overlay() -> void:
if debug_overlay == null:
return
debug_overlay.set_context_locked(settlement != null or _adventure_mode) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放——T2)
debug_overlay.update_run_data(_debug_run_data()) # T3 当局数据只读区块喂入(开窗+模态开合信号驱动)
# 当局数据六项只读投影(P4 甲——C4 零直读内部状态、零写通路):种子=adventure.master_seed 现值
# (非 _seed_value() 重算);调试两层/非对局无冒险对象=RNG 服务 master_seed 现值(E5 只读出口,
# 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域);
# 缺位=「—」占位。零动效(纯标签重查——17卡③契约)。
func _debug_run_data() -> Dictionary:
var r = adventure.runner() if adventure != null else null
var seed_now: int = _current_master_seed()
var seed_txt: String = str(seed_now) if seed_now >= 0 else "—"
if r == null:
return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"}
return {"seed": seed_txt, "level": int(adventure.level_index), "node": int(r.node_index),
"hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(adventure.keystone)}
# 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值;
# 调试两层=RNG 服务 master_seed。禁 _seed_value() 重算冒充本局种子(main.gd 旧 :1174 病灶随批改向)。
func _current_master_seed() -> int:
if adventure != null:
return int(adventure.master_seed)
return int(get_node("/root/RngService").master_seed)
func _close_debug_overlay() -> void:
_close_modal(debug_overlay) # 模态栈单口——开始层全程未隐藏,无需恢复
func _on_tune_apply(atk_pct: int, hp_pct: int) -> void:
# E4:加成无持久态可"落"——各战斗开局经 _debug_hero_override/runner 现算生效(应用=关闭浮窗)
_close_debug_overlay()
func _on_tune_simulate(n: int) -> void:
# 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数。
# 对局中禁用兜底(迭代 18 卡⑦ T2——长程异步模拟与活跃结算并存态不开放;探针 emit 绕过 disabled 时由此守住)
if settlement != null:
return
_tune_sim.run(n)
# ==================== 工具 ====================
func _seed_value() -> int:
# 种子三分判定单点(迭代 18 卡⑦ C1——全工程唯一种子框读取处,禁第二处读框):
# ①显式合法整数全场景优先(含首局——测试/调试接口显式指定;第五路 DebugSession Callable
# 经本函数=调试对局与玩家同口径,P6 甲);
# ②首局固定 42(P5 甲——存档现役布尔 is_first_adventure_done:新档显式 false=教学局恒 42〔教学
# 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机);
# ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属 ADR-0007 豁免面,不进玩法 RNG 消费)
if _seed_edit.text.is_valid_int():
return int(_seed_edit.text)
if save_manager != null and save_manager.is_first_adventure_done():
return randi()
return 42