772 lines
39 KiB
GDScript
772 lines
39 KiB
GDScript
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||
## 组合根:唯一的模式流转与 core↔view 接线处(垂直切片验证:线性第一关承载元进度循环)。
|
||
## db.hero 运行期只读,加成经 runner.set_hero_override / debug_session 生效值入战斗。
|
||
## 禁链式穿透 settlement.*(相位读取只走 presentation_snapshot);模态开合只走 ui.modals.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.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.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
|
||
match _confirm_consumer:
|
||
"delete_slot":
|
||
save_manager.slots.delete_slot(_confirm_consumer_slot)
|
||
ui.modals.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.modals.flow.close(ui.modals.confirm_dialog) # 模态栈单口
|
||
match _confirm_consumer:
|
||
"gate":
|
||
_camp_pres.on_camp_closed()
|
||
_start_adventure()
|
||
_confirm_consumer = ""
|
||
|
||
|
||
func _on_confirm_declined() -> void:
|
||
ui.modals.flow.close(ui.modals.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.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(续玩同口径)
|
||
ui.screens.start_overlay.visible = false
|
||
_begin_session(true)
|
||
_wire_adventure()
|
||
run_scope.level_active = true
|
||
_sync_status_entry() # 续玩恢复点亮状态面板入口角标
|
||
ui.modals.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.debug_overlay.atk_pct(), ui.modals.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.modals.flow.close(ui.modals.debug_overlay) # 开冒险路径显式关浮窗(调试浮窗默认开态下开局不带窗;对局中经角标再开)
|
||
_begin_session(true)
|
||
_wire_adventure()
|
||
run_scope.level_active = true
|
||
_sync_status_entry() # 进对局点亮状态面板入口角标
|
||
ui.modals.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.debug_overlay.atk_pct(), ui.modals.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.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.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.end_overlay.open_defeat(Narrator.battle_end_text("defeat", false),
|
||
"返回", ui.battle.board_zone.board_view.get_global_rect())
|
||
ui.modals.flow.open_after_playback(ui.modals.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.modals.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.modals.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.modals.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.debug_overlay.seed_edit.text.is_valid_int():
|
||
return int(ui.modals.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
|