1379 lines
79 KiB
GDScript
1379 lines
79 KiB
GDScript
# 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_excluding(debug_overlay) 排除式
|
||
# 开态查询+三例外(迭代 18 卡⑦——浮窗开态不计入锁);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 career_overlay: CareerOverlay # 生涯页·只读全屏页(迭代 18 卡④——跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕)
|
||
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 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局
|
||
# < 商店 < 营地 < 设置 < 暂停(只读) < 生涯页(只读·迭代 18 卡④) < 确认弹窗 < 调试浮窗)
|
||
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)
|
||
# 生涯页·只读全屏页(迭代 18 卡④——P3 甲:登记序=营地同档,设置层旁、确认弹窗之前;
|
||
# 树序同步=addChild 同位(登记序=树序=z 序纪律);数据=组合根开层实时读 SaveManager 计数
|
||
# 访问器+MetaProgression 访问器喂入,本层零写通路零跳转;入口=开始层「营地与系统」组生涯钮)
|
||
career_overlay = CareerOverlay.new()
|
||
add_child(career_overlay)
|
||
career_overlay.closed.connect(_on_career_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.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组)
|
||
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 甲;生涯页同档随暂停层之后(迭代 18 卡④ P3 甲))
|
||
_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, career_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())
|
||
# 迭代 19 卡⓪①——同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径)
|
||
if not adventure.resume_adventure(bp, build_adventure_params(save_manager, meta,
|
||
{"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 卡①——停点快照写盘;调试/模拟不经过此路径)
|
||
|
||
|
||
# 冒险开局参数面构造(主菜单开新/续玩两入口共用——迭代 19 卡⓪①:同调三键随开局下传,真实局此前恒空。
|
||
# 取值口径与营地投影完全同源:装配集=save.attunement_equipped(save_manager.gd 访问器)、Y 刷新池=
|
||
# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects);
|
||
# hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。
|
||
# 注:runner 侧 resume_adventure 受理面现收 attunement/attune_refresh_y/tree_rules 三键
|
||
# (2026-10-08 尾裁微批b 卡⓪受理闭环——tree_rules 受理补齐,传参/受理两侧对齐,披露句退役)。
|
||
static func build_adventure_params(save_manager_ref, meta_ref, extra: Dictionary = {}) -> Dictionary:
|
||
var params: Dictionary = {
|
||
"hero_override": meta_ref.hero_override() if meta_ref != null else {},
|
||
"attunement": save_manager_ref.attunement_equipped() if save_manager_ref != null else [],
|
||
"attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0,
|
||
"tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [],
|
||
}
|
||
params.merge(extra, true)
|
||
return params
|
||
|
||
|
||
# 冒险模式(三关连探——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(教学重放);置位后逐关抽签
|
||
# 迭代 19 卡⓪①——同调三键随开局下传(build_adventure_params 统一构造,与营地投影同源)
|
||
var first_script: bool = not save_manager.is_first_adventure_done()
|
||
if not adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta,
|
||
{"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()
|
||
# 迭代 19 卡⓪②——开场行改传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源:
|
||
# 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」
|
||
# 后缀判断同步改认局外加成——永久强化 atk/max_hp flat 或调试浮窗 pct 任一存在才带,白板局不带
|
||
var ov: Dictionary = meta.hero_override() if meta != null else {}
|
||
log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed),
|
||
runner.effective_atk(), runner.effective_max_hp(),
|
||
int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0
|
||
or 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)
|
||
# 提灯视野同步(迭代 19 卡④——呈现桥:每战首轮按装配集显式重算 set_fog_lantern 含否〔🔴 契约 7
|
||
# Q8.1 复原语义:禁依赖战斗结束隐式复原——下一战装配链重算即含「未装配→写 0」复原〕)。挂点时序
|
||
# 依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)——board_changed 在
|
||
# _install_new_board 内先发、装配集后注入,同帧直挂会读到上一战的旧集,故挂此处不挂 _on_board_changed。
|
||
# settlement 空防御照 :854 既有形态(接手先于本信号:冒险路径 board_changed 内 runner 同步、debug
|
||
# 路径 battle_created 先于 begin_battle)。未装配=false→写 0=缺省零扰动基线逐位不变。
|
||
if ri == 1:
|
||
_sync_fog_lantern()
|
||
# 开战横幅——板级地形变异预告(全局作用域变异首发批 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()
|
||
|
||
|
||
# 提灯视野呈现桥(迭代 19 卡④——装配集只读出口消费面:settlement.attune_has("fog_lantern") 真值
|
||
# 显式写给 board_view.set_fog_lantern(true=半径 8+2 / false=复原 0),写入即 setter 内 refresh 全板
|
||
# 重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:只写呈现态布尔,
|
||
# minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。
|
||
func _sync_fog_lantern() -> void:
|
||
if settlement == null or board_view == null:
|
||
return
|
||
board_view.set_fog_lantern(settlement.attune_has("fog_lantern"))
|
||
|
||
|
||
# ---- 一次性非模态提示(每 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 重建各行),跨开层零持有;零写调用。
|
||
# 属性锻造三行投影已删——迭代 21 柜台退役连带〔暂停层锻造段同步删除〕)
|
||
func _pause_data() -> Dictionary:
|
||
return {
|
||
"balance": meta.keystone_balance(),
|
||
"workshop": meta.treasure_shop_status(), # 迭代 22 T2——秘宝匣 12 件解锁投影(旧工坊面退役)
|
||
"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) # 模态栈单口
|
||
|
||
|
||
# ---- 生涯页·只读(迭代 18 卡④——入口=开始层生涯钮;开合仿设置面先例:开始层互斥;
|
||
# 数据快照随开随建零写通路,页内零跳转)----
|
||
|
||
func _open_career() -> void:
|
||
_start_overlay.visible = false
|
||
_open_modal(career_overlay) # 模态栈单口
|
||
career_overlay.open_career(_career_data())
|
||
|
||
|
||
func _on_career_closed() -> void:
|
||
_close_modal(career_overlay) # 模态栈单口
|
||
_start_overlay.visible = true
|
||
_refresh_resume_button()
|
||
|
||
|
||
# 生涯页快照投影(只读同源——每次开层实时读 SaveManager 计数访问器 + MetaProgression 访问器
|
||
# 现值拼装(暂停层 _pause_data 同构);快照仅服务单次开层生命周期,跨开层零持有;零写调用。
|
||
# 账号成长态三投影已删〔forge_levels_total/workshop/tree——2026-10-12 用户裁:生涯页不统计
|
||
# 天赋树与账号成长态,只统计终身计数〕)
|
||
func _career_data() -> Dictionary:
|
||
var clears: Array[Dictionary] = []
|
||
var boss_first := 0
|
||
for i in db.level_count():
|
||
var li: int = i + 1
|
||
clears.append({"display_name": String(db.level_row(li).get("display_name", "")),
|
||
"count": save_manager.clear_count(li)})
|
||
if save_manager.boss_first_kill_marked(li):
|
||
boss_first += 1
|
||
return {
|
||
"total": save_manager.adventure_total(),
|
||
"cleared": save_manager.adventure_cleared(),
|
||
"clears": clears,
|
||
"boss_first_kills": boss_first,
|
||
"boss_first_total": db.level_count(),
|
||
"boss_kills": save_manager.boss_kill_count(),
|
||
"keystone": meta.keystone_balance(),
|
||
}
|
||
|
||
|
||
# 入口角标点击(两屏同款处理器):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
|