类/文件改名 route_overlay.gd→level_map_screen.gd(RouteOverlay 退役,uid 随迁);屏底=暗幕 0.6 半透→不透明 BG_ROOT+石纹底噪(TEX_BACKDROP_STONE load_tex 守卫缺位纯色回退,spec Layout 形态升格);open_choices 进入转场 alpha 淡入 200ms(UiKit.motion;就地刷新/模态栈恢复不重放)。 组合根:_battle_root 宿主引用留存+_level_screen_active 状态+_enter/_exit_level_screen 单一出口(进屏隐战斗屏/出屏显战斗屏);_on_choices_presented 走进屏;_on_route_chosen 出屏分支;_on_adventure_finished 强制回战斗屏呈现摘要(放弃入口终局兜底)。_input_locked/模态登记序(节点层与商店层之间)不变——兑付覆盖层域内弹出的遮挡恢复语义由模态栈零改动复用。 探针机械改名 main.route_overlay→main.level_screen(ui_probe 5/ui_click_probe 4/ui_input 5/ui_tune 2 处)。坑⑤再证:新 class_name 后未刷编辑器缓存致四探针 Parse Error 超时——--editor --quit 后全绿。轻闸=冒烟 557/0+五探针 PASS。
872 lines
42 KiB
GDScript
872 lines
42 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——本类不拼呈现字符串。
|
||
# ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于驱动视图状态。
|
||
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 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 logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
|
||
var _level_active := false
|
||
var _seed_edit: LineEdit
|
||
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 之上、模态之下)
|
||
# 模态 overlay(z 序 = 登记序;pattern 6 层级纪律)
|
||
var legend_overlay: LegendOverlay
|
||
var review_overlay: SettlementReviewOverlay
|
||
var node_overlay: NodeOverlay
|
||
var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现)
|
||
var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现)
|
||
var tune_panel: TunePanel
|
||
|
||
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 # 结算相位上升沿检测(结算轮光带触发——演出链批)
|
||
|
||
|
||
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()
|
||
# 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点
|
||
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()
|
||
_start_overlay.visible = true
|
||
|
||
|
||
# ==================== 视图接线(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
|
||
log_view.review_requested.connect(_open_review_from_band)
|
||
board_zone.action_requested.connect(_on_board_action)
|
||
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)
|
||
|
||
logger = PlaytestLogger.new()
|
||
logger.ap_per_round = int(db.action_economy.ap_per_round)
|
||
|
||
# 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 选路 < 终局 < 商店 < 调整板 < 开始)
|
||
var overlays: Dictionary = UiAssembler.build_overlays(self, db, int(db.hero.atk), int(db.hero.max_hp))
|
||
legend_overlay = overlays.legend_overlay
|
||
review_overlay = overlays.review_overlay
|
||
node_overlay = overlays.node_overlay
|
||
_end_overlay = overlays.end_overlay
|
||
shop_overlay = overlays.shop_overlay
|
||
camp_overlay = overlays.camp_overlay
|
||
tune_panel = overlays.tune_panel
|
||
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))
|
||
review_overlay.closed.connect(_on_modal_closed.bind(review_overlay))
|
||
node_overlay.continue_pressed.connect(_on_node_continue)
|
||
level_screen.route_chosen.connect(_on_route_chosen)
|
||
level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
|
||
shop_overlay.buy_requested.connect(_on_shop_buy)
|
||
shop_overlay.leave_pressed.connect(_on_shop_leave)
|
||
camp_overlay.buy_requested.connect(_on_camp_buy)
|
||
camp_overlay.closed.connect(_on_camp_closed)
|
||
_end_overlay.review_pressed.connect(_on_end_review)
|
||
_end_overlay.continue_pressed.connect(_on_battle_end_continue)
|
||
tune_panel.closed.connect(_close_tune_panel)
|
||
tune_panel.apply_requested.connect(_on_tune_apply)
|
||
tune_panel.simulate_requested.connect(_on_tune_simulate)
|
||
_modals = ModalStack.new()
|
||
for m: Control in [legend_overlay, review_overlay, node_overlay, level_screen, _end_overlay, shop_overlay, camp_overlay, tune_panel]:
|
||
_register_modal(m)
|
||
_tune_sim = TuneSimController.new(db, tune_panel, int(db.hero.max_hp))
|
||
|
||
var start: Dictionary = UiAssembler.build_start_overlay(self, db)
|
||
_start_overlay = start.overlay
|
||
_seed_edit = start.seed_edit
|
||
start.lvbtn.pressed.connect(_start_adventure_from_menu)
|
||
start.tunebtn.pressed.connect(_show_tune_panel)
|
||
start.dbgbtn.pressed.connect(_start_debug_levels)
|
||
start.campbtn.pressed.connect(_open_camp)
|
||
|
||
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)
|
||
|
||
|
||
func _fatal_screen(msg: String) -> void:
|
||
var l := UiKit.label(msg, 20, true)
|
||
l.add_theme_color_override("font_color", 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:
|
||
# 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈
|
||
_modals.open(m)
|
||
|
||
|
||
func _close_modal(m: Control) -> void:
|
||
_modals.close(m)
|
||
|
||
|
||
func _on_modal_closed(m: Control) -> void:
|
||
_close_modal(m)
|
||
|
||
|
||
func _input_locked() -> bool:
|
||
# 模态/非战斗期棋盘输入锁(各 overlay 自身可见即锁定)
|
||
return legend_overlay.is_open() or review_overlay.is_open() or _end_overlay.is_open() \
|
||
or node_overlay.visible or level_screen.visible or shop_overlay.visible or tune_panel.visible \
|
||
or _start_overlay.visible or _inter_overlay.visible or settlement == null
|
||
|
||
|
||
# ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)====================
|
||
|
||
func _start_adventure_from_menu() -> void:
|
||
_start_overlay.visible = false
|
||
_start_adventure()
|
||
|
||
|
||
# 冒险模式(三关连探——AdventureRunner 编排;转发信号签名与 runner 一致,接线照原样对接;
|
||
# 信号连接只在组合根——ADR-0001)
|
||
func _start_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)
|
||
adventure.big_reward_paid.connect(_on_big_reward)
|
||
adventure.adventure_finished.connect(_on_adventure_finished)
|
||
adventure.level_started.connect(_on_adventure_level_started)
|
||
_level_active = true
|
||
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(tune_panel.atk_pct(), tune_panel.hp_pct())
|
||
# 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
|
||
if not adventure.start_adventure(seed_val, {"hero_override": meta.hero_override() if meta != null else {}}):
|
||
_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),
|
||
tune_panel.atk_pct() > 0 or tune_panel.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":
|
||
_open_modal(node_overlay)
|
||
node_overlay.show_reward()
|
||
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 接线),零自动推进
|
||
_open_modal(shop_overlay)
|
||
shop_overlay.open_shop(runner.shop_products(), int(runner.gp))
|
||
log_view.append_bb(Narrator.shop_line(int(runner.gp)))
|
||
logger.log_node(node, info, runner.node_index)
|
||
_refresh_presentations()
|
||
|
||
|
||
# ---- 冒险模式终局面(大奖励屏 / 冒险结算屏——冒险结构实现批)----
|
||
|
||
# 大奖励兑付(非末关通关恰一次):复用 _inter_overlay(label + 确认按钮,零新增交互范式);
|
||
# 文案装配归 Narrator
|
||
func _on_big_reward(reward: Dictionary) -> void:
|
||
_inter_label.text = Narrator.big_reward_text(reward)
|
||
_inter_overlay.visible = true
|
||
_refresh_presentations()
|
||
|
||
|
||
# 冒险结算(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_settlement_visuals() # → 全关
|
||
stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体 2026-09-26 迁 B 区站桩)
|
||
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
|
||
# 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变——
|
||
# 调试两层不经过本处理器,天然不写档不污染生涯数据)。
|
||
var detail: Dictionary = {}
|
||
if meta != null:
|
||
detail = meta.settle_adventure(summary)
|
||
var text := Narrator.adventure_summary_text(summary)
|
||
if not detail.is_empty():
|
||
text += "\n" + Narrator.keystone_settle_text(detail, _item_name_lookup())
|
||
if result == "completed":
|
||
_end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect())
|
||
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(_end_overlay)
|
||
_refresh_presentations()
|
||
|
||
|
||
# 工坊 item_id → 显示名投影(keystone_settle_text 文案数据源——装配归本根,拼句归 Narrator)
|
||
func _item_name_lookup() -> Dictionary:
|
||
var lookup := {}
|
||
if meta != null:
|
||
for row in db.meta_workshop_items():
|
||
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
|
||
return lookup
|
||
|
||
|
||
func _on_battle_end_continue() -> void:
|
||
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
|
||
_close_modal(_end_overlay)
|
||
_end_overlay.close()
|
||
_debug.reset()
|
||
if runner != null:
|
||
runner.restore_hero_base()
|
||
runner = null
|
||
settlement = null
|
||
adventure = null
|
||
_adventure_mode = false
|
||
tune_panel.reset_sliders()
|
||
_start_overlay.visible = true
|
||
_refresh_presentations()
|
||
|
||
|
||
func _on_end_review() -> void:
|
||
review_overlay.open(log_view.parsed_text())
|
||
|
||
|
||
# ==================== 调试模式(连续两层——编排归 debug_session.gd;
|
||
# 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用)====================
|
||
|
||
func _start_debug_levels() -> void:
|
||
tune_panel.reset_sliders()
|
||
_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 整类隐患随之移除)
|
||
func _debug_hero_override() -> Dictionary:
|
||
return {"atk": _scaled(int(db.hero.atk), tune_panel.atk_pct()),
|
||
"max_hp": _scaled(int(db.hero.max_hp), tune_panel.hp_pct())}
|
||
|
||
|
||
func _scaled(base: int, pct: int) -> int:
|
||
return maxi(1, int(round(float(base) * (100.0 + float(pct)) / 100.0)))
|
||
|
||
|
||
# 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音)
|
||
func _identity_lookup(c: Vector2i):
|
||
if settlement == null or settlement.minesweeper.current_board() == null or settlement.action_economy.phase != settlement.action_economy.PHASE_ACTIVE:
|
||
return null
|
||
return settlement.query_reward_identity(c)
|
||
|
||
|
||
# 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动)
|
||
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 = settlement.db.variation.row_by_id(String(vid))
|
||
if String(vrow.get("effect", "")) == "hide_number":
|
||
return true
|
||
return false
|
||
|
||
|
||
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 反馈面)
|
||
stage.gray_all_enemies() # 死亡兜底:站桩灰化残位(禁瞬消语义的战斗收尾面)
|
||
ap_bar.set_rap(0) # RAP 清零脉冲 + 微文案(载体 2026-09-26 迁 D 区 AP 条)
|
||
stage.hero_standee.set_effects_cleared_feedback() # 状态全清可见反馈(增益状态×4④——载体迁 B 区站桩)
|
||
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))
|
||
"standalone_end":
|
||
if result == "victory":
|
||
_end_overlay.open_victory(Narrator.battle_end_text("victory", false),
|
||
"回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload))
|
||
else:
|
||
_end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
|
||
"返回", board_view.get_global_rect())
|
||
_open_modal(_end_overlay)
|
||
_refresh_presentations()
|
||
|
||
|
||
# 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;账本语义零改动,纯呈现读数):
|
||
# 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账
|
||
# 连接先于组合根 handler——此读数即战后入账完毕的总量)。
|
||
func _gp_summary_text(payload: Dictionary) -> String:
|
||
return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)]
|
||
|
||
|
||
# ==================== 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)
|
||
|
||
|
||
func _on_step_row(row: Dictionary) -> void:
|
||
log_view.append_row(row)
|
||
stage.on_step_row_fx(row)
|
||
_play_cross_zone_fx(row) # 跨区因果演出(结算演出链批):开格之因 → 站桩之果
|
||
# 高光演出(战斗结算×3①② 占位):完美排雷横幅 / 标中旗加重弹道
|
||
if String(row.get("type", "")) == "highlight":
|
||
board_zone.fx.play_perfect_sweep()
|
||
elif String(row.get("highlight", "")) == "flag_hit":
|
||
board_zone.fx.play_flag_hit_projectile(true)
|
||
_refresh_presentations()
|
||
|
||
|
||
# 跨区因果演出(§Transitions 结算逐行兑现——动画密度终稿归战斗表现样张批):
|
||
# 伤害行 → ⚔ 弹道自棋盘飞向目标敌方站桩(crit 金档加重);治疗行 → ✚ 弹道飞向主角站桩;
|
||
# 命中反馈(闪白/血条递减/灰化)由站桩侧 on_step_row_fx/set_data 承接
|
||
func _play_cross_zone_fx(row: Dictionary) -> void:
|
||
var from_v: Vector2 = board_view.get_global_rect().get_center()
|
||
match String(row.get("type", "")):
|
||
"damage":
|
||
var target := String(row.get("target", ""))
|
||
if target == "" or target == "hero":
|
||
return
|
||
var to_v = stage.enemy_standee_center(target)
|
||
if to_v != null:
|
||
var crit := bool(row.get("crit", false))
|
||
settlement_fx.play_projectile(from_v, to_v, "⚔",
|
||
UiKit.GOLD if crit else UiKit.RED_MINE, crit)
|
||
"heal":
|
||
if String(row.get("target", "")) != "hero" and String(row.get("target", "")) != "":
|
||
return
|
||
settlement_fx.play_projectile(from_v, stage.hero_standee_center(), "✚", Color("7cc47c"))
|
||
|
||
|
||
func _on_round_started(ri: int) -> void:
|
||
log_view.finish_settlement_round() # 上轮摘要行(挖掘期 1 行——明细带 pattern 3)
|
||
log_view.append_bb("[color=#e0b860]── 第 %d 轮 ──[/color]" % ri)
|
||
_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))
|
||
_enter_level_screen()
|
||
level_screen.open_choices(candidates, runner)
|
||
_refresh_presentations()
|
||
|
||
|
||
func _on_route_chosen(node_id: int) -> void:
|
||
_close_modal(level_screen)
|
||
if not level_screen.visible:
|
||
_exit_level_screen() # 战斗/Boss 节点:切回战斗屏;奖励/恢复/商店:本屏保持激活(T3 接管域内弹出)
|
||
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)
|
||
|
||
|
||
func _exit_level_screen() -> void:
|
||
_level_screen_active = false
|
||
_battle_root.visible = true
|
||
if level_screen.visible:
|
||
_close_modal(level_screen)
|
||
|
||
|
||
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")
|
||
|
||
|
||
# ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)----
|
||
|
||
# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后,
|
||
# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账)
|
||
func _open_camp() -> void:
|
||
if meta == null:
|
||
return
|
||
meta.refresh_workshop_unlocks()
|
||
_start_overlay.visible = false
|
||
camp_overlay.open_camp(_camp_data())
|
||
_refresh_presentations()
|
||
|
||
|
||
func _on_camp_closed() -> void:
|
||
camp_overlay.visible = false
|
||
_start_overlay.visible = true
|
||
_refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
|
||
|
||
|
||
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
|
||
func _camp_data() -> Dictionary:
|
||
var forge_rows: Array = []
|
||
if meta != null:
|
||
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)),
|
||
"price": meta.forge_price(attr),
|
||
})
|
||
return {
|
||
"balance": meta.keystone_balance() if meta != null else 0,
|
||
"forge": forge_rows,
|
||
"workshop": meta.workshop_status() if meta != null else [],
|
||
"recent": save_manager.recent_summaries() if save_manager != null else [],
|
||
}
|
||
|
||
|
||
func _on_camp_buy(attr: String) -> void:
|
||
if meta == null:
|
||
return
|
||
if not meta.buy_forge(attr):
|
||
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
|
||
board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn")
|
||
return
|
||
camp_overlay.refresh_camp(_camp_data())
|
||
_refresh_presentations()
|
||
|
||
|
||
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
|
||
|
||
func _on_shop_buy(product_id: String) -> void:
|
||
if runner == null:
|
||
return
|
||
if not runner.buy_product(product_id):
|
||
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
|
||
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
|
||
board_zone.toast.show_toast("金币不足——买不起这件商品", "warn")
|
||
return
|
||
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
|
||
_refresh_presentations()
|
||
|
||
|
||
func _on_shop_leave() -> void:
|
||
_close_modal(shop_overlay)
|
||
if runner != null:
|
||
# 复用兑付确认纪律(接口漂移清偿):离开 ≠ 推进——确认后经 choices_presented 弹出选路口
|
||
runner.continue_after_node()
|
||
|
||
|
||
func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void:
|
||
# wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷
|
||
logger.log_wallet(event, gp, extra)
|
||
if shop_overlay.visible and runner != null:
|
||
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
|
||
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)
|
||
_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.action_economy.rap) 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
|
||
board_view.set_desaturated(settling)
|
||
board_view.set_dig_glow_enabled(not settling)
|
||
stage.set_settle_focus(settling)
|
||
log_view.set_mode("settle" if settling else "dig")
|
||
stage.set_round_text(_phase_text())
|
||
|
||
|
||
func _is_settling() -> bool:
|
||
if settlement == null or not settlement.is_battle_live():
|
||
return false
|
||
return settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING
|
||
|
||
|
||
func _phase_text() -> String:
|
||
# 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫)
|
||
if settlement == null:
|
||
return "第 — 轮 · —"
|
||
return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result),
|
||
settlement.action_economy.phase == settlement.action_economy.PHASE_WAITING,
|
||
_boss_active, Narrator.boss_name(runner, db))
|
||
|
||
|
||
# ==================== B 区明细带 / 图例(模态入口)====================
|
||
|
||
func _open_review_from_band() -> void:
|
||
review_overlay.open(log_view.parsed_text())
|
||
|
||
|
||
func _on_legend_toggled() -> void:
|
||
if legend_overlay.is_open():
|
||
_close_modal(legend_overlay)
|
||
else:
|
||
_open_modal(legend_overlay)
|
||
|
||
|
||
# ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)====================
|
||
|
||
func _on_diagnose_pressed() -> void:
|
||
var ctx := {"mode": "level" if _level_active else "debug", "seed": _seed_value()}
|
||
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")
|
||
|
||
|
||
# ==================== 数值调整板(加成应用/批量模拟编排——组合根承接,视图纯 UI)====================
|
||
|
||
func _show_tune_panel() -> void:
|
||
_start_overlay.visible = false
|
||
tune_panel.visible = true
|
||
|
||
|
||
func _close_tune_panel() -> void:
|
||
tune_panel.visible = false
|
||
_start_overlay.visible = true
|
||
|
||
|
||
func _on_tune_apply(atk_pct: int, hp_pct: int) -> void:
|
||
# E4:加成无持久态可"落"——各战斗开局经 _debug_hero_override/runner 现算生效(面板仅关闭回开始层)
|
||
_close_tune_panel()
|
||
|
||
|
||
func _on_tune_simulate(n: int) -> void:
|
||
# 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数
|
||
_tune_sim.run(n)
|
||
|
||
|
||
# ==================== 工具 ====================
|
||
|
||
func _seed_value() -> int:
|
||
if _seed_edit.text.is_valid_int():
|
||
return int(_seed_edit.text)
|
||
return 42
|