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

772 lines
39 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## VERTICAL SLICE - NOT FOR PRODUCTION
## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。
## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.flow(注册面编排口)。
## 界面句柄与层清单在 ui 注册面(ui_registry.gd)——屏壳/战区/模态三域句柄与 LAYER_CONTRACT 单点宿主。
## 局态全在 run_scope.gd 节点,禁在屏/视图侧持有局态引用;autoload 节点路径常量单点在 AUTOLOAD_PATH_*。
extends Control
# autoload 节点路径单点(降格实现:审计处方「编译期全局名」与本仓 --script
# 模式编译器不解析裸 autoload 标识符冲突〔run_all.gd 头注+pacing 实证〕——字符串拼写风险
# 收敛到四常量,类型面由读视图属性承载)
const AUTOLOAD_PATH_CFG := "/root/ConfigDB"
const AUTOLOAD_PATH_RNG := "/root/RngService"
const AUTOLOAD_PATH_SAVE := "/root/SaveManager"
const AUTOLOAD_PATH_META := "/root/MetaProgression"
const UiAssembler := preload("res://presentation/ui_assembler.gd")
const Narrator := preload("res://presentation/battle_narrator.gd")
const TuneSimController := preload("res://presentation/tune_sim_controller.gd")
const DebugSessionScript := preload("res://presentation/debug_session.gd")
const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互
const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互
const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影
const BoardResolversScript := preload("res://presentation/board_resolvers.gd") # 棋盘解析回调族
const MainUiBuild := preload("res://presentation/main_ui_build.gd") # 装配分步承接件
const MainAdventureFlow := preload("res://presentation/main_adventure_flow.gd") # 冒险流程承接件
const MainOverlayFlow := preload("res://presentation/main_overlay_flow.gd") # 覆盖层流程承接件
const MainDebugFlow := preload("res://presentation/main_debug_flow.gd") # 调试流程承接件
const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景
const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn")
const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn")
var db
var run_scope = null # 会话状态承载(run_scope.gd;_ready 即建闲时态,永非 null)
# SaveManager/MetaProgression 升格 autoload——成员名保留为「autoload 读视图」
#(属性 getter 经节点路径解析:--script 模式编译器不解析裸 autoload 标识符〔run_all.gd 头注在案〕,
# 节点路径双模式通用;组合根调用点/探针挂载面 main.save_manager 语义零迁移)。
var save_manager: SaveManagerCore:
get:
return get_node(AUTOLOAD_PATH_SAVE)
var meta: MetaProgressionCore:
get:
return get_node(AUTOLOAD_PATH_META)
var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件)
var _debug_entry := false # 调试会话门控(缺省 false=发布态:角标恒隐、浮窗无 UI 开启口;沿革查 git log)
var ui: UiRegistry # 界面注册面(ui_registry.gd——界面句柄/层序合同/模态编排句柄的单点宿主)
var _confirm_consumer := "" # 确认弹窗消费方状态量(契约 3 编排——open 前设、分派后清)
var _confirm_consumer_slot := 0 # 消费方附随槽号(delete_slot 消费方专用)
# 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI)
var _debug = null
# 拆件后的组合根持有面(职责见各类头注;装配见 _build_ui)
var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图)
var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入)
var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发)
var settings_store # 系统级设置独立存储(音量/全屏/减动效)
var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd)
func _replay_locked_click() -> void:
if run_scope.locked_click.is_empty() or not ui.screens.row_pacer.pending_rows().is_empty():
return
var click: Dictionary[Variant, Variant] = run_scope.locked_click
run_scope.locked_click = {}
_on_board_action(click["cell"], click["kind"])
# 会话生命周期:_ready 建闲时态(永非 null——全组合根零空判责),
# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。
func _begin_session(adventure_session: bool) -> void:
ui.screens.row_pacer.flush() # 会话更替即清队(待播行不住局态——机制保证,不靠时序约定)
if run_scope != null:
run_scope.queue_free()
run_scope = RunScope.new()
run_scope.is_adventure = adventure_session
add_child(run_scope)
func _ready() -> void:
db = get_node(AUTOLOAD_PATH_CFG)
if not db.is_ready:
var ok: bool = db.load_tables("res://data/tables", false)
if not ok:
_fatal_screen("数值表加载失败——见输出错误清单(fail-fast)")
return
# SaveManager/MetaProgression 升格 autoload:读盘已在各自 _ready 完成
#(autoload 先于主场景就绪——读盘时序等价),此处零动作。
# 系统级设置(独立存储不随存档):启动即恢复音量/全屏/减动效引擎态
settings_store = SettingsStore.new()
settings_store.load_settings()
settings_store.apply()
ui = UiRegistry.new() # 注册面先于装配建(_build_ui 八步向容器登记——装配时序零变化)
_build_ui()
_begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序)
ui.screens.start_overlay.visible = true
ui.screens.debug_badge.visible = _debug_entry # 浮层角标可见性须门控落定后赋值(构建/连接在 _build_ui);_debug_entry 缺省 false=发布态恒隐
# boot 菜单优先——启动不自动开浮窗(开始屏不得被调试面板盖住);
# 调试入口=UI 最顶层全局浮层角标(LAYER_CONTRACT 末位 float 类,任何 UI 不遮挡)
# ==================== 视图接线(三屏壳 + 模态层装配;构建归屏壳/UiAssembler,本类只接线) ====================
func _build_ui() -> void:
MainUiBuild.build(self)
ui.modals.menu.debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 诊断转储入口
func _fatal_screen(msg: String) -> void:
MainUiBuild.fatal_screen(self, msg)
# ==================== 冒险模式入口(唯一试玩入口)====================
func _start_adventure_from_menu() -> void:
# 主菜单「开始冒险」钮已随三入口化撤出(卡② mn6——主菜单侧断点互斥确认链同步摘除,
# 弹窗本体与两分支逻辑归卡④大门承接)。本函数为探针直调中间态保留(mn10——冒险段探针
# 直调位点逐个登记实现报告「已知中间态」;卡④到位后由大门两分支换代)。无激活态写盘拒绝
# 由卡① sv3 承接(结算不落盘)。
ui.screens.start_overlay.visible = false
_start_adventure()
# 确认弹窗分派(卡③ 契约 3):按 _confirm_consumer 路由——"delete_slot"=卡③ 删档确认
#(确认才删、取消只关窗——裁决 3)。"gate"/"gate_new"=卡④ 大门两分支消费方(有/无断点),
# "campfire_leave"=篝火二次确认(卡④ T4 接入——分派机制扩展第三/第四消费方)。
func _on_confirm_accepted() -> void:
ui.flow.close(ui.modals.menu.confirm_dialog) # 模态栈单口
match _confirm_consumer:
"delete_slot":
save_manager.slots.delete_slot(_confirm_consumer_slot)
ui.modals.menu.save_select_overlay.open_save_select(HudPresenter.save_select_data(save_manager)) # 就地刷新(该槽回空态)
"gate":
_camp_pres.on_camp_closed() # 冒险发起后营地场景屏隐藏(选档层同收——卡④ T4 关层收闭)
_resume_from_menu() # 继续上一段冒险(现役续玩编排同源——卡④ 契约 4)
"gate_new":
_camp_pres.on_camp_closed()
_start_adventure() # 开始新冒险(现役开局编排同源)
"campfire_leave":
_camp_pres.on_camp_closed() # 篝火确认离场=营地与选档层同收+开始层恢复(回主菜单)
_confirm_consumer = ""
# 次确认分派(三钮形态中钮——大门「开始新冒险」出口;卡④ T4)
func _on_confirm_secondary() -> void:
ui.flow.close(ui.modals.menu.confirm_dialog) # 模态栈单口
match _confirm_consumer:
"gate":
_camp_pres.on_camp_closed()
_start_adventure()
_confirm_consumer = ""
func _on_confirm_declined() -> void:
ui.flow.close(ui.modals.menu.confirm_dialog) # 只关窗(取消无状态变更——裁决 3:取消不删/卡④:退回营地);关侧恢复归 ModalFlow
_confirm_consumer = ""
# 退出游戏(一级菜单入口;设置即时落盘后退出)
func _quit_game() -> void:
if settings_store != null:
settings_store.save_settings()
get_tree().quit()
# 继续上局(resume_adventure 的消费入口):断点重建至最近停点
func _resume_from_menu() -> void:
if not save_manager.has_breakpoint():
return
var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot()
ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
ui.screens.start_overlay.visible = false
_begin_session(true)
_wire_adventure()
run_scope.level_active = true
_sync_status_entry() # 续玩恢复点亮状态面板入口角标
ui.modals.run.level_screen.set_abandon_available(true)
ui.battle.log_view.clear()
logger.start_session(int(bp.get("master_seed", 0)))
run_scope.adventure.set_hero_override(ui.modals.menu.debug_overlay.atk_pct(), ui.modals.menu.debug_overlay.hp_pct())
# 同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径)
if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta,
{"treasure_unlocked": _treasure_ui.unlocked_set()})):
_fatal_screen("续玩恢复失败——见输出日志")
return
func _wire_adventure() -> void:
MainAdventureFlow._wire_adventure(self)
# 冒险开局参数面构造(主菜单开新/续玩两入口共用:同调三键随开局下传。
# 取值口径与营地投影完全同源:装配集=save.attunement_equipped(save_manager.gd 访问器)、Y 刷新池=
# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects);
# hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。
# 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照;
# 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。
# 注:runner 侧 resume_adventure 受理面收 attunement/attune_refresh_y/tree_rules 三键
# (传参/受理两侧对齐)。
static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref: MetaProgressionCore, extra: Dictionary = {}) -> Dictionary:
var params: Dictionary[Variant, Variant] = {
"hero_override": meta_ref.hero_override() if meta_ref != null else {},
"attack_type": meta_ref.hero_type_axes()["attack"] if meta_ref != null else "",
"armor_type": meta_ref.hero_type_axes()["armor"] if meta_ref != null else "",
"attunement": save_manager_ref.meta_state.attune_state().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 一致,接线照原样对接;
# 信号连接只在组合根)
func _start_adventure() -> void:
ui.flow.close(ui.modals.menu.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
_begin_session(true)
_wire_adventure()
run_scope.level_active = true
_sync_status_entry() # 进对局点亮状态面板入口角标
ui.modals.run.level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期)
ui.battle.log_view.clear()
var seed_val := _seed_value()
logger.start_session(seed_val)
run_scope.adventure.set_hero_override(ui.modals.menu.debug_overlay.atk_pct(), ui.modals.menu.debug_overlay.hp_pct())
# 永久强化注入:冒险开始参数面——meta 现算 override 显式传参(全零 = 白板)
# 开局参数面(同调三键同源构造——确定性承载=固定种子+参数档,首局教学图由种子域三分判定给 42)
if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta,
{"treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})):
_fatal_screen("冒险启动失败——见输出日志")
return
func _on_adventure_level_started(level_index: int, display_name: String)-> void:
MainAdventureFlow._on_adventure_level_started(self, level_index, display_name)
func _on_runner_node(node: Dictionary, info: Dictionary)-> void:
MainAdventureFlow._on_runner_node(self, node, info)
func _on_adventure_finished(summary: Dictionary)-> void:
MainAdventureFlow._on_adventure_finished(self, summary)
func _on_battle_end_continue() -> void:
MainAdventureFlow._on_battle_end_continue(self)
func _start_debug_levels() -> void:
MainDebugFlow._start_debug_levels(self)
func _on_debug_battle_created(settlement_ref: BattleOrchestrator, _level: int, _carry: int)-> void:
MainDebugFlow._on_debug_battle_created(self, settlement_ref, _level, _carry)
func _on_debug_battle_ready(settlement_ref: BattleOrchestrator, _level: int, _carry: int)-> void:
MainDebugFlow._on_debug_battle_ready(self, settlement_ref, _level, _carry)
func _on_inter_next_pressed() -> void:
MainAdventureFlow._on_inter_next_pressed(self)
# 加成生效值供给 / 奖励徽章身份解析 / 地形变异两种解析 —— 见 board_resolvers.gd(有态绑定族判离)
func _show_board_fx_for_node(node_type: String = "") -> void:
# Boss 遭遇变体:紫赤影子爬棋盘一次性占位演出(§2.6)
if node_type == "boss":
ui.battle.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(ui.battle.stage, ui.battle.board_zone.ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道
var decision: Dictionary[Variant, Variant] = _debug.on_battle_finished(payload)
match String(decision.outcome):
"next_layer":
ui.screens.inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [
int(decision.layer), int(decision.carry_hp), int(db.combat.hero.max_hp)]
ui.screens.inter_overlay.visible = true
_refresh_presentations()
return
"level_end":
ui.modals.run.end_overlay.open_victory(Narrator.battle_end_text("victory", true),
"返回", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
"standalone_end":
if result == "victory":
ui.modals.run.end_overlay.open_victory(Narrator.battle_end_text("victory", false),
"回到遭遇选择", ui.battle.board_zone.board_view.get_global_rect(), _end_victory_opts(payload))
else:
ui.modals.run.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
"返回", ui.battle.board_zone.board_view.get_global_rect())
ui.flow.open_after_playback(ui.modals.run.end_overlay)
_refresh_presentations()
# 战利品行装配(battle.md OQ14——胜利总结结算展示 GP 总量+增量;纯呈现读数):
# 增量 = battle_finished 载荷 gp_dropped(战斗域上抛);总量 = runner.gp 现值(LevelRunner 入账
# 连接先于组合根 handler——此读数即战后入账完毕的总量)。
# runner 缺位守卫(续玩停点恢复分支:adventure_runner
# resume_adventure 的 bonus_pending 分支在 level_started 之前同步 curio_choice_presented.emit
# 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照
# 此刻必在档,语义=续玩恢复面的合计即存档合计;承接体见 HudPresenter.gp_summary_text)
func _gp_summary_text(payload: Dictionary) -> String:
return HudPresenter.gp_summary_text(payload, run_scope.runner, save_manager)
# ==================== core↔view 接线(战斗信号统一走这里)====================
## 战末账行束(open_victory opts 键装配单点——GP/钥石/加成三行与增量合计)
func _end_victory_opts(payload: Dictionary) -> Dictionary:
return {"gp_summary": _gp_summary_text(payload),
"ks_summary": "钥石 · 累计 %d" % run_scope.runner.keystone,
"bonus_summary": HudPresenter.bonus_summary_text(run_scope.runner),
"gp_gain": int(payload.get("gp_dropped", 0)), "gp_total": int(run_scope.runner.gp)}
func _wire_battle(settlement_ref: BattleOrchestrator) -> 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:
# 清图奖励可感知反馈(挖完全图须有奖励感——clear_blast 兑付对敌伤害
# 零可见,toast 桥接归组合根呈现层;零兑付(未生效)不弹安静留明细带)
if String(row.get("source_class", "")) == "clear_reward" and String(row.get("type", "")) != "reward_ineffective":
ui.battle.board_zone.toast.show_toast("清图奖励:%s" % String(row.get("display", "")).replace("(清图奖励)", ""), "info")
# 排播入口:减动效=延迟归零同帧直落(行呈现不跳过);常态入队按段阶梯播放
if UiKit.motion_scale < 1.0:
ui.screens.row_pacer.consume_now(row)
return
ui.screens.row_pacer.enqueue(row)
# 段间延迟取数面(减动效双态断言用——pacing_queue_test 直读;委托 row_pacer 同名面)
func pace_delay_for(prev_seg: String, next_seg: String) -> float:
return ui.screens.row_pacer.pace_delay_for(prev_seg, next_seg)
func _on_round_started(ri: int) -> void:
# 轮首分隔行随排播队列走(落在前轮各行之后——带序正确);轮横幅即轮切换时刻发射
#(view 内判减动效=静态化变体)。减动效快路=分隔行同帧直落(不跳过——consume_now 复用
# _consume_row 轮首分隔行分支)。
if UiKit.motion_scale < 1.0:
ui.screens.row_pacer.consume_now({"__round_line": ri})
else:
ui.screens.row_pacer.enqueue({"__round_line": ri})
ui.battle.settlement_fx.play_round_banner(ri)
# 秘宝呈现同步:每战首轮装配链
# 显式重算〔🔴 契约复原语义:下一战装配链重算即含「未装配→写 0」复原〕。
# 挂点时序依据:round_started 首轮发射位在 begin_battle 尾(attunement 注入之后)。
# settlement 空防御照既有形态。未装配=参数 0→写 0=缺省零扰动基线逐位不变。)
if ri == 1:
_sync_treasure_glow()
# 巢穴预告呈现(装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示)
if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0:
ui.battle.board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info")
# 开战横幅——整图地形变异预告:首轮快照 board_variants
# 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。
if ri == 1 and run_scope.settlement != null:
var bvs: Array[Variant] = run_scope.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():
ui.battle.settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines))
_refresh_presentations()
func _on_board_changed() -> void:
# 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步
if run_scope.settlement == null and run_scope.level_active and run_scope.runner != null:
run_scope.settlement = run_scope.runner.current_battle()
if run_scope.settlement == null:
return
var board = run_scope.settlement.minesweeper.current_board()
ui.battle.board_zone.board_view.rebuild(board, run_scope.settlement.minesweeper)
if board != null:
ui.battle.stage.set_board_type(String(board.board_type)) # 舞台背景选图随板(C8——既有重建点单行同步)
_check_board_hints()
_refresh_presentations()
# 秘宝视野呈现桥:阔视野带 light_radius_bonus
# 装配参数合计显式写给 ui.battle.board_zone.board_view.set_fog_glow_bonus(装配=半径 8+4 / 未装配=复原 0),写入即 setter
# 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2:
# 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=缺省基线。
func _sync_treasure_glow() -> void:
if run_scope.settlement == null or ui.battle.board_zone.board_view == null:
return
ui.battle.board_zone.board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus"))
# ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ----
func _check_board_hints() -> void:
var board = run_scope.settlement.minesweeper.current_board()
if board == null:
return
# 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态)
if board.illegal_product:
ui.battle.board_zone.toast.show_toast("本板为零合法板降级产物——出图器已尽力,数据面诚实告知", "warn")
# 接受最优降级(有合法板但无一达标)——降级标记独立于非法产物标记,同走诚实告知
elif board.degraded_product:
ui.battle.board_zone.toast.show_toast("本板为接受最优降级产物(未达可推理率及格线)——数据面诚实告知", "warn")
func _on_hover_forbidden(_cell: Vector2i) -> void:
if not run_scope.hints_shown.has("no_adjacent"):
_show_hint("需先走近(点击已开格移动,免费)", "no_adjacent")
func _on_chord_attempted(_cell: Vector2i) -> void:
if not run_scope.hints_shown.has("chord"):
_show_hint("本作无双击快开——封印后轮末自动判定标中;点数字格可移动小人", "chord")
func _show_hint(text: String, hint_id: String) -> void:
run_scope.hints_shown[hint_id] = true
logger.log_ux("ux_hint_shown", {"hint_id": hint_id})
ui.battle.board_zone.toast.show_toast(text, "info")
# ---- 节点覆盖层"继续"(奖励/恢复点兑付确认)与节点图选路覆盖层 ----
func _on_node_continue() -> void:
MainAdventureFlow._on_node_continue(self)
func _on_choices_presented(candidates: Array)-> void:
MainAdventureFlow._on_choices_presented(self, candidates)
func _on_route_chosen(node_id: int)-> void:
MainAdventureFlow._on_route_chosen(self, node_id)
# ---- 独立屏屏级互斥(单一出口:进屏隐战斗屏 / 出屏显战斗屏)----
# 兑付覆盖层(节点/商店)在本屏激活域内弹出期间保持隐藏态不变——模态栈打开低层时隐藏本屏、
# 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。
func _enter_level_screen() -> void:
MainAdventureFlow._enter_level_screen(self)
func _exit_level_screen() -> void:
MainAdventureFlow._exit_level_screen(self)
func _on_abandon_requested() -> void:
MainAdventureFlow._on_abandon_requested(self)
# ---- 最小设置面(入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)----
func _open_settings() -> void:
MainOverlayFlow._open_settings(self)
func _on_settings_closed() -> void:
MainOverlayFlow._on_settings_closed(self)
func _status_data() -> Dictionary:
return MainOverlayFlow._status_data(self)
func _open_status_panel() -> void:
MainOverlayFlow._open_status_panel(self)
func _on_status_closed() -> void:
MainOverlayFlow._on_status_closed(self)
# ---- 生涯页·只读(入口=开始层生涯钮;开合仿设置面先例:开始层互斥;
# 数据快照随开随建零写通路,页内零跳转)----
func _open_career() -> void:
MainOverlayFlow._open_career(self)
func _on_career_closed() -> void:
MainOverlayFlow._on_career_closed(self)
func _open_save_select() -> void:
MainOverlayFlow._open_save_select(self)
func _on_save_select_closed() -> void:
MainOverlayFlow._on_save_select_closed(self)
func _on_save_select_new(slot: int)-> void:
MainOverlayFlow._on_save_select_new(self, slot)
func _on_save_select_load(slot: int)-> void:
MainOverlayFlow._on_save_select_load(self, slot)
func _on_save_select_delete(slot: int)-> void:
MainOverlayFlow._on_save_select_delete(self, slot)
func _on_status_badge_pressed() -> void:
MainOverlayFlow._on_status_badge_pressed(self)
func _sync_status_entry() -> void:
MainOverlayFlow._sync_status_entry(self)
# ---- 探针缝保缝(click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd / main_overlay_flow.gd)----
func _on_camp_closed() -> void:
MainOverlayFlow._on_camp_closed(self)
func _on_camp_object_pressed(key: StringName) -> void:
MainOverlayFlow._on_camp_object_pressed(self, key) # 场景点选路由(卡④ T3——薄委托,实现归覆盖层流程承接件)
# Esc 输入入口(产品码首个键盘入口——卡⑤ T1 契约 9 接线定稿:事件驱动 _unhandled_input、
# KEY_ESCAPE 键值判定配 physical_keycode、pressed 且非 echo、禁 Input 单例轮询、不挂 ui_cancel)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ESCAPE or event.physical_keycode == KEY_ESCAPE):
MainEscFlow.on_esc_input(self)
func _on_esc_return() -> void:
MainEscFlow._on_return_to_menu(self)
func _on_esc_settings() -> void:
MainEscFlow._on_settings(self)
func _on_esc_quit() -> void:
MainEscFlow._on_quit(self)
func _camp_data() -> Dictionary:
return MainOverlayFlow._camp_data(self)
func _on_wallet_changed(event: String, gp: int, extra: Dictionary)-> void:
MainOverlayFlow._on_wallet_changed(self, event, gp, extra)
# ==================== 玩家意图 → 核心写调用(唯一出口)====================
func _on_board_action(cell: Vector2i, kind: String) -> void:
# 玩家动作 → 核心写调用的唯一通道(模态/无战斗拒绝在此收口)
if ui.flow.locks_input():
return
# 结算演播期棋盘冻结(v3 口径——不可点/不可移/不可挖):
# 锁面=排播队列活跃(纯队列判据;排播泵尾迹窗口不可作演播中标志——见 row_pacer._process 注)。
# 队列排空即解锁、无状态残留;HUD 按钮不在锁面(停手/明细回看各自通路,开启前
# 开层前排空钩子(flow.flush_rows——模态开启前明细带恒完整)。
# **点击不丢弃**(v3.1——「卡死」体感根因):锁期点击缓存最后一次,演播完边沿
# (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。
if not ui.screens.row_pacer.pending_rows().is_empty():
run_scope.locked_click = {"cell": cell, "kind": kind}
return
# 秘宝选中态劫持(两步交互第二步):点格=使用目标拾取,不落挖格;
# 只劫持挖掘点击(move/seal 语义不冲突——选中期玩家仍可移动/封印)
var treasure_sel: int = _treasure_ui.active_selection()
if treasure_sel != 0 and kind == "dig":
_treasure_ui.attempt_use(treasure_sel, cell)
return
if run_scope.settlement == null:
return # 入口显式空防护(双保险——锁谓词正常态已挡,防锁判据异常时崩于空引用)
var outcomes: Array[Variant] = run_scope.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 run_scope.runner != null and run_scope.runner.treasure_inventory != null:
_treasure_ui.refresh_slots() # 授予发生在 settle_batch 结算段内(同步走完,行尾重查)
_handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {})
# settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被
# runner 置空,清图演出判定须先判 settlement(重入隐患守卫)
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared():
ui.battle.board_zone.fx.play_clear_burst()
if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null:
# 挖掘演出:挥铲三帧 + 角色弹跳——触发=格子点开那一刻
#(settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态;
# 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位)
var hero_cell: Vector2i = run_scope.settlement.minesweeper.hero_pos()
ui.battle.board_zone.fx.play_shovel_swing(ui.battle.board_zone.board_view.hero_hand_point(), cell - hero_cell, ui.battle.board_zone.board_view.hero_sprite_scale())
ui.battle.board_zone.board_view.play_hero_dig_bounce()
elif accepted and kind == "move":
ui.battle.board_zone.board_view.notify_hero_move() # 原地移动(目标=当前格幂等受理)帧闪回执——refresh diff 前置告知
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
func _on_stop_pressed() -> void:
if ui.flow.locks_input():
return
# stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济)
if run_scope.settlement == null:
return # 入口显式空防护(同棋盘动作入口——零成本双保险)
var outcomes: Array[Variant] = run_scope.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:防静默改判读感欺骗)
ui.battle.board_zone.toast.show_toast("本轮已清图,停手未执行 · 剩余 AP 已作废", "warn")
logger.log_ux("ux_reject", {"source": "voided", "sub_reason": "stop_voided"})
ui.battle.board_zone.board_view.refresh()
_refresh_presentations()
func _on_refresh_confirmed() -> void:
# 弃图刷新(挖掘期 Active 当场换图继续挖——D4;RAP 按门槛扣费 · 旧板封印作废,每轮上限读表)
if ui.flow.locks_input():
return
var ok: bool = false
if run_scope.level_active and run_scope.runner != null:
ok = run_scope.runner.shop.player_request_refresh()
elif run_scope.settlement != null:
ok = run_scope.settlement.player_request_refresh()
logger.log_action("refresh", Vector2i(-1, -1), ok)
if ok:
logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()})
ui.battle.board_zone.toast.show_toast("RAP−%d · 旧板封印作废——新图已就位,AP 保留继续挖" % int(db.run.action_economy.rap_refresh_cost), "gold")
else:
# 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断)
var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0
var cost: int = int(db.run.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_snapshot() + 1})
if result == "rejected_rap":
ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
ui.battle.board_zone.ap_bar.pulse_rap()
else:
ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
ui.battle.board_zone.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", ""))})
ui.battle.board_zone.toast.show_toast(HudPresenter.outcome_text(outcome), "warn")
if String(outcome.get("reject_reason", "")) == "resource_face":
ui.battle.board_zone.ap_bar.pulse_ap()
# ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)====================
# 本轮已受理刷新次数只读面(刷新双账收口:
# 单一事实源=战斗编排器 _refresh_used_this_round,经 presentation_snapshot 成文面读取)
func _refresh_used_snapshot() -> int:
if run_scope == null or run_scope.settlement == null:
return 0
return int(run_scope.settlement.presentation_snapshot().get("refresh_used", 0))
func _refresh_presentations() -> void:
# 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查)
var refresh_remaining: int = maxi(0, int(db.run.action_economy.refresh_cap_per_round) - _refresh_used_snapshot())
HudPresenter.refresh(ui.battle.stage, ui.battle.info_panel, ui.battle.board_zone.ap_bar, {"settlement": run_scope.settlement, "runner": run_scope.runner, "level_active": run_scope.level_active, "db": db, "refresh_remaining": refresh_remaining})
_treasure_ui.refresh_slots() # C 区秘宝槽重查(试炼镐积点进度随挖格刷新悬停浮窗)
_sync_settlement_visuals()
func _sync_settlement_visuals() -> void:
# 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开(高度读 battle_layout)
var settling := _is_settling()
if settling and not run_scope.was_settling:
ui.battle.settlement_fx.play_round_sweep(ui.battle.board_zone.board_view.get_global_rect()) # 结算轮光带——时序敏感演出触发,例外留在组合根
run_scope.was_settling = settling
HudPresenter.sync_settlement_visuals(ui.battle.stage, ui.battle.board_zone.board_view, ui.battle.log_view, settling,
HudPresenter.phase_text(run_scope.settlement, run_scope.runner, run_scope.boss_active, db))
func _is_settling() -> bool:
# 结算视觉期跟随排播演播到放完。核心层同步走完(轮末结算后逻辑秒回 Active),
# 判据若只跟逻辑态,演播期棋盘会提前恢复可挖观感=「没意识到在结算演出」感知缺口根源;
# 演播期维持去饱和+禁挖光(结算中语义),放完自然回挖掘认知。判据=纯队列(排播泵尾迹
# 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外:
# overlay 开启经 flow.flush_rows 钩子排空,视觉随队列空同步交接结算框。
# 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透)。
if run_scope.settlement == null or not run_scope.settlement.is_battle_live():
return not ui.screens.row_pacer.pending_rows().is_empty()
if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING:
return true
return not ui.screens.row_pacer.pending_rows().is_empty()
# 相位行 —— 承接体见 HudPresenter.phase_text(caller 直接改引)
# ==================== B 区明细带 / 图例(模态入口)====================
func _on_legend_toggled() -> void:
MainOverlayFlow._on_legend_toggled(self)
# ==================== 诊断转储(卡死时点击——全量核心状态 + 环形缓冲)====================
func _on_diagnose_pressed() -> void:
MainDebugFlow._on_diagnose_pressed(self)
# ==================== 调试浮窗(开合/应用/模拟编排由组合根承接)====================
func _show_debug_overlay() -> void:
MainDebugFlow._show_debug_overlay(self)
func _on_debug_badge_pressed() -> void:
MainDebugFlow._on_debug_badge_pressed(self)
func _sync_debug_overlay() -> void:
MainDebugFlow._sync_debug_overlay(self)
func _debug_run_data() -> Dictionary:
return MainDebugFlow._debug_run_data(self)
# 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值;
# 调试两层=RNG 服务 master_seed。禁 _seed_value() 重算冒充本局种子。
func _current_master_seed() -> int:
if run_scope.adventure != null:
return int(run_scope.adventure.master_seed)
return int(get_node(AUTOLOAD_PATH_RNG).master_seed)
func _close_debug_overlay() -> void:
MainDebugFlow._close_debug_overlay(self)
func _on_tune_apply(atk_pct: int, hp_pct: int)-> void:
MainDebugFlow._on_tune_apply(self, atk_pct, hp_pct)
func _on_tune_simulate(n: int)-> void:
MainDebugFlow._on_tune_simulate(self, n)
# ==================== 工具 ====================
func _seed_value() -> int:
# 种子三分判定单点(全工程唯一种子框读取处,禁第二处读框):
# ①显式合法整数全场景优先(含首局——测试/调试接口显式指定;第五路 DebugSession Callable
# 经本函数=调试对局与玩家同口径);
# ②首局固定 42(存档现役布尔 is_first_adventure_done:新档显式 false=教学局恒 42〔教学
# 重放模板 0 即在此种子下标定〕;置位 true=教学局已收束的非首局走 ③ 随机);
# ③非首局空/非法框=引擎全局熵 randi()(Godot 4 启动自播种子,headless 同;不走 rng_service
# 封闭流、禁 randomize()/自建 RNG——组合根建种子属引擎熵豁免面,不进玩法 RNG 消费)
if ui.modals.menu.debug_overlay.seed_edit.text.is_valid_int():
return int(ui.modals.menu.debug_overlay.seed_edit.text)
if save_manager.meta_state.career_state().first_adventure_done:
return randi() # rng-seed-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免)
return 42