diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 449e156b..9f955482 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -14,18 +14,29 @@ # 参数面显式传参)。 # 纪律:数值零字面量——价格/档位/阈值/两档金额只读 db.meta_* 访问器;存档只经 SaveManager 访问器, # 禁读内部结构;行集解析禁自扫表(一律走 ConfigDB 访问器)。 -class_name MetaProgression -extends RefCounted +class_name MetaProgressionCore +extends Node var db: ConfigDb -var save: SaveManager +var save: SaveManagerCore -func _init(db_ref: ConfigDb, save_ref: SaveManager) -> void: +# 迭代 27 T1 裁点①——MetaProgression 升格 autoload(RngService 先例:类名加 Core 后缀、 +# autoload 占净名)。_init 参数可缺省兼容双形态:autoload 实例化走 .new() 零参 + _ready 从 +# autoload 链取依赖(工程序 ConfigDB→RngService→SaveManager→MetaProgression,_ready 时序 +# 已就绪);单测/工具直呼 .new(db, save) 形态不变(不入树零 _ready)。 +func _init(db_ref: ConfigDb = null, save_ref: SaveManagerCore = null) -> void: db = db_ref save = save_ref +func _ready() -> void: + if db == null: + db = get_node("/root/ConfigDB") + if save == null: + save = get_node("/root/SaveManager") + + func keystone_balance() -> int: return save.keystone_balance() diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 968b4a39..a26635a2 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -47,8 +47,8 @@ # # JSON 键纪律:字典键一律 String——JSON 键非字符串的坑,int 键(关卡序号)读写两侧都要 str() 换算, # 混用必出静默 miss。 -class_name SaveManager -extends RefCounted +class_name SaveManagerCore +extends Node const SAVE_FORMAT_VERSION := 7 # registry schema_version:v7=锻造工坊域(迭代 23 T5——meta 新域 forge_routes〔六路线等级 levels+直购解锁清单 unlocked+当前武器/甲选中标记 weapon/armor;无特效域——裁 6 满级特效本期不做〕,旧档缺键由 _normalize 按默认补齐=初始持有态〔sword/light 在档余四锁、选中=初始路线〕,v6→v7 空迁移=缺键补齐模式——v3→v4 先例);v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;需求卡片「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) @@ -74,6 +74,22 @@ func _init(path: String = "user://save.json") -> void: register_migration(6, _migrate_v6_v7) +# autoload 生命周期(迭代 27 T1 裁点①——SaveManager 升格跨场景持有者,RngService 先例: +# 类名加 Core 后缀、autoload 占净名;读盘挂 _ready=autoload 先于主场景就绪,时序与旧 +# main._ready 读盘等价)。独立 .new() 实例(单测/工具)不入树零 _ready,读盘仍由调用方显式触发。 +func _ready() -> void: + load_from_disk() + + +# 原地重置(迭代 27 T1——探针隔离面,沿 RngService.set_master_seed 单例原地重置先例: +# 换路径+回默认数据+重读盘;对象引用恒定,MetaProgression 持有的 save 引用零失效) +func attach_path(path: String) -> void: + _path = path + _read_only = false + _data = _default_data() + load_from_disk() + + # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) func _migrate_v1_v2(data: Dictionary) -> Dictionary: if not data.has("breakpoint"): diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 5a0c2d7e..00f45356 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -6,7 +6,8 @@ # current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离 # debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。 # 组合根(composition root):唯一的模式流转与 core↔view 接线处。 -# 职责边界:① 构建四区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线) +# 职责边界:① 构建三屏场景并接线信号(屏壳场景 menu_screen/camp_screen/adventure_screen——迭代 27 T1 +# 场景拆分波,视图构建仍归 ui_assembler.gd——规范 1.1,本类只接线;模态层仍归本类程序化装配) # ② 模式管理(正式冒险 AdventureRunner——唯一试玩入口;调试两层编排归 debug_session.gd;单场原语仅探针使用) # ③ 核心写调用只在此处发生(minesweeper.settle_batch / 刷新编排入口 / runner 推进; # E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable @@ -29,6 +30,16 @@ # settlement.action_economy / settlement.db 链式穿透。 # ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于 # 驱动视图状态;信号连接只在组合根(row_pacer/treasure_interact_controller 零连接,只收注入)。 +# 迭代 27 T1(场景拆分+局生命周期物化波——审计 P0-1/P0-2): +# · 三屏场景化——menu_screen/camp_screen/adventure_screen .tscn 屏壳(视图代码原样构建, +# 装配器/视图类零 .tscn 化),本类保留全部接线句柄(ADR-0001 组合根职责),_build_ui 解体为 +# 「三屏实例化 + 模态装配 + 接线」;树序 = [菜单屏, 冒险屏, 模态层...]——屏内两容器无同时可见态, +# 全部模态仍在屏壳之上(视觉共存对逐对验算零漂移——实现报告留痕)。 +# · 会话物化——run_scope.gd 节点持全部局态(adventure/runner/settlement/局态旗/账本), +# _begin_session 创建、会话更替/终局整节点 queue_free(),旧手工复位清单 +# (_reset_run_scope 三项 + _on_battle_end_continue 置空四件)消亡。 +# · SaveManager/MetaProgression 升格 autoload(裁点①甲——RngService 先例:类名加 Core 后缀、 +# autoload 占净名),存档读盘挂 autoload _ready(时序与旧 main._ready 读盘等价)。 extends Control const UiAssembler := preload("res://presentation/ui_assembler.gd") @@ -38,51 +49,43 @@ const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批) const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互(T3 拆件批) -const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批) +const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批;T3 收口波将退役) const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互(T4/D4 拆件批) const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影(T4/D4 拆件批) -const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) -const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律) +const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景(迭代 27 T1) +const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") +const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") var db -var settlement = null # 当前战斗编排器(关卡与调试模式统一读它) -var runner = null # 关卡推进器(冒险模式每关重取 adventure.runner();单关 UI 直入口已退役——LevelRunner 仅存 headless 测试面) -var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接) -var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构) -var meta = null # 元进度账本服务(元进度实现批——余额/定价/购买/工坊解锁/Boss两档入账/注入计算;零信号纯被动) -var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派) -var _debug_entry := false # 调试会话门控(迭代 18 卡⑦——DS_DEBUG=1:启动即开浮窗+两屏角标点亮;_ready 赋值) +var run_scope = null # 会话状态承载(run_scope.gd——迭代 27 T1 物化;_ready 即建闲时态,永非 null) +# SaveManager/MetaProgression 升格 autoload(迭代 27 T1 裁点①)——成员名保留为「autoload 读视图」 +#(属性 getter 经节点路径解析:--script 模式编译器不解析裸 autoload 标识符〔run_all.gd 头注在案〕, +# 节点路径双模式通用;组合根调用点/探针挂载面 main.save_manager 语义零迁移)。 +var save_manager: SaveManagerCore: + get: + return get_node("/root/SaveManager") +var meta: MetaProgressionCore: + get: + return get_node("/root/MetaProgression") var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) -var _level_active := false +var _debug_entry := false # 调试会话门控(迭代 18 卡⑦——DS_DEBUG=1:启动即开浮窗+两屏角标点亮;_ready 赋值) var _seed_edit: LineEdit var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮) var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化) -var _settlement_stop_timer := ArmedTimer.new() # 卡⑥ 结算框挂起计时(T3/D3 P4 乙——裸布尔暂存机制收口;窗口 0=挂起到显式回落) -var _settlement_stop_pending: bool: # 挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm) - get: - return _settlement_stop_timer.is_armed() - set(v): - if v: - _settlement_stop_timer.arm() - else: - _settlement_stop_timer.disarm() -var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷) -var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用) var _start_overlay: Control var _end_overlay: BattleEndOverlay var _inter_overlay: Control var _inter_label: Label -var level_screen: LevelMapScreen # 关卡画面(独立屏——2026-09-28 形态升格批:激活期战斗屏宿主整体隐藏;零自动推进停点语义不变) -var _battle_root: Control # 战斗屏宿主(B/C/D 三区容器——独立屏激活期 visible=false 级互斥) -var _level_screen_active := false # 独立屏激活状态(覆盖兑付覆盖层在屏域内弹出期间——战斗屏保持隐藏) +var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen——迭代 27 T1 场景拆分) +var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root——同上) # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null # 视图(views/)——battle.md 三区(A 区关卡进度条顶栏随 停点前移批 2026-09-23 退役——节点链进度 # 由关卡画面本体选路覆盖层承载;E 区道具面板随可选类删除下线) -var stage: BattleStageView # B 区(含 log_view 明细带) +var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen var log_view: BattleLogView var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区——2026-09-26 信息归属拍板精简) var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) @@ -90,22 +93,28 @@ var board_view: BoardView # 探针兼容别名(= board_zone.bo var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下) +# 三屏壳实例(迭代 27 T1——屏成员经本类句柄暴露,探针挂载假设 main.X 一级路径零迁移) +var _menu_screen = null # MenuScreen(开始菜单屏壳) +var _camp_screen = null # CampScreen(营地屏壳) +var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器) + # T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui) var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图) var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入) var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发) -var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂) -# GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况) +# GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况; +# 账本实例随会话住 run_scope.gp_ledger——迭代 27 T1) var _adventure_gp_view: int: get: - return _gp_ledger.view_total() + return run_scope.gp_ledger.view_total() if run_scope != null and run_scope.gp_ledger != null else 0 set(value): - _gp_ledger.set_view_total(value) + if run_scope != null and run_scope.gp_ledger != null: + run_scope.gp_ledger.set_view_total(value) var _adventure_gp_final: int: get: - return _gp_ledger.final_total() + return run_scope.gp_ledger.final_total() if run_scope != null and run_scope.gp_ledger != null else 0 # 排播队列只读委托面(探针/测试读 main._row_queue——RowPacer 迁移后保留的同名可读面,共享同一 Array 引用) var _row_queue: Array: @@ -116,7 +125,7 @@ var _row_queue: Array: var legend_overlay: LegendOverlay var node_overlay: NodeOverlay var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现) -var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现) +var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay——迭代 27 T1) var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效) var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面) var status_panel: StatusPanel # 局内状态面板·只读(迭代 17 卡③——钥石余额+营地三柜台进度查看;登记设置层同档) @@ -125,13 +134,19 @@ var debug_overlay: DebugOverlay # 调试浮窗(迭代 14.5 调试整合 var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体) var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd) -var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent) -var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零) -var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) -var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批) - -var _locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放,玩家操作零丢失) +# 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住 +# run_scope.settlement_stop_timer——迭代 27 T1) +var _settlement_stop_pending: bool: + get: + return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false + set(v): + if run_scope == null: + return + if v: + run_scope.settlement_stop_timer.arm() + else: + run_scope.settlement_stop_timer.disarm() # 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 #(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, @@ -163,13 +178,24 @@ func _on_pump_ended() -> void: func _replay_locked_click() -> void: - if _locked_click.is_empty() or not _row_queue.is_empty(): + if run_scope.locked_click.is_empty() or not _row_queue.is_empty(): return - var click: Dictionary = _locked_click - _locked_click = {} + var click: Dictionary = run_scope.locked_click + run_scope.locked_click = {} _on_board_action(click["cell"], click["kind"]) +# 会话生命周期(迭代 27 T1——P0-2 物化):_ready 建闲时态(永非 null——全组合根零空判责), +# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。 +func _begin_session(adventure_session: bool) -> void: + if run_scope != null: + run_scope.queue_free() + run_scope = RunScope.new() + run_scope.is_adventure = adventure_session + run_scope.gp_ledger = GpShadowLedgerScript.new() + add_child(run_scope) + + func _ready() -> void: db = get_node("/root/ConfigDB") if not db.is_ready: @@ -177,21 +203,18 @@ func _ready() -> void: if not ok: _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return - # 存档读盘(存档序列化批):拒读/损坏均已在 SaveManager 内部兜底,失败不阻断启动 - # (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。 - save_manager = SaveManagerScript.new() - save_manager.load_from_disk() + # SaveManager/MetaProgression 升格 autoload(迭代 27 T1 裁点①):读盘已在各自 _ready 完成 + #(autoload 先于主场景就绪——时序与旧 main._ready 读盘等价),此处零动作。 # 系统级设置(迭代 8 卡④——独立存储不随存档):启动即恢复音量/全屏/减动效引擎态 settings_store = SettingsStore.new() settings_store.load_settings() settings_store.apply() - # 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点 - meta = MetaProgressionScript.new(db, save_manager) _debug = DebugSessionScript.new(db, get_node("/root/RngService"), Callable(self, "_debug_hero_override"), Callable(self, "_seed_value")) _debug.battle_created.connect(_on_debug_battle_created) _debug.battle_ready.connect(_on_debug_battle_ready) _build_ui() + _begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序) _refresh_resume_button() # 迭代 8 卡②——启动读盘后按断点在档态点亮「继续上局」 _start_overlay.visible = true _debug_entry = OS.get_environment("DS_DEBUG") == "1" # 迭代 18 卡⑦ P3 甲——DS_DEBUG=1 调试会话门控 @@ -199,20 +222,27 @@ func _ready() -> void: _show_debug_overlay() # 启动即开(手动可关、两屏角标可再开——P3 甲) -# ==================== 视图接线(battle.md 四区布局 1280×800;节点构建归 UiAssembler) ==================== +# ==================== 视图接线(三屏壳 + 模态层装配;构建归屏壳/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 + # 三屏壳实例化(迭代 27 T1 场景拆分):树序 = [菜单屏, 冒险屏, 模态层...]——屏壳全在模态之下。 + # 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差; + # 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、 + # debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。 + _menu_screen = MenuScreenScene.instantiate() + add_child(_menu_screen) + _adventure_screen = AdventureScreenScene.instantiate() + add_child(_adventure_screen) + _start_overlay = _menu_screen.start_overlay + _battle_root = _adventure_screen.battle_root + stage = _adventure_screen.stage + log_view = _adventure_screen.log_view + info_panel = _adventure_screen.info_panel + board_zone = _adventure_screen.board_zone + board_view = _adventure_screen.board_view # 探针兼容别名(= board_zone.board_view) + ap_bar = _adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar) + settlement_fx = _adventure_screen.settlement_fx + level_screen = _adventure_screen.level_screen # 结算行排播泵(T3 拆件批——迁 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) _row_pacer = RowPacer.new() @@ -227,7 +257,7 @@ func _build_ui() -> void: # 秘宝两步交互(T3 拆件批——迁 treasure_interact_controller.gd;settlement/runner 随关卡更替, # 经现值回调注入——仿 identity_resolver 只读回调先例) _treasure_ui = TreasureInteractController.new(db, meta, info_panel, board_zone.toast, board_view, - func() -> Variant: return settlement, func() -> Variant: return runner) + func() -> Variant: return run_scope.settlement, func() -> Variant: return run_scope.runner) board_zone.action_requested.connect(_on_board_action) board_zone.status_badge.pressed.connect(_on_status_badge_pressed) # 状态面板入口(迭代 17 卡③——战斗屏 HUD 角标) @@ -249,18 +279,18 @@ func _build_ui() -> void: logger.ap_per_round = int(db.action_economy.ap_per_round) # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 - # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读·迭代 18 卡④) < 确认弹窗 < 调试浮窗) + # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读·迭代 18 卡④) < 确认弹窗 < 调试浮窗; + # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) var overlays: Dictionary = UiAssembler.build_overlays(self, db) legend_overlay = overlays.legend_overlay node_overlay = overlays.node_overlay _end_overlay = overlays.end_overlay shop_overlay = overlays.shop_overlay - camp_overlay = overlays.camp_overlay - level_screen = LevelMapScreen.new() - add_child(level_screen) - # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——关卡画面须夹在节点层与商店层之间 - #(兑付覆盖层在本屏域内弹出:模态栈打开低层时自动隐藏高层本屏、关闭自动恢复——零机制改动复用) - move_child(level_screen, node_overlay.get_index() + 1) + _camp_screen = CampScreenScene.instantiate() + add_child(_camp_screen) + camp_overlay = _camp_screen.camp_overlay + # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——营地层须夹在商店层与设置层之间 + move_child(_camp_screen, shop_overlay.get_index() + 1) legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay)) node_overlay.continue_pressed.connect(_on_node_continue) level_screen.route_chosen.connect(_on_route_chosen) @@ -285,29 +315,28 @@ func _build_ui() -> void: # 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进) curio_overlay = CurioPickOverlay.new() add_child(curio_overlay) + move_child(curio_overlay, _end_overlay.get_index()) # 提升三选一与替换子交互(T4/D4 拆件批——迁 curio_interact_controller.gd;本类只接线与回调注入, # ADR-0001——模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) _bonus = CurioInteractController.new(db, func() -> Variant: return meta, curio_overlay, _end_overlay, board_view, - board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return adventure, - func() -> Variant: return runner, func() -> Variant: return _adventure_mode, + board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return run_scope.adventure, + func() -> Variant: return run_scope.runner, func() -> Variant: return run_scope.is_adventure, _open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations, - _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount})) + _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": run_scope.last_gain_amount})) curio_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed) curio_overlay.continue_pressed.connect(_bonus.on_continue) curio_overlay.refresh_requested.connect(_bonus.on_refresh) - move_child(curio_overlay, _end_overlay.get_index()) - var start: Dictionary = UiAssembler.build_start_overlay(self, db) - _start_overlay = start.overlay - _resume_btn = start.resumebtn - start.lvbtn.pressed.connect(_start_adventure_from_menu) - start.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口 + # 菜单屏接线(原 build_start_overlay 返回值接线段——句柄经屏壳暴露) + _menu_screen.lvbtn.pressed.connect(_start_adventure_from_menu) + _menu_screen.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口 + _resume_btn = _menu_screen.resumebtn # 调试角标入口退役(迭代 18 卡⑦ P2 甲——迁宿主:战斗屏 HUD+关卡地图屏各一枚, # 接线见两屏装配段;开始层不再有调试入口,DS_DEBUG=1 会话=启动即开浮窗) # 营地/商店投影(T4/D4 拆件批——迁 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) _camp_pres = CampShopPresenter.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay, - board_zone.toast, _start_overlay, logger, func() -> Variant: return runner, + board_zone.toast, _start_overlay, logger, func() -> Variant: return run_scope.runner, _open_modal, _close_modal, _refresh_presentations, _refresh_resume_button) shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy) shop_overlay.refresh_requested.connect(_camp_pres.on_shop_refresh) # MC-1 @@ -320,10 +349,10 @@ func _build_ui() -> void: camp_overlay.forge_unlock_requested.connect(_camp_pres.on_camp_forge_unlock) camp_overlay.forge_select_requested.connect(_camp_pres.on_camp_forge_select) camp_overlay.closed.connect(_camp_pres.on_camp_closed) - start.campbtn.pressed.connect(_camp_pres.open_camp) - start.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组) - start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 - start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 + _menu_screen.campbtn.pressed.connect(_camp_pres.open_camp) + _menu_screen.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组) + _menu_screen.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 + _menu_screen.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 var inter: Dictionary = UiAssembler.build_inter_overlay(self) _inter_overlay = inter.overlay @@ -353,7 +382,7 @@ func _build_ui() -> void: debug_overlay.debug_levels_requested.connect(_start_debug_levels) debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 迭代 26 T5b——诊断转储入口随迁(原角落钮连接) - # 模态登记(缺陷⑤纪律——登记序 = 树序:含 move_child 调位后的关卡画面/三选一终位; + # 模态登记(缺陷⑤纪律——登记序 = 树序:关卡画面住冒险屏壳但登记位仍夹在节点层与三选一层之间; # 状态面板设置层同档、确认弹窗之前——P2 甲;生涯页同档随状态面板之后(迭代 18 卡④ P3 甲)) _modals = ModalStack.new() for m: Control in [legend_overlay, node_overlay, level_screen, curio_overlay, _end_overlay, shop_overlay, camp_overlay, settings_overlay, status_panel, career_overlay, confirm_dialog, debug_overlay]: @@ -406,7 +435,7 @@ func _input_locked() -> bool: # 不再手工罗列各层;例外三件:开始层/过场层(非模态全屏态,不在登记册)与战斗缺位(棋盘输入无载体)) # 迭代 18 卡⑦ T2 防护(审计🔴):判据改排除式——调试浮窗对局中可开(P2 甲),其开态不计入锁 #(否则 DS_DEBUG 默认开×他层关闭复活浮窗×has_open 组合=棋盘永久锁死——S1+ 停机点防线) - return _modals.has_open_excluding(debug_overlay) or _start_overlay.visible or _inter_overlay.visible or settlement == null + return _modals.has_open_excluding(debug_overlay) or _start_overlay.visible or _inter_overlay.visible or run_scope.settlement == null # ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)==================== @@ -414,7 +443,7 @@ func _input_locked() -> bool: func _start_adventure_from_menu() -> void: # 互斥确认弹窗化(二轮修改批 A——用户目检裁决「弹窗提示」):有断点时开新冒险弹实底确认框, # 「开新冒险」提交(弃旧断点+写盘)/「取消」回菜单 - if save_manager != null and save_manager.has_breakpoint(): + if save_manager.has_breakpoint(): var bp: Dictionary = save_manager.breakpoint_snapshot() confirm_dialog.open("已有未完冒险(第 %d 关)——开新冒险将覆盖旧断点" % maxi(1, int(bp.get("level_index", 1))), "开新冒险", "取消") @@ -425,9 +454,8 @@ func _start_adventure_from_menu() -> void: func _on_confirm_new_adventure() -> void: confirm_dialog.close() - if save_manager != null: - save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) - save_manager.write_to_disk() + save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) + save_manager.write_to_disk() _start_overlay.visible = false _start_adventure() @@ -445,7 +473,7 @@ func _quit_game() -> void: # 继续上局按钮可见性 = 断点在档态(迭代 8 卡②——启动读盘后与每次回开始菜单刷新) func _refresh_resume_button() -> void: - if _resume_btn == null or save_manager == null: + if _resume_btn == null: return var has: bool = save_manager.has_breakpoint() _resume_btn.visible = has @@ -456,21 +484,21 @@ func _refresh_resume_button() -> void: # 继续上局(迭代 8 卡②——卡① resume_adventure 的消费入口):断点重建至最近停点 func _resume_from_menu() -> void: - if save_manager == null or not save_manager.has_breakpoint(): + if not save_manager.has_breakpoint(): return var bp: Dictionary = save_manager.breakpoint_snapshot() _close_modal(debug_overlay) # 开冒险路径显式关浮窗(迭代 18 卡⑦ T2 防护——续玩同口径) _start_overlay.visible = false + _begin_session(true) _wire_adventure() - _level_active = true + run_scope.level_active = true _sync_status_entry() # 迭代 17 卡③——续玩恢复点亮状态面板入口角标 level_screen.set_abandon_available(true) - _reset_run_scope() log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) - adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 迭代 19 卡⓪①——同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径) - if not adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, + if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, {"treasure_unlocked": _treasure_ui.unlocked_set()})): _fatal_screen("续玩恢复失败——见输出日志") return @@ -478,20 +506,19 @@ func _resume_from_menu() -> void: # 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根,ADR-0001;断点注入同口) func _wire_adventure() -> void: - adventure = AdventureRunner.new(db, get_node("/root/RngService")) - _adventure_mode = true - adventure.step_row.connect(_on_step_row) - adventure.round_started.connect(_on_round_started) - adventure.board_changed.connect(_on_board_changed) - adventure.node_entered.connect(_on_runner_node) - adventure.choices_presented.connect(_on_choices_presented) - adventure.wallet_changed.connect(_on_wallet_changed) + run_scope.adventure = AdventureRunner.new(db, get_node("/root/RngService")) + run_scope.adventure.step_row.connect(_on_step_row) + run_scope.adventure.round_started.connect(_on_round_started) + run_scope.adventure.board_changed.connect(_on_board_changed) + run_scope.adventure.node_entered.connect(_on_runner_node) + run_scope.adventure.choices_presented.connect(_on_choices_presented) + run_scope.adventure.wallet_changed.connect(_on_wallet_changed) # 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线 - adventure.curio_choice_presented.connect(_bonus.on_presented) - adventure.curio_resolved.connect(_bonus.on_resolved) - adventure.adventure_finished.connect(_on_adventure_finished) - adventure.level_started.connect(_on_adventure_level_started) - adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) + run_scope.adventure.curio_choice_presented.connect(_bonus.on_presented) + run_scope.adventure.curio_resolved.connect(_bonus.on_resolved) + run_scope.adventure.adventure_finished.connect(_on_adventure_finished) + run_scope.adventure.level_started.connect(_on_adventure_level_started) + run_scope.adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) # 冒险开局参数面构造(主菜单开新/续玩两入口共用——迭代 19 卡⓪①:同调三键随开局下传,真实局此前恒空。 @@ -519,20 +546,20 @@ static func build_adventure_params(save_manager_ref, meta_ref, extra: Dictionary # 信号连接只在组合根——ADR-0001) func _start_adventure() -> void: _close_modal(debug_overlay) # 开冒险路径显式关浮窗(迭代 18 卡⑦ T2 防护——DS_DEBUG 默认开态下开局不带窗;对局中经角标再开) + _begin_session(true) _wire_adventure() - _level_active = true + run_scope.level_active = true _sync_status_entry() # 迭代 17 卡③——进对局点亮状态面板入口角标 level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板) - _reset_run_scope() log_view.clear() var seed_val := _seed_value() logger.start_session(seed_val) - adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签 # 迭代 19 卡⓪①——同调三键随开局下传(build_adventure_params 统一构造,与营地投影同源) var first_script: bool = not save_manager.is_first_adventure_done() - if not adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, + if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, {"first_script": first_script, "treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})): _fatal_screen("冒险启动失败——见输出日志") return @@ -541,35 +568,28 @@ func _start_adventure() -> void: # 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达; # 停点前移批 2026-09-23:本回调返回后才有 choices_presented 到达——入口选路层随即弹出) func _on_adventure_level_started(level_index: int, display_name: String) -> void: - runner = adventure.runner() + run_scope.runner = run_scope.adventure.runner() # 迭代 19 卡⓪②——开场行改传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源: # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 # 后缀判断同步改认局外加成——永久强化 atk/max_hp flat 或调试浮窗 pct 任一存在才带,白板局不带 - var ov: Dictionary = meta.hero_override() if meta != null else {} - log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed), - runner.effective_atk(), runner.effective_max_hp(), + var ov: Dictionary = meta.hero_override() + log_view.append_bb(Narrator.level_open_line(display_name, int(run_scope.adventure.master_seed), + run_scope.runner.effective_atk(), run_scope.runner.effective_max_hp(), int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 or debug_overlay.atk_pct() > 0 or debug_overlay.hp_pct() > 0)) -func _reset_run_scope() -> void: - # 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号) - _hints_shown = {} - _refresh_used_round = 0 - _boss_active = false - - func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 - if _adventure_mode and adventure != null: - runner = adventure.runner() + if run_scope.is_adventure and run_scope.adventure != null: + run_scope.runner = run_scope.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" + run_scope.settlement = run_scope.runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) + logger.attach_battle(run_scope.settlement) + run_scope.boss_active = t == "boss" _show_board_fx_for_node(t) - log_view.append_bb(Narrator.node_banner(runner, node, t)) + log_view.append_bb(Narrator.node_banner(run_scope.runner, node, t)) elif t == "reward": # 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 # T4/D4 拆件:呈现编排归 curio_interact_controller.present_node_curio(_curio_context 随迁)。 @@ -585,15 +605,15 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡(迭代 11 第②批 T4);legacy 面 = 固定三件(off 对照承载) _open_modal(shop_overlay) - if runner.is_shop_pool(): - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行 - shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) # MC-1 + if run_scope.runner.is_shop_pool(): + shop_overlay.open_shelf(run_scope.runner.shop_shelf(), int(run_scope.runner.gp)) + shop_overlay.set_visit_info(int(run_scope.runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行 + shop_overlay.set_refresh_display(run_scope.runner.shop_refresh_free_left(), run_scope.runner.shop_refresh_cost(), int(run_scope.runner.gp) >= run_scope.runner.shop_refresh_cost()) # MC-1 else: - shop_overlay.open_shop(runner.shop_products(), int(runner.gp)) + shop_overlay.open_shop(run_scope.runner.shop_products(), int(run_scope.runner.gp)) shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) - log_view.append_bb(Narrator.shop_line(int(runner.gp))) - logger.log_node(node, info, runner.node_index) + log_view.append_bb(Narrator.shop_line(int(run_scope.runner.gp))) + logger.log_node(node, info, run_scope.runner.node_index) _refresh_presentations() @@ -601,9 +621,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 func _on_adventure_finished(summary: Dictionary) -> void: - _level_active = false + run_scope.level_active = false var result: String = String(summary.get("result", "")) - settlement = null + run_scope.settlement = null _exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 _sync_status_entry() # 迭代 17 卡③——离局熄灭状态面板入口角标(防菜单误现) _sync_settlement_visuals() # → 全关 @@ -611,8 +631,7 @@ func _on_adventure_finished(summary: Dictionary) -> void: # 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary[String, Variant] = {} - if meta != null: - detail.assign(meta.settle_adventure(summary)) + detail.assign(meta.settle_adventure(summary)) var text := Narrator.adventure_summary_text(summary) if not detail.is_empty(): text += "\n" + Narrator.keystone_settle_text(detail, _bonus.item_name_lookup()) @@ -622,8 +641,8 @@ func _on_adventure_finished(summary: Dictionary) -> void: else: _end_overlay.open_defeat(text, "返回", board_view.get_global_rect()) logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) - if runner != null: - runner.restore_hero_base() + if run_scope.runner != null: + run_scope.runner.restore_hero_base() _open_modal_after_playback(_end_overlay) _refresh_presentations() @@ -639,18 +658,15 @@ func _on_battle_end_continue() -> void: if _settlement_stop_pending: _settlement_stop_pending = false _enter_level_screen() - level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, runner.feats_held()))) - level_screen.open_choices(_pending_candidates, runner) - _pending_candidates = [] + level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()))) + level_screen.open_choices(run_scope.pending_candidates, run_scope.runner) + run_scope.pending_candidates = [] _refresh_presentations() return + # 会话收束(迭代 27 T1 物化):旧手工置空清单(runner/settlement/adventure/_adventure_mode + + # _reset_run_scope 三项)整节点 queue_free() 消亡——新闲时会话即建,局态旗/账本/模态挂起全复位 _debug.reset() - if runner != null: - runner.restore_hero_base() - runner = null - settlement = null - adventure = null - _adventure_mode = false + _begin_session(false) debug_overlay.reset_sliders() _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新(收束后已清=隐藏) @@ -660,14 +676,14 @@ func _on_battle_end_continue() -> void: func _start_debug_levels() -> void: # 守卫(迭代 18 卡⑦ T2——浮窗对局中可开后,调试对局与活跃冒险/结算并存不开放:并发会清写 # settlement/编排态=状态机破坏;随开随收逻辑改向——对局中开窗只读浮窗,不再视为开局前置) - if _adventure_mode or settlement != null: + if run_scope.is_adventure or run_scope.settlement != null: return debug_overlay.reset_sliders() _close_modal(debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) _start_overlay.visible = false log_view.clear() log_view.append_bb(Narrator.debug_open_line(_seed_value(), 1, true)) - _reset_run_scope() + _begin_session(false) if not _debug.start_debug_levels(): _fatal_screen("战斗启动失败——见输出日志") @@ -675,22 +691,22 @@ func _start_debug_levels() -> void: 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 + run_scope.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) + run_scope.settlement = settlement_ref + board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) _check_board_hints() _refresh_presentations() func _on_inter_next_pressed() -> void: _inter_overlay.visible = false - if _adventure_mode: + if run_scope.is_adventure: # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) - if adventure != null and not adventure.continue_to_next_level(): + if run_scope.adventure != null and not run_scope.adventure.continue_to_next_level(): _fatal_screen("关卡推进失败——见输出日志") return if not _debug.next_layer(): @@ -708,20 +724,20 @@ func _debug_hero_override() -> Dictionary: # 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) func _identity_lookup(c: Vector2i): - if settlement == null or settlement.minesweeper.current_board() == null: + if run_scope.settlement == null or run_scope.settlement.minesweeper.current_board() == null: return null - if String(settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: + if String(run_scope.settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: return null - return settlement.query_reward_identity(c) + return run_scope.settlement.query_reward_identity(c) # 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动。 # 变异表直读本根 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession # 构造传参),免 settlement 内部链式穿透,T3 收口批) func _variant_mask_lookup(c: Vector2i) -> bool: - if settlement == null: + if run_scope.settlement == null: return false - var b = settlement.minesweeper.current_board() + var b = run_scope.settlement.minesweeper.current_board() if b == null: return false for vid in b.variant_ids(c): @@ -734,9 +750,9 @@ func _variant_mask_lookup(c: Vector2i) -> bool: # 地形变异 角标解析(迭代 6 T6——伤害型变异未开格视觉标记;读取方按格子变异数据行动,规则本体零特判。 # 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络。变异表直读本根 db——同上款收口) func _variant_mark_lookup(c: Vector2i) -> String: - if settlement == null: + if run_scope.settlement == null: return "" - var b = settlement.minesweeper.current_board() + var b = run_scope.settlement.minesweeper.current_board() if b == null: return "" for vid in b.variant_ids(c): @@ -768,14 +784,14 @@ func _on_battle_finished(payload: Dictionary) -> void: "level_end": _end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % runner.keystone, _bonus_summary_text(), - int(payload.get("gp_dropped", 0)), int(runner.gp)) + "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), + int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) "standalone_end": if result == "victory": _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % runner.keystone, _bonus_summary_text(), - int(payload.get("gp_dropped", 0)), int(runner.gp)) + "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), + int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) @@ -791,17 +807,17 @@ func _on_battle_finished(payload: Dictionary) -> void: # 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照 # 此刻必在档,语义=续玩恢复面的合计即存档合计;与 _bonus_summary_text 同款守卫式) func _gp_summary_text(payload: Dictionary) -> String: - if runner == null: - var bp: Dictionary = save_manager.breakpoint_snapshot() if save_manager != null else {} + if run_scope.runner == null: + var bp: Dictionary = save_manager.breakpoint_snapshot() return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(bp.get("gp", 0))] - return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)] + return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)] # M9 结算三行账·本关加成行装配(呈现只读投影——runner.bonus 统一加算区同源;零加成=行隐藏) func _bonus_summary_text() -> String: - if runner == null: + if run_scope.runner == null: return "" - var b: Dictionary = runner.bonus + var b: Dictionary = run_scope.runner.bonus var parts: Array[String] = [] if int(b.get("atk", 0)) != 0: parts.append("攻 %+d" % int(b.get("atk", 0))) @@ -861,27 +877,27 @@ func _on_round_started(ri: int) -> void: if ri == 1: _sync_treasure_glow() # 巢穴预告呈现(迭代 22 秘宝批 T4——装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示) - if settlement != null and int(settlement.nest_preview_count) >= 0: - board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(settlement.nest_preview_count), "info") + if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0: + board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info") # 开战横幅——板级地形变异预告(全局作用域变异首发批 2026-10-04):首轮快照 board_variants # 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。 - if ri == 1 and settlement != null: - var bvs: Array = settlement.presentation_snapshot().get("board_variants", []) + if ri == 1 and run_scope.settlement != null: + var bvs: Array = 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(): settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines)) - _refresh_used_round = 0 + run_scope.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: + 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 - board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper) + board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) _check_board_hints() _refresh_presentations() @@ -891,15 +907,15 @@ func _on_board_changed() -> void: # 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2: # 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。 func _sync_treasure_glow() -> void: - if settlement == null or board_view == null: + if run_scope.settlement == null or board_view == null: return - board_view.set_fog_glow_bonus(settlement.attune_param_sum("light_radius_bonus")) + 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 = settlement.minesweeper.current_board() + var board = run_scope.settlement.minesweeper.current_board() if board == null: return # 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态) @@ -911,17 +927,17 @@ func _check_board_hints() -> void: func _on_hover_forbidden(_cell: Vector2i) -> void: - if not _hints_shown.has("no_adjacent"): + if not run_scope.hints_shown.has("no_adjacent"): _show_hint("需先走近(点击已开格移动,免费)", "no_adjacent") func _on_chord_attempted(_cell: Vector2i) -> void: - if not _hints_shown.has("chord"): + if not run_scope.hints_shown.has("chord"): _show_hint("本作无双击快开——插旗后轮末自动判定标中;点数字格可移动小人", "chord") func _show_hint(text: String, hint_id: String) -> void: - _hints_shown[hint_id] = true + run_scope.hints_shown[hint_id] = true logger.log_ux("ux_hint_shown", {"hint_id": hint_id}) board_zone.toast.show_toast(text, "info") @@ -930,33 +946,33 @@ func _show_hint(text: String, hint_id: String) -> void: func _on_node_continue() -> void: _close_modal(node_overlay) - if runner != null: + if run_scope.runner != null: # 泛化语义(接口漂移清偿):确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 - runner.continue_after_node() + run_scope.runner.continue_after_node() func _on_choices_presented(candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(候选按钮退役 2026-09-28——战斗胜利/兑付确认统一到达此处) - if runner == null: + if run_scope.runner == null: return - log_view.append_bb(Narrator.route_line(candidates, runner)) + log_view.append_bb(Narrator.route_line(candidates, run_scope.runner)) # 卡⑥ 战斗胜利总结结算(battle.md OQ14 拍板面接线——欠账清偿):战斗胜利停点先弹 BattleEndOverlay # 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点 # 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点 # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 - if _adventure_mode and runner.current_battle() == null \ - and String(runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): + if run_scope.is_adventure and run_scope.runner.current_battle() == null \ + and String(run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): _settlement_stop_pending = true - _pending_candidates = candidates.duplicate() + run_scope.pending_candidates = candidates.duplicate() _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点", board_view.get_global_rect(), - "战利品 GP +%d · 合计 %d" % [_last_gain_amount, int(runner.gp)]) + "战利品 GP +%d · 合计 %d" % [run_scope.last_gain_amount, int(run_scope.runner.gp)]) _open_modal_after_playback(_end_overlay) _refresh_presentations() return _enter_level_screen() - level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, runner.feats_held()))) - level_screen.open_choices(candidates, runner) + level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()))) + level_screen.open_choices(candidates, run_scope.runner) _refresh_presentations() @@ -967,7 +983,7 @@ func _on_route_chosen(node_id: int) -> void: if not level_screen.visible: _close_modal(level_screen) _exit_level_screen() - if runner != null and not runner.pick_next_node(node_id): + if run_scope.runner != null and not run_scope.runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) @@ -976,14 +992,14 @@ func _on_route_chosen(node_id: int) -> void: # 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。 func _enter_level_screen() -> void: - _level_screen_active = true + run_scope.level_screen_active = true _battle_root.visible = false _open_modal(level_screen) _sync_status_entry() # 迭代 17 卡③——屏级互斥切换同步(_open_modal 单口不识屏级状态) func _exit_level_screen() -> void: - _level_screen_active = false + run_scope.level_screen_active = false _battle_root.visible = true if level_screen.visible: _close_modal(level_screen) @@ -994,9 +1010,9 @@ func _on_abandon_requested() -> void: # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——推翻 冒险结构.md「MVP 无界面入口」后置条款; # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 - if not _adventure_mode or adventure == null: + if not run_scope.is_adventure or run_scope.adventure == null: return - if not adventure.abandon(): + if not run_scope.adventure.abandon(): logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") @@ -1097,8 +1113,8 @@ func _sync_status_entry() -> void: if status_panel == null or _modals == null or board_zone == null or level_screen == null: return var map_on: bool = level_screen.visible - var battle_live: bool = _level_active or settlement != null # 调试两层无 _level_active——settlement 判据补位(迭代 18 卡⑦) - board_zone.status_badge.visible = _level_active and not map_on + var battle_live: bool = run_scope.level_active or run_scope.settlement != null # 调试两层无 level_active——settlement 判据补位(迭代 18 卡⑦) + board_zone.status_badge.visible = run_scope.level_active and not map_on level_screen.status_badge.visible = map_on # 置灰判据改排除式(迭代 18 卡⑦ T2——调试浮窗开态不计入:对局中浮窗开着时角标仍可点) board_zone.status_badge.disabled = _modals.has_open_excluding(debug_overlay) @@ -1124,14 +1140,14 @@ func _camp_data() -> Dictionary: func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 logger.log_wallet(event, gp, extra) - _gp_ledger.apply_event(event, gp) # GP 影子账(迭代 6 T7——收束清零前取值,纯呈现零核心触碰=C1;账本归 gp_shadow_ledger.gd) + run_scope.gp_ledger.apply_event(event, gp) # GP 影子账(迭代 6 T7——收束清零前取值,纯呈现零核心触碰=C1;账本住 run_scope——迭代 27 T1) if String(event) == "gain": - _last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) - if shop_overlay.visible and runner != null: - if runner.is_shop_pool(): - shop_overlay.refresh_shelf_state(int(runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) + run_scope.last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) + if shop_overlay.visible and run_scope.runner != null: + if run_scope.runner.is_shop_pool(): + shop_overlay.refresh_shelf_state(int(run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: - shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) + shop_overlay.refresh_state(run_scope.runner.shop_products(), int(run_scope.runner.gp)) _refresh_presentations() @@ -1148,7 +1164,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: # **点击不丢弃**(v3.1——用户实机「卡死」根因):锁期点击缓存最后一次,演播完边沿 # (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。 if not _row_queue.is_empty(): - _locked_click = {"cell": cell, "kind": kind} + run_scope.locked_click = {"cell": cell, "kind": kind} return # 秘宝选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格; # 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗) @@ -1156,21 +1172,21 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: if treasure_sel != 0 and kind == "dig": _treasure_ui.attempt_use(treasure_sel, cell) return - var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}]) + var outcomes: Array = 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 runner != null and runner.treasure_inventory != null: + if run_scope.runner != null and run_scope.runner.treasure_inventory != null: _treasure_ui.refresh_slots() # 迭代 7 T4——授予发生在 settle_batch 结算段内(同步走完,行尾重查) _handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {}) # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固) - if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared(): + 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(): board_zone.fx.play_clear_burst() - if accepted and kind == "dig" and settlement != null and settlement.minesweeper != null: + if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null: # 挖掘演出(棋盘小人贴图批 2026-09-25):挥铲三帧 + 角色弹跳——触发=格子点开那一刻 #(2026-09-25 用户裁决;settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态; # 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位) - var hero_cell: Vector2i = settlement.minesweeper.hero_pos() + var hero_cell: Vector2i = run_scope.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": @@ -1183,7 +1199,7 @@ func _on_stop_pressed() -> void: if _input_locked(): return # stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济) - var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) + var outcomes: Array = 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:防静默改判读感欺骗) @@ -1198,21 +1214,21 @@ func _on_refresh_confirmed() -> void: 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() + if run_scope.level_active and run_scope.runner != null: + ok = run_scope.runner.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: - _refresh_used_round += 1 - logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round}) + run_scope.refresh_used_round += 1 + logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": run_scope.refresh_used_round}) board_zone.toast.show_toast("RAP−%d · 旧板旗作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold") else: # 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断) - var rap: int = int(settlement.presentation_snapshot().get("rap", 0)) if settlement != null else 0 + var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.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}) + logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": run_scope.refresh_used_round + 1}) if result == "rejected_rap": board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn") ap_bar.pulse_rap() @@ -1237,8 +1253,8 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void: 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) + var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - run_scope.refresh_used_round) + HudPresenter.refresh(stage, info_panel, ap_bar, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining) _treasure_ui.refresh_slots() # C 区秘宝槽重查(反哺批⑤——试炼镐积点进度随挖格刷新悬停浮窗) _sync_settlement_visuals() @@ -1246,9 +1262,9 @@ func _refresh_presentations() -> void: func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() - if settling and not _was_settling: + if settling and not run_scope.was_settling: settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带(演出链批)——时序敏感演出触发,例外留在组合根 - _was_settling = settling + run_scope.was_settling = settling HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, _phase_text()) @@ -1259,9 +1275,9 @@ func _is_settling() -> bool: # 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外: # overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。 # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透(T3 收口批)。 - if settlement == null or not settlement.is_battle_live(): + if run_scope.settlement == null or not run_scope.settlement.is_battle_live(): return not _row_queue.is_empty() - if String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: + if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: return true return not _row_queue.is_empty() @@ -1269,11 +1285,11 @@ func _is_settling() -> bool: func _phase_text() -> String: # 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫;相位取数经 # presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) - if settlement == null: + if run_scope.settlement == null: return "第 — 轮 · —" - return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result), - String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, - _boss_active, Narrator.boss_name(runner, db)) + return Narrator.phase_text(int(run_scope.settlement.round_index), String(run_scope.settlement.battle_result), + String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, + run_scope.boss_active, Narrator.boss_name(run_scope.runner, db)) # ==================== B 区明细带 / 图例(模态入口)==================== @@ -1289,12 +1305,12 @@ func _on_legend_toggled() -> void: func _on_diagnose_pressed() -> void: # 迭代 18 卡⑦——本局种子现值(原 _seed_value() 重算冒充病灶改向 _current_master_seed 只读单点) - var ctx := {"mode": "level" if _level_active else "debug", "seed": _current_master_seed()} - if _level_active and runner != null: - ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result), - "hero_hp": runner.hero_hp, "settlement_is_null": runner.current_battle() == null} - if settlement != null: - ctx["settlement"] = settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12) + var ctx := {"mode": "level" if run_scope.level_active else "debug", "seed": _current_master_seed()} + if run_scope.level_active and run_scope.runner != null: + ctx["runner"] = {"node_index": run_scope.runner.node_index, "level_result": String(run_scope.runner.level_result), + "hero_hp": run_scope.runner.hero_hp, "settlement_is_null": run_scope.runner.current_battle() == null} + if run_scope.settlement != null: + ctx["settlement"] = run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12) var path: String = logger.dump_diagnosis(ctx) board_zone.toast.show_toast("已转储诊断: " + path, "info") @@ -1325,7 +1341,7 @@ func _on_debug_badge_pressed() -> void: func _sync_debug_overlay() -> void: if debug_overlay == null: return - debug_overlay.set_context_locked(settlement != null or _adventure_mode) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放——T2) + debug_overlay.set_context_locked(run_scope.settlement != null or run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放——T2) debug_overlay.update_run_data(_debug_run_data()) # T3 当局数据只读区块喂入(开窗+模态开合信号驱动) @@ -1334,20 +1350,20 @@ func _sync_debug_overlay() -> void: # 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域); # 缺位=「—」占位。零动效(纯标签重查——17卡③契约)。 func _debug_run_data() -> Dictionary: - var r = adventure.runner() if adventure != null else null + var r = run_scope.adventure.runner() if run_scope.adventure != null else null var seed_now: int = _current_master_seed() var seed_txt: String = str(seed_now) if seed_now >= 0 else "—" if r == null: return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} - return {"seed": seed_txt, "level": int(adventure.level_index), "node": int(r.node_index), - "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(adventure.keystone)} + return {"seed": seed_txt, "level": int(run_scope.adventure.level_index), "node": int(r.node_index), + "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(run_scope.adventure.keystone)} # 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值; # 调试两层=RNG 服务 master_seed。禁 _seed_value() 重算冒充本局种子(main.gd 旧 :1174 病灶随批改向)。 func _current_master_seed() -> int: - if adventure != null: - return int(adventure.master_seed) + if run_scope.adventure != null: + return int(run_scope.adventure.master_seed) return int(get_node("/root/RngService").master_seed) @@ -1363,7 +1379,7 @@ func _on_tune_apply(atk_pct: int, hp_pct: int) -> void: func _on_tune_simulate(n: int) -> void: # 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数。 # 对局中禁用兜底(迭代 18 卡⑦ T2——长程异步模拟与活跃结算并存态不开放;探针 emit 绕过 disabled 时由此守住) - if settlement != null: + if run_scope.settlement != null: return _tune_sim.run(n) @@ -1380,6 +1396,6 @@ func _seed_value() -> int: # 封闭流、禁 randomize()/自建 RNG——组合根建种子属 ADR-0007 豁免面,不进玩法 RNG 消费) if _seed_edit.text.is_valid_int(): return int(_seed_edit.text) - if save_manager != null and save_manager.is_first_adventure_done(): + if save_manager.is_first_adventure_done(): return randi() return 42 diff --git a/game/presentation/run_scope.gd b/game/presentation/run_scope.gd new file mode 100644 index 00000000..6b3e82d7 --- /dev/null +++ b/game/presentation/run_scope.gd @@ -0,0 +1,24 @@ +class_name RunScope +extends Node +# 一局会话状态承载(迭代 27 T1——P0-2「局生命周期物化」修复波)。 +# 会话 = 一次冒险(开新/续玩)或一次调试两层对局;组合根 _begin_session 创建本节点挂树、 +# 会话更替/终局整节点 queue_free()——旧手工复位清单(原 main._reset_run_scope 三项 + +# _on_battle_end_continue 置空四件)随节点释放机制消亡,此后新增跨局字段零新增泄漏点 +#(泄漏=低频难复现脏状态的手工契约病灶,审计 P0-2)。 +# 纪律:本节点零信号连接、零视图构建、零核心写调用(ADR-0001——信号连接只在组合根): +# 纯状态承载,组合根经 run_scope.X 读写。 +var adventure = null # 冒险结构编排器(调试两层恒 null) +var runner = null # 关卡推进器(冒险每关重取 adventure.runner();调试两层恒 null) +var settlement = null # 当前战斗编排器(冒险与调试两层统一读它) +var is_adventure := false # 冒险会话标记(inter 层确认分派与终局分派) +var level_active := false # 对局激活(状态面板入口点亮/刷新受理分派) +var level_screen_active := false # 关卡独立屏激活状态(屏级互斥——形态升格批语义) +var refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零) +var boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) +var was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批) +var hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent) +var settlement_stop_timer := ArmedTimer.new() # 卡⑥ 结算框挂起计时(T3/D3 P4 乙——窗口 0=挂起到显式回落) +var pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷) +var locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放) +var last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) +var gp_ledger = null # GP 冒险钱包影子账(gp_shadow_ledger.gd 实例——wallet 事件投喂;T3 账本收口波将退役) diff --git a/game/presentation/run_scope.gd.uid b/game/presentation/run_scope.gd.uid new file mode 100644 index 00000000..ce8f9dcb --- /dev/null +++ b/game/presentation/run_scope.gd.uid @@ -0,0 +1 @@ +uid://d05dkfqnbqdlx diff --git a/game/presentation/scenes/adventure_screen.gd b/game/presentation/scenes/adventure_screen.gd new file mode 100644 index 00000000..f31e1776 --- /dev/null +++ b/game/presentation/scenes/adventure_screen.gd @@ -0,0 +1,37 @@ +class_name AdventureScreen +extends Control +# 冒险屏壳(迭代 27 T1 场景拆分——审计 P0-1 第一步;内含关卡地图屏与战斗屏两容器)。 +# 两容器视图原样构建(build_battle_ui 归 ui_assembler 纪律不变、LevelMapScreen 类视图继续 +# 代码构建、零 .tscn 化改造——契约 2);战斗信号接线归组合根 main.gd(ADR-0001——信号连接 +# 只在组合根)。树序语义:本屏壳挂 main 首子位(全模态之下)——屏内 level_screen 与 +# battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后序零视觉差; +# 模态登记序不变(level_screen 实例仍按节点层与三选一层之间登记)。 +# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。 + +var battle_root: Control # 战斗屏宿主(B/C/D 三区容器——原 main._battle_root 句柄指向成员) +var stage: BattleStageView # B 区(含 log_view 明细带) +var log_view: BattleLogView +var info_panel: HeroInfoPanelView # C 区 +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 # 结算演出覆盖层 +var level_screen: LevelMapScreen # 关卡画面(独立屏) + +const UiAssembler := preload("res://presentation/ui_assembler.gd") # 构建归装配器(规范 1.1——非全局类,preload 引用) + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) + battle_root = VBoxContainer.new() + add_child(battle_root) + var battle_ui: Dictionary = UiAssembler.build_battle_ui(battle_root, get_node("/root/ConfigDB")) + 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 + ap_bar = battle_ui.ap_bar + settlement_fx = battle_ui.settlement_fx + level_screen = LevelMapScreen.new() + add_child(level_screen) diff --git a/game/presentation/scenes/adventure_screen.gd.uid b/game/presentation/scenes/adventure_screen.gd.uid new file mode 100644 index 00000000..2046833d --- /dev/null +++ b/game/presentation/scenes/adventure_screen.gd.uid @@ -0,0 +1 @@ +uid://csw5cl1xa7qu7 diff --git a/game/presentation/scenes/adventure_screen.tscn b/game/presentation/scenes/adventure_screen.tscn new file mode 100644 index 00000000..4645ba9d --- /dev/null +++ b/game/presentation/scenes/adventure_screen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://slice0advn0screen"] + +[ext_resource type="Script" path="res://presentation/scenes/adventure_screen.gd" id="1_advn"] + +[node name="AdventureScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_advn") diff --git a/game/presentation/scenes/camp_screen.gd b/game/presentation/scenes/camp_screen.gd new file mode 100644 index 00000000..fb32f571 --- /dev/null +++ b/game/presentation/scenes/camp_screen.gd @@ -0,0 +1,14 @@ +class_name CampScreen +extends Control +# 营地屏壳(迭代 27 T1 场景拆分——审计 P0-1 第一步)。 +# 营地覆盖层视图原样构建(CampOverlay 类视图继续代码构建、零 .tscn 化改造——契约 2); +# 购买/开合接线归组合根经 CampShopPresenter(ADR-0001——信号连接只在组合根)。 +# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。 + +var camp_overlay: CampOverlay # 营地覆盖层(原 build_overlays 营地段迁入——main 组合根句柄指向本屏成员) + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) + camp_overlay = CampOverlay.new() + add_child(camp_overlay) diff --git a/game/presentation/scenes/camp_screen.gd.uid b/game/presentation/scenes/camp_screen.gd.uid new file mode 100644 index 00000000..c817baf2 --- /dev/null +++ b/game/presentation/scenes/camp_screen.gd.uid @@ -0,0 +1 @@ +uid://dw1k6rn5w8gso diff --git a/game/presentation/scenes/camp_screen.tscn b/game/presentation/scenes/camp_screen.tscn new file mode 100644 index 00000000..5d1f5594 --- /dev/null +++ b/game/presentation/scenes/camp_screen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://slice0camp0screen"] + +[ext_resource type="Script" path="res://presentation/scenes/camp_screen.gd" id="1_camp"] + +[node name="CampScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_camp") diff --git a/game/presentation/scenes/menu_screen.gd b/game/presentation/scenes/menu_screen.gd new file mode 100644 index 00000000..ee8eb9d4 --- /dev/null +++ b/game/presentation/scenes/menu_screen.gd @@ -0,0 +1,31 @@ +class_name MenuScreen +extends Control +# 开始菜单屏壳(迭代 27 T1 场景拆分——审计 P0-1「场景作为组合单元/生命周期单元」第一步)。 +# 视图代码原样构建(节点构建归 ui_assembler.gd 纪律不变——本屏壳只持有与暴露句柄); +# 按钮接线归组合根 main.gd(ADR-0001——信号连接只在组合根)。 +# 可独立实例化(F6/headless 挂载)——V2「各新屏场景可独立实例化」探针断言载体。 +# main 组合根保留 _start_overlay 等句柄指向本屏成员——可见性开关与探针挂载假设零迁移。 + +var start_overlay: Control # 全屏菜单层(原 main._start_overlay——组合根可见性开关句柄不变) +var box: VBoxContainer +var lvbtn: Button +var resumebtn: Button +var campbtn: Button +var careerbtn: Button +var settingsbtn: Button +var quitbtn: Button + +const UiAssembler := preload("res://presentation/ui_assembler.gd") # 构建归装配器(规范 1.1——非全局类,preload 引用) + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) + var start: Dictionary = UiAssembler.build_start_overlay(self, get_node("/root/ConfigDB")) + start_overlay = start.overlay + box = start.box + lvbtn = start.lvbtn + resumebtn = start.resumebtn + campbtn = start.campbtn + careerbtn = start.careerbtn + settingsbtn = start.settingsbtn + quitbtn = start.quitbtn diff --git a/game/presentation/scenes/menu_screen.gd.uid b/game/presentation/scenes/menu_screen.gd.uid new file mode 100644 index 00000000..fdee5253 --- /dev/null +++ b/game/presentation/scenes/menu_screen.gd.uid @@ -0,0 +1 @@ +uid://dqijp10rs1kaj diff --git a/game/presentation/scenes/menu_screen.tscn b/game/presentation/scenes/menu_screen.tscn new file mode 100644 index 00000000..b9cd0d63 --- /dev/null +++ b/game/presentation/scenes/menu_screen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://slice0menu0screen"] + +[ext_resource type="Script" path="res://presentation/scenes/menu_screen.gd" id="1_menu"] + +[node name="MenuScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_menu") diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index 0430715e..5d352e23 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -62,9 +62,10 @@ static func build_battle_ui(root: Control, db) -> Dictionary: # 模态覆盖层组(z 序 = 登记序:图例 < 节点 < 终局 < 商店)。 -# 返回 {legend_overlay, node_overlay, end_overlay, shop_overlay, camp_overlay};调用方按本序 addChild。 +# 返回 {legend_overlay, node_overlay, end_overlay, shop_overlay};调用方按本序 addChild。 # (调整板已随迭代 14.5 调试整合批退役——调试浮窗由组合根直构挂树序最末,见 main._build_ui; -# 明细回看 overlay 已随 M9/P-C7 退役 2026-10-04——冒险中回看归「生涯」06 登记) +# 明细回看 overlay 已随 M9/P-C7 退役 2026-10-04——冒险中回看归「生涯」06 登记; +# 营地覆盖层已随迭代 27 T1 场景拆分迁 camp_screen 屏壳——本组不再构建) static func build_overlays(host: Control, db) -> Dictionary: var legend_overlay := LegendOverlay.new(int(db.action_economy.rap_refresh_cost)) # 打磨包 2026-09-29——RAP 行表值注入 host.add_child(legend_overlay) @@ -79,16 +80,11 @@ static func build_overlays(host: Control, db) -> Dictionary: var shop_overlay := ShopOverlay.new() host.add_child(shop_overlay) - # 营地覆盖层(元进度实现批 2026-09-21——主菜单营地入口兑付呈现;开合与开始层互斥仿设置面先例) - var camp_overlay := CampOverlay.new() - host.add_child(camp_overlay) - return { "legend_overlay": legend_overlay, "node_overlay": node_overlay, "end_overlay": end_overlay, "shop_overlay": shop_overlay, - "camp_overlay": camp_overlay, } diff --git a/game/presentation/views/status_panel.gd.uid b/game/presentation/views/status_panel.gd.uid new file mode 100644 index 00000000..a22797d1 --- /dev/null +++ b/game/presentation/views/status_panel.gd.uid @@ -0,0 +1 @@ +uid://dd071t8g8cjb3 diff --git a/game/project.godot b/game/project.godot index 66438d00..1eb9b8ec 100644 --- a/game/project.godot +++ b/game/project.godot @@ -18,6 +18,8 @@ config/features=PackedStringArray("4.7") ConfigDB="*res://core/config_db.gd" RngService="*res://core/rng_service.gd" +SaveManager="*res://core/save_manager.gd" +MetaProgression="*res://core/meta_progression.gd" [display] diff --git a/game/smoke/fx_shot_probe.gd b/game/smoke/fx_shot_probe.gd index c29fddd4..ae6f2d33 100644 --- a/game/smoke/fx_shot_probe.gd +++ b/game/smoke/fx_shot_probe.gd @@ -51,11 +51,11 @@ func _ready() -> void: main.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame - var st = main.settlement + var st = main.run_scope.settlement if st == null: _fail("首战未开(settlement 空)") return - var rules = main.settlement.minesweeper + var rules = main.run_scope.settlement.minesweeper var board = rules.current_board() # 自有驱动(只挖/走——测试驱动可用板面数据挑安全格;新手策略档的插旗拒绝会耗尽预算):挖到 3 格停 var dug := 0 diff --git a/game/smoke/probe_ui_util.gd b/game/smoke/probe_ui_util.gd index fbcc6ce1..3b93bd76 100644 --- a/game/smoke/probe_ui_util.gd +++ b/game/smoke/probe_ui_util.gd @@ -90,8 +90,11 @@ static func _text_match(text: String, needle: String, mode: String) -> bool: # tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。 # mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注 #(隔离 cfg 名 = _settings.cfg,与被收口探针原文件名逐一同名);refresh_resume = 组合根 -# 「继续上局」按钮重刷;swap_save = 是否换注 main.save_manager(营地先行段只换 meta 不换 save_manager)。 +# 「继续上局」按钮重刷;swap_save = 是否换注全局 SaveManager(营地先行段只换 meta 侧不换 SaveManager)。 # 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。 +# 迭代 27 T1——SaveManager/MetaProgression 升格 autoload 后隔离面改单例原地重置 +#(attach_path 沿 RngService.set_master_seed 先例;MetaProgression 持 save 引用透传, +# swap_save=false 分支换 meta 侧引用=独立 iso 实例,语义同旧「只换 meta 不换 save_manager」)。 static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false, refresh_resume: bool = false, swap_save: bool = true): var save_path := "user://%s.json" % tag @@ -102,9 +105,15 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_ iso.load_from_disk() if mark_first_done: iso.mark_first_adventure_done() + # --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析 + var sm = main.get_node("/root/SaveManager") if swap_save: - main.save_manager = iso - main.meta = MetaProgressionScript.new(main.db, iso) + sm.attach_path(save_path) + if mark_first_done: + sm.mark_first_adventure_done() + iso = sm + else: + main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动 if with_settings: main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) if refresh_resume: diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 75d38edd..c0aa8aaa 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -73,10 +73,128 @@ func _initialize() -> void: if load_ok: _run_core_smoke(db, rng) + _split_tail() # 迭代 27 T1 V2/V3——场景拆分+会话物化断言(挂树帧推进型异步尾段;摘要与退出码随尾段落) + + +# ---- 场景拆分/会话物化异步尾段(迭代 27 T1——autoload 挂载需过 _initialize 后首帧〔run_all.gd 头注时序〕, +# 挂 main.tscn 的断言在帧后执行;摘要打印与 quit 从 _initialize 移本尾段)---- +func _split_tail() -> void: + await process_frame + await process_frame + var autoload_ok: bool = get_root().get_node_or_null("/root/ConfigDB") != null and get_root().get_node_or_null("/root/SaveManager") != null + if autoload_ok: + await _smoke_scene_split() # 协程函数必须 await(无 await=发射后不管,首个 await 后即被 quit 截断) + await _smoke_run_lifecycle() + else: + check(false, "V2/V3 前置:autoload 链未就绪(ConfigDB/SaveManager)") print("=== 冒烟完成:%d 项检查,%d 项失败 ===" % [_checks, _failures]) quit(1 if _failures > 0 else 0) +# ---- 三屏场景化断言(迭代 27 T1 V2——独立实例化+关键节点在位+组合根树序/z 序保序)---- +func _smoke_scene_split() -> void: + print("--- 迭代27 T1 三屏场景化(V2:屏壳独立实例化+组合根装配保序)---") + # ① 三屏壳独立实例化(F6 等价——headless 直挂;视图代码构建在屏壳 _ready) + var menu: Node = load("res://presentation/scenes/menu_screen.tscn").instantiate() + root.add_child(menu) + await process_frame + check(menu.start_overlay != null and menu.lvbtn != null and menu.resumebtn != null and menu.campbtn != null and menu.careerbtn != null and menu.settingsbtn != null and menu.quitbtn != null, + "V2 菜单屏壳独立实例化(start_overlay+操作组六钮在位)") + check(menu.start_overlay.visible, "V2 菜单层默认可见(非模态全屏态)") + check(menu.mouse_filter == Control.MOUSE_FILTER_IGNORE, "V2 屏壳根零鼠标截获(IGNORE——全屏 STOP 壳吃输入病灶回归闸)") + menu.queue_free() + var camp: Node = load("res://presentation/scenes/camp_screen.tscn").instantiate() + root.add_child(camp) + await process_frame + check(camp.camp_overlay != null, "V2 营地屏壳独立实例化(camp_overlay 在位)") + check(not camp.camp_overlay.visible, "V2 营地层默认隐藏(模态纪律)") + camp.queue_free() + var adv: Node = load("res://presentation/scenes/adventure_screen.tscn").instantiate() + root.add_child(adv) + await process_frame + check(adv.battle_root != null and adv.stage != null and adv.board_zone != null and adv.board_view != null and adv.ap_bar != null and adv.level_screen != null, + "V2 冒险屏壳独立实例化(战斗宿主+四区句柄+关卡地图屏在位)") + adv.queue_free() + await process_frame + # ② 组合根装配(树序=菜单屏@0+冒险屏@1 在全模态之下;营地屏壳夹商店与设置层之间——z 序保序) + var main: Node = load("res://presentation/main.tscn").instantiate() + root.add_child(main) + await process_frame + var kids: Array = main.get_children() + check(kids.find(main._menu_screen) == 0 and kids.find(main._adventure_screen) == 1, + "V2 组合根树序=屏壳在前(menu@0 adventure@1——视觉共存对全模态在其上)") + var i_camp: int = kids.find(main._camp_screen) + check(i_camp > kids.find(main.shop_overlay) and i_camp < kids.find(main.settings_overlay), + "V2 营地屏壳树序夹商店层与设置层之间(登记序=树序=z 序纪律保序)") + check(kids.find(main._adventure_screen) < kids.find(main.legend_overlay), + "V2 冒险屏壳在全模态之下(图例战斗中开=其上——视觉共存对验算位)") + # ③ 探针挂载假设句柄在座(main.X 一级路径——探针迁移面收口证明) + check(main._start_overlay == main._menu_screen.start_overlay and main.camp_overlay == main._camp_screen.camp_overlay and main.level_screen == main._adventure_screen.level_screen and main._battle_root == main._adventure_screen.battle_root and main.board_view == main._adventure_screen.board_view, + "V2 组合根句柄指向屏壳成员(探针挂载假设 main.X 零迁移)") + check(main.run_scope != null and main.run_scope.gp_ledger != null and not main.run_scope.is_adventure, + "V2 闲时会话自 _ready 即在(永非 null——探针注入面时序)") + main.queue_free() + await process_frame + + +# ---- 会话物化断言(迭代 27 T1 V3——两连局复位/无脏状态残留;原手工复位清单消亡的行为级证明)---- +func _smoke_run_lifecycle() -> void: + print("--- 迭代27 T1 会话物化(V3:两连局复位+脏状态不残留)---") + var main: Node = load("res://presentation/main.tscn").instantiate() + root.add_child(main) + await process_frame + # 存档隔离(autoload attach 原地重置——绝不碰真实档 user://save.json) + var sm = main.get_node("/root/SaveManager") + for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]: + if FileAccess.file_exists(p): + DirAccess.remove_absolute(p) + sm.attach_path("user://smoke_run_lifecycle.json") + sm.mark_first_adventure_done() + # ---- 第一局:开局→注脏→放弃→终局出口→会话收束 ---- + main.debug_overlay.seed_edit.text = "42" + main._start_adventure_from_menu() + await process_frame + check(main.run_scope != null and main.run_scope.is_adventure and main.run_scope.adventure != null, + "V3 开局建会话(is_adventure+adventure 在座)") + check(main.run_scope.hints_shown.is_empty() and main.run_scope.refresh_used_round == 0, + "V3 新会话局态零值") + # 注入脏状态(旧手工清单时代的典型泄漏源——新会话必须整体吞掉) + main.run_scope.hints_shown["chord"] = true + main.run_scope.refresh_used_round = 2 + main.run_scope.boss_active = true + main.run_scope.last_gain_amount = 77 + var finished := {"done": false} + main.run_scope.adventure.adventure_finished.connect(func(_s: Dictionary) -> void: + finished.done = true, CONNECT_ONE_SHOT) + check(main.run_scope.adventure.abandon(), "V3 放弃受理(入口停点天然非战斗期)") + for i in range(8): + if finished.done: + break + await process_frame + check(finished.done, "V3 冒险终局到达(abandon→adventure_finished 同步链)") + check(main._end_overlay.is_open(), "V3 终局面板弹起") + main._on_battle_end_continue() # 终局出口(回菜单+会话收束——手工置空清单已消亡) + await process_frame + check(not main.run_scope.is_adventure and main.run_scope.adventure == null and main.run_scope.runner == null, + "V3 会话收束(新闲时会话零冒险/关卡对象)") + check(main.run_scope.hints_shown.is_empty() and main.run_scope.refresh_used_round == 0 and not main.run_scope.boss_active and main.run_scope.last_gain_amount == 0, + "V3 局态旗/账本全复位(queue_free 物化——注入脏状态零残留)") + check(main._start_overlay.visible, "V3 回开始菜单(终局出口路径)") + # ---- 第二局:放弃终局无断点残留→直进开局→全新会话 ---- + check(not sm.has_breakpoint(), "V3 放弃终局清断点(无残留断点——续玩按钮态联动)") + main.debug_overlay.seed_edit.text = "43" + main._start_adventure_from_menu() + await process_frame + check(main.run_scope.is_adventure and main.run_scope.adventure != null and main.run_scope.hints_shown.is_empty() and main.run_scope.refresh_used_round == 0, + "V3 第二连局全新会话(两连局零泄漏)") + check(not main._start_overlay.visible, "V3 直进入局(菜单隐)") + main.queue_free() + await process_frame + for p in ["user://smoke_run_lifecycle.json", "user://smoke_run_lifecycle.json.tmp"]: + if FileAccess.file_exists(p): + DirAccess.remove_absolute(p) + + # ---- Core 集成冒烟(表加载成功才执行)---- var _db var _rng diff --git a/game/smoke/t17_defer_probe.gd b/game/smoke/t17_defer_probe.gd index 1837d9b7..90aa8fb1 100644 --- a/game/smoke/t17_defer_probe.gd +++ b/game/smoke/t17_defer_probe.gd @@ -45,32 +45,32 @@ func _ready() -> void: main.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame - if main.settlement == null: + if main.run_scope.settlement == null: _fail("战斗未启动") return # 直挖至击杀胜利(settle_batch 真实管线——step_row 信号→排播队列,与点击路径同链) - var ms = main.settlement.minesweeper + var ms = main.run_scope.settlement.minesweeper var guard := 200 var t_ap_out := -1 # AP 用完/停手帧 var ap_clock := "" # 墙上时钟时间戳 var settle_clock := "" - while guard > 0 and String(main.settlement.battle_result) == "": + while guard > 0 and String(main.run_scope.settlement.battle_result) == "": guard -= 1 var plan = load("res://smoke/auto_player.gd").choose_dig(ms) if plan.is_empty(): - main.settlement.settle_batch([{"action": "stop", "cell": Vector2i(-1, -1)}]) + main.run_scope.settlement.settle_batch([{"action": "stop", "cell": Vector2i(-1, -1)}]) await get_tree().process_frame continue if plan.hero_cell != ms.hero_pos(): ms.settle_batch([{"action": "move", "cell": plan.hero_cell}]) ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if t_ap_out < 0 and main.settlement.action_economy.phase != "Active": + if t_ap_out < 0 and main.run_scope.settlement.action_economy.phase != "Active": t_ap_out = Time.get_ticks_msec() ap_clock = _clock_ms() settle_clock = ap_clock # 触发结算与 AP 用完同帧(核心同步)——同一墙上时刻 await get_tree().process_frame - if String(main.settlement.battle_result) != "victory": - _fail("战斗未胜利(result='%s')" % str(main.settlement.battle_result)) + if String(main.run_scope.settlement.battle_result) != "victory": + _fail("战斗未胜利(result='%s')" % str(main.run_scope.settlement.battle_result)) return var t_settle := Time.get_ticks_msec() var _rows_at_victory: int = main._row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 25631a2b..a0294613 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -269,7 +269,7 @@ func _ready() -> void: _problems.append("三选一停点打开时输入未锁定") if main.curio_overlay._continue_btn.visible: main.curio_overlay.press_continue() - elif main.adventure != null and not main.adventure._curio_pending.is_empty(): + elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty(): main.curio_overlay.press_card(0) main.curio_overlay.press_confirm() else: @@ -319,7 +319,7 @@ func _ready() -> void: next_btn.pressed.emit() await get_tree().process_frame await get_tree().process_frame - if main.runner == null or int(main.runner.level_row.get("level_index", -1)) != 2: + if main.run_scope.runner == null or int(main.run_scope.runner.level_row.get("level_index", -1)) != 2: _problems.append("大奖励确认后未推进到第二关(level_index != 2)") else: _done = true @@ -332,8 +332,8 @@ func _ready() -> void: if not main._input_locked(): _problems.append("胜利结算框打开时输入未锁定") var gp_line: String = "%s %s" % [main._end_overlay._gp_name.text, main._end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言 - if not gp_line.contains("GP +%d" % main._last_gain_amount) or not gp_line.contains("合计 %d" % int(main.runner.gp)): - _problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main._last_gain_amount, int(main.runner.gp)]) + if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)): + _problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)]) if main._end_overlay.verdict_text() == "": _problems.append("胜利结算框判定行缺失") _settlement_confirms += 1 @@ -350,8 +350,8 @@ func _ready() -> void: # T17 演播后排窗适配:真终局 vs 中途结算面判据=adventure.state(冒险结构三态—— # 战斗胜利停点=running / 真终局(失败/放弃/打穿)=settling)。旧判据 # (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。 - var _adv_running: bool = main.adventure != null and String(main.adventure.state) == "running" - if main._adventure_mode and _adv_running: + var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running" + if main.run_scope.is_adventure and _adv_running: main._end_overlay._btn.pressed.emit() continue if not main._input_locked(): @@ -360,12 +360,12 @@ func _ready() -> void: print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [ main._end_overlay.verdict_text().replace("\n", " "), _route_clicks]) break - if main.settlement == null or main.runner == null: + if main.run_scope.settlement == null or main.run_scope.runner == null: continue - var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) + var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) var st := { - "node": main.runner.node_index, "round": main.settlement.round_index, - "phase": main.settlement.action_economy.phase, "ap": main.settlement.action_economy.ap, + "node": main.run_scope.runner.node_index, "round": main.run_scope.settlement.round_index, + "phase": main.run_scope.settlement.action_economy.phase, "ap": main.run_scope.settlement.action_economy.ap, "settling": diag.settling, } if JSON.stringify(st) == JSON.stringify(_last_state): @@ -384,7 +384,7 @@ func _ready() -> void: _ap_spent_this_round = 0 _last_state = st _locktest_tick() - if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active": + if main.run_scope.settlement.battle_result == "" and main.run_scope.settlement.action_economy.phase == "Active": # T1 D1——锁测在飞期(state 1)冻结探针动作面:排空帧 autoplay 的免费 move 会抢在 # 帧末 deferred 重放之前改变英雄位(flag 拒收 not_adjacent 实证)——「注入→重放」 # 窗口内局面零变化正是 v3.1 被测语义(演播期冻结),动作面恢复待断言收口。 @@ -402,14 +402,14 @@ func _ready() -> void: await get_tree().process_frame if main._end_overlay.is_open(): # 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。 - if main._adventure_mode and main.adventure != null and String(main.adventure.state) == "running": + if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running": main._end_overlay._btn.pressed.emit() else: break elif main.curio_overlay.visible: if main.curio_overlay._continue_btn.visible: main.curio_overlay.press_continue() - elif main.adventure != null and not main.adventure._curio_pending.is_empty(): + elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty(): main.curio_overlay.press_card(0) main.curio_overlay.press_confirm() else: @@ -432,8 +432,8 @@ func _ready() -> void: for i in 10: await get_tree().process_frame break - elif main.settlement != null and main.settlement.battle_result == "" \ - and main.settlement.action_economy.phase == "Active": + elif main.run_scope.settlement != null and main.run_scope.settlement.battle_result == "" \ + and main.run_scope.settlement.action_economy.phase == "Active": _play_one_action() # 排播期/Waiting 等待态=自然流逝(演播秒级、锁随队列空解除) # T17 适配:战败或放弃二途皆达终局面(放弃=预算耗尽收口路径——见上),断点段前提不变。 @@ -474,11 +474,11 @@ func _ready() -> void: iso_chk.load_from_disk() if not iso_chk.has_breakpoint(): _problems.append("断点段:入口停点断点未写盘(卡①写面失守)") - var cand_before: Array = main.runner.next_candidate_ids() + var cand_before: Array = main.run_scope.runner.next_candidate_ids() # 合成「关游戏重开回菜单」(探针专用——真实路径无此入口;断点已在盘) main._exit_level_screen() main._start_overlay.visible = true - main.adventure = null + main.run_scope.adventure = null main._refresh_resume_button() await get_tree().process_frame if not resume_btn.visible: @@ -487,10 +487,10 @@ func _ready() -> void: resume_btn.pressed.emit() # 真实按钮信号路径 → _resume_from_menu → resume_adventure await get_tree().process_frame await get_tree().process_frame - if main.adventure == null or main.runner == null or not main.level_screen.visible: + if main.run_scope.adventure == null or main.run_scope.runner == null or not main.level_screen.visible: _problems.append("断点段:续玩未恢复至停点(关卡画面未现)") else: - var cand_after: Array = main.runner.next_candidate_ids() + var cand_after: Array = main.run_scope.runner.next_candidate_ids() if cand_after.size() != cand_before.size() or (cand_after.size() > 0 and int(cand_after[0]) != int(cand_before[0])): _problems.append("断点段:续玩候选与中断前不一致(停点重建失准)") if not iso_chk.load_from_disk() or not iso_chk.has_breakpoint(): @@ -498,12 +498,12 @@ func _ready() -> void: # ③ 互斥确认弹窗(二轮修改批 A):断点在档点开始冒险 → 弹确认框;取消=回菜单;开新冒险=提交 main._exit_level_screen() main._start_overlay.visible = true - main.adventure = null + main.run_scope.adventure = null main._refresh_resume_button() await get_tree().process_frame lv_btn.pressed.emit() await get_tree().process_frame - if not main.confirm_dialog.is_open() or main.adventure != null: + if not main.confirm_dialog.is_open() or main.run_scope.adventure != null: _problems.append("断点段:有断点点开始冒险未弹确认框(二轮 A 失守)") var cancel_btn: Button = ProbeUiUtil.find_button(main.confirm_dialog, "取消", "contains") if cancel_btn == null: @@ -511,7 +511,7 @@ func _ready() -> void: else: cancel_btn.pressed.emit() await get_tree().process_frame - if main.confirm_dialog.is_open() or main.adventure != null or not main._start_overlay.visible: + if main.confirm_dialog.is_open() or main.run_scope.adventure != null or not main._start_overlay.visible: _problems.append("断点段:取消未回开始菜单(二轮 A)") lv_btn.pressed.emit() await get_tree().process_frame @@ -522,7 +522,7 @@ func _ready() -> void: confirm_btn.pressed.emit() await get_tree().process_frame await get_tree().process_frame - if main.adventure == null or main._start_overlay.visible or main.confirm_dialog.is_open(): + if main.run_scope.adventure == null or main._start_overlay.visible or main.confirm_dialog.is_open(): _problems.append("断点段:确认开新冒险后未开局(二轮 A)") print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [ str(_done), str(action_budget), _problems.size()]) @@ -610,24 +610,24 @@ func _run_modal_probes_once() -> void: # ④ 弃图刷新确认链(pattern 7 异位:确认 chips 与常规确认不同位 + 显式代价文案) if not _tested_refresh_chain and main.ap_bar.is_refresh_ready(): _tested_refresh_chain = true - var rap_before: int = int(main.settlement.action_economy.rap) - var board_before = main.settlement.minesweeper.current_board() + var rap_before: int = int(main.run_scope.settlement.action_economy.rap) + var board_before = main.run_scope.settlement.minesweeper.current_board() main.ap_bar.press_refresh_for_probe() if not main.ap_bar.confirm_visible(): _problems.append("弃图刷新确认 chips 未出现(异位确认缺失)") main.ap_bar.press_confirm_for_probe() - if int(main.settlement.action_economy.rap) != rap_before - 8: - _problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.settlement.action_economy.rap)]) - if main.settlement.action_economy.phase != "Active": - _problems.append("轮内刷新后相位丢失(%s)" % main.settlement.action_economy.phase) - if main.settlement.minesweeper.current_board() == board_before: + if int(main.run_scope.settlement.action_economy.rap) != rap_before - 8: + _problems.append("弃图刷新未扣 RAP8(%d→%d)" % [rap_before, int(main.run_scope.settlement.action_economy.rap)]) + if main.run_scope.settlement.action_economy.phase != "Active": + _problems.append("轮内刷新后相位丢失(%s)" % main.run_scope.settlement.action_economy.phase) + if main.run_scope.settlement.minesweeper.current_board() == board_before: _problems.append("轮内刷新未当场换图") - if main._refresh_used_round != 1: - _problems.append("刷新轮内计数错误(%d)" % main._refresh_used_round) + if main.run_scope.refresh_used_round != 1: + _problems.append("刷新轮内计数错误(%d)" % main.run_scope.refresh_used_round) func _play_one_action() -> void: - var settlement = main.settlement + var settlement = main.run_scope.settlement if settlement == null: return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固) if not main._row_queue.is_empty(): @@ -696,7 +696,7 @@ func _click_stop() -> void: func _locktest_tick() -> void: if _locktest_state == 2: return - var settlement = main.settlement + var settlement = main.run_scope.settlement if settlement == null or settlement.minesweeper.current_board() == null: return if _locktest_state == 0: @@ -716,7 +716,7 @@ func _locktest_tick() -> void: _problems.append("锁期断言:演播期点击未被冻结(旗即时受理——v3 冻结失守)") _locktest_state = 2 return - if main._locked_click.is_empty() or main._locked_click.get("cell") != target: + if main.run_scope.locked_click.is_empty() or main.run_scope.locked_click.get("cell") != target: _problems.append("锁期断言:演播期点击未入缓存(v3.1 缓存失守)") _locktest_state = 2 return @@ -733,10 +733,10 @@ func _locktest_tick() -> void: if _locktest_frames < 3: return # 排空边沿的 deferred 重放(pump_ended 帧末执行)在途宽限 if _locktest_frames > 90: - _problems.append("重放断言:演播完边沿后缓存未清(%s)" % str(main._locked_click)) + _problems.append("重放断言:演播完边沿后缓存未清(%s)" % str(main.run_scope.locked_click)) _locktest_state = 2 return - if main._locked_click.is_empty(): + if main.run_scope.locked_click.is_empty(): if not settlement.minesweeper.has_flag(_locktest_cell): var b = settlement.minesweeper.current_board() print("[lockdiag] 重放后旗未置:br='%s' phase=%s ap=%d cell=%s revealed=%s board=%dx%d hero=%s" % [ @@ -767,20 +767,20 @@ func _report_stall(st: Dictionary) -> void: why = "结算停滞:Waiting 相位无推进(结算应同步走完——窗口段已删除)" elif st.phase == "Active" and st.ap == 0: why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)" - elif st.phase == "Idle" and main.settlement.battle_result == "": + elif st.phase == "Idle" and main.run_scope.settlement.battle_result == "": why = "Idle 相位但战斗未结束(begin 失败?)" else: - why = "状态无推进(battle_result='%s')" % str(main.settlement.battle_result) + why = "状态无推进(battle_result='%s')" % str(main.run_scope.settlement.battle_result) print("[click_probe] 🔴 卡死复现(PROBE_SEED=%s):%s" % [OS.get_environment("PROBE_SEED"), why]) _dump_state() get_tree().quit(2) func _dump_state() -> void: - var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) + var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [ - str(main.runner.node_index), str(main.settlement.round_index), - str(main.settlement.action_economy.phase), str(main.settlement.action_economy.ap), str(main.settlement.action_economy.rap)]) + str(main.run_scope.runner.node_index), str(main.run_scope.settlement.round_index), + str(main.run_scope.settlement.action_economy.phase), str(main.run_scope.settlement.action_economy.ap), str(main.run_scope.settlement.action_economy.rap)]) print(" battle_result='%s' settling=%s" % [ str(diag.battle_result), str(diag.settling)]) print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [ diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index 3876cb81..f84b04f0 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -40,11 +40,11 @@ func _ready() -> void: # 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。 if main.level_screen.visible: var ro = main.level_screen - var cands: Array = main.runner.next_candidate_ids() - var nds: Array = main.runner.nodes() + var cands: Array = main.run_scope.runner.next_candidate_ids() + var nds: Array = main.run_scope.runner.nodes() var tgt := {} for n in nds: - if main.runner.node_id_of(n) == int(cands[0]): + if main.run_scope.runner.node_id_of(n) == int(cands[0]): tgt = n var npos: Vector2 = ro.layout_probe(nds)[tgt] var gpos: Vector2 = ro.canvas_probe()["origin"] + npos @@ -63,30 +63,30 @@ func _ready() -> void: main.get_viewport().push_input(evc, true) await get_tree().process_frame await get_tree().process_frame - if main.level_screen.visible or main.runner.awaiting_choice(): - _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.runner.awaiting_choice())]) + if main.level_screen.visible or main.run_scope.runner.awaiting_choice(): + _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) for i in range(3): await get_tree().process_frame # ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x, # 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界) var ro2 = main.level_screen - var nds2: Array = main.runner.nodes() + var nds2: Array = main.run_scope.runner.nodes() var layer2 := {} var inbound2 := {} for n2 in nds2: - for tid2 in main.runner.node_out_ids(n2): + for tid2 in main.run_scope.runner.node_out_ids(n2): inbound2[tid2] = true var queue2: Array = [] for n2 in nds2: - if not inbound2.has(main.runner.node_id_of(n2)): + if not inbound2.has(main.run_scope.runner.node_id_of(n2)): queue2.append(n2) layer2[n2] = 0 while not queue2.is_empty(): var cur2: Dictionary = queue2.pop_front() - for tid2 in main.runner.node_out_ids(cur2): + for tid2 in main.run_scope.runner.node_out_ids(cur2): var tg2 := {} for n2 in nds2: - if main.runner.node_id_of(n2) == tid2: + if main.run_scope.runner.node_id_of(n2) == tid2: tg2 = n2 if tg2.is_empty() or layer2.has(tg2): continue @@ -102,9 +102,9 @@ func _ready() -> void: if float(cvmin["min_size"].x) < need_x2 - 0.5: _pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [cvmin["min_size"].x, need_x2]) var bv = main.board_view - var ms = main.settlement.minesweeper if main.settlement != null else null + var ms = main.run_scope.settlement.minesweeper if main.run_scope.settlement != null else null if ms == null or bv.cells_probe().is_empty(): - _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.settlement)]) + _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.run_scope.settlement)]) return bv.action_requested.connect(func(c: Vector2i, kind: String): _actions.append([c, kind])) bv.chord_attempted.connect(func(c: Vector2i): _chords.append(c)) @@ -426,13 +426,13 @@ func _ready() -> void: var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta) if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"): _problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", []))) - main.settlement.attunement = ["wide_scout_band"] + main.run_scope.settlement.attunement = ["wide_scout_band"] main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 4: _problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: _problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)") - main.settlement.attunement = [] + main.run_scope.settlement.attunement = [] main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 0: _problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)") diff --git a/game/smoke/ui_layout_probe.gd b/game/smoke/ui_layout_probe.gd index 459a729b..ce4cde07 100644 --- a/game/smoke/ui_layout_probe.gd +++ b/game/smoke/ui_layout_probe.gd @@ -43,7 +43,7 @@ func _ready() -> void: continue print("%-16s pos=%s size=%s min=%s hflag=%d vflag=%d" % [k, ctl.position, ctl.size, ctl.custom_minimum_size, ctl.size_flags_horizontal, ctl.size_flags_vertical]) - var root_box: Control = main.get_child(0) + var root_box: Control = main._adventure_screen.battle_root # 迭代 27 T1——屏壳化后战斗四区根迁冒险屏壳(序数寻址漂移改名寻址) print("four-zone root: anchors=(%s,%s)-(%s,%s) offsets=(%s,%s,%s,%s) size=%s" % [ root_box.anchor_left, root_box.anchor_top, root_box.anchor_right, root_box.anchor_bottom, root_box.offset_left, root_box.offset_top, root_box.offset_right, root_box.offset_bottom, root_box.size]) diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index 78ec22ce..22e05fcf 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -67,8 +67,8 @@ func _ready() -> void: problems.append("关卡开始未弹入口选路停点(level_screen 应可见)") elif main._battle_root.visible: problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") - elif main.runner == null or not main.runner.awaiting_choice() \ - or main.runner.next_candidate_ids().size() != 1: + elif main.run_scope.runner == null or not main.run_scope.runner.awaiting_choice() \ + or main.run_scope.runner.next_candidate_ids().size() != 1: problems.append("入口停点状态失守(选路态 + 入口单候选)") else: main.level_screen.press_candidate(0) @@ -82,7 +82,7 @@ func _ready() -> void: else: main.level_screen.abandon_btn.pressed.emit() await get_tree().process_frame - if main.adventure == null or String(main.adventure.state) != "running": + if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running": problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") # ---- 既有断言(回归基线;A 区进度条断言随 停点前移批 2026-09-23 退役——节点链进度由关卡画面独立屏承载)---- if main.board_view.cells_probe().is_empty(): @@ -91,7 +91,7 @@ func _ready() -> void: problems.append("board 未装载") if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"): problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()]) - if main.settlement == null or main.settlement.battle_result != "": + if main.run_scope.settlement == null or main.run_scope.settlement.battle_result != "": problems.append("首战未处于进行中") if not main.log_view.parsed_text().contains("节点 1/"): problems.append("节点头日志缺失") @@ -193,7 +193,7 @@ func _ready() -> void: # ---- L2 单场段(debug_session 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)---- # 2026-09-13 单场用户入口已删——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线; # _reset_run_scope 复现原 _start_single 的提示去重复位) - main._reset_run_scope() + main._begin_session(false) main._debug.start_single(2) await get_tree().process_frame await get_tree().process_frame @@ -218,11 +218,15 @@ func _ready() -> void: main._on_wallet_changed("clear", 0, {}) if int(main._adventure_gp_final) != 57 or int(main._adventure_gp_view) != 0: problems.append("GP 影子账清零转存失败(final=%s view=%s)" % [str(main._adventure_gp_final), str(main._adventure_gp_view)]) - var meta_saved = main.meta - main.meta = null # 合成摘要不落结算(settle_adventure 会写隔离档→跨跑自污染),GP 行只验呈现 + var meta_saved = main.meta.save + for p2 in ["user://smoke_ui_probe_settle_scratch.json", "user://smoke_ui_probe_settle_scratch.json.tmp"]: + if FileAccess.file_exists(p2): + DirAccess.remove_absolute(p2) + main.meta.save = load("res://core/save_manager.gd").new("user://smoke_ui_probe_settle_scratch.json") + # 合成摘要不落隔离档(settle_adventure 写盘改道 scratch——跨跑自污染断根),GP 行只验呈现 main._on_adventure_finished({"result": "completed", "cleared": true, "levels_cleared": 3, "stopped_at": 0, "keystone_total": 30, "master_seed": 42}) - main.meta = meta_saved + main.meta.save = meta_saved if not main._end_overlay.is_open(): problems.append("冒险终局面板未开(合成摘要路径)") elif not main._end_overlay._gp_row.visible or not main._end_overlay._gp_val.text.contains("57"): diff --git a/game/smoke/ui_struct_probe.gd b/game/smoke/ui_struct_probe.gd index e2e479e2..5a388ea8 100644 --- a/game/smoke/ui_struct_probe.gd +++ b/game/smoke/ui_struct_probe.gd @@ -167,7 +167,7 @@ func _ready() -> void: ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_resume", true) main.save_manager.set_breakpoint(_resume_bp_fixture()) main._refresh_resume_button() - main.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) + main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) main._resume_from_menu() _settle(8) if not main._end_overlay.visible: @@ -181,7 +181,7 @@ func _ready() -> void: if not s.contains("合计 24"): problems.append("[resume] _gp_summary_text runner=null 守卫未取快照 gp(%s)" % s) # 恢复现场(resume 改写了 main 的 adventure/save 态——后续无屏依赖,仅为整洁) - main._level_active = false + main.run_scope.level_active = false # ---- 判定 ---- if problems.is_empty(): diff --git a/game/smoke/ui_tune_probe.gd b/game/smoke/ui_tune_probe.gd index 7938229b..a897d779 100644 --- a/game/smoke/ui_tune_probe.gd +++ b/game/smoke/ui_tune_probe.gd @@ -99,14 +99,14 @@ func _ready() -> void: # ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——DS_DEBUG 会话默认开态下开局不带窗,对局中经角标再开) if main.debug_overlay.visible: problems.append("开冒险路径未显式关浮窗(T2 防护失守)") - if int(main.adventure.master_seed) != 42: - problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.adventure.master_seed)) + if int(main.run_scope.adventure.master_seed) != 42: + problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed)) # 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 if main.level_screen.visible: main.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame - var hero: Dictionary = main.settlement.presentation_snapshot().hero + var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero if int(hero.get("atk", -1)) != boosted_atk or int(hero.get("max_hp", -1)) != boosted_hp: problems.append("战斗加成单次生效违例(atk=%s max_hp=%s,期望 %d/%d)" % [ str(hero.get("atk", "?")), str(hero.get("max_hp", "?")), boosted_atk, boosted_hp]) @@ -132,19 +132,19 @@ func _ready() -> void: problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)") # 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算) var rd: Dictionary = main.debug_overlay.run_data_probe() - var r = main.adventure.runner() + var r = main.run_scope.adventure.runner() if String(rd.get("seed", "")) != "42": problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed"))) - if int(rd.get("level", -1)) != int(main.adventure.level_index): - problems.append("当局数据关卡失对(%s ≠ %s)" % [str(rd.get("level")), str(main.adventure.level_index)]) + if int(rd.get("level", -1)) != int(main.run_scope.adventure.level_index): + problems.append("当局数据关卡失对(%s ≠ %s)" % [str(rd.get("level")), str(main.run_scope.adventure.level_index)]) if int(rd.get("node", -999)) != int(r.node_index): problems.append("当局数据节点失对(%s ≠ %s)" % [str(rd.get("node")), str(r.node_index)]) if String(rd.get("hp", "")) != "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())]: problems.append("当局数据 HP 失对(%s)" % str(rd.get("hp"))) if int(rd.get("gp", -1)) != int(r.gp): problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)]) - if int(rd.get("keystone", -1)) != int(main.adventure.keystone): - problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.adventure.keystone)]) + if int(rd.get("keystone", -1)) != int(main.run_scope.adventure.keystone): + problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.keystone)]) var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix") if close_btn == null: problems.append("浮窗关闭钮缺失(真实收窗路径中断)") @@ -161,14 +161,14 @@ func _ready() -> void: main.debug_overlay.seed_edit.text = "777" if main._seed_value() != 777: problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)") - var fresh_sm = load("res://core/save_manager.gd").new("user://smoke_tune_probe_seed.json") - fresh_sm.load_from_disk() # 全新空档——first_adventure_done=false(首局态;新档缺省面) - var keep_sm = main.save_manager - main.save_manager = fresh_sm + # 迭代 27 T1——SaveManager 升格 autoload 后换注改原地 attach(首局态挂载+恢复隔离档); + # --script 模式无裸 autoload 标识符——经 main 节点路径解析 + var sm = main.get_node("/root/SaveManager") + sm.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面) main.debug_overlay.seed_edit.text = "" if main._seed_value() != 42: problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") - main.save_manager = keep_sm # 恢复隔离档(非首局态) + sm.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位) var draws: Array[int] = [] for i in range(4): draws.append(main._seed_value()) diff --git a/game/tests/unit/core/treasure_effects_test.gd.uid b/game/tests/unit/core/treasure_effects_test.gd.uid new file mode 100644 index 00000000..e0d1e1ed --- /dev/null +++ b/game/tests/unit/core/treasure_effects_test.gd.uid @@ -0,0 +1 @@ +uid://dwsud4v5wh2av diff --git a/game/tests/unit/core/tree_requires_level_test.gd.uid b/game/tests/unit/core/tree_requires_level_test.gd.uid new file mode 100644 index 00000000..597866db --- /dev/null +++ b/game/tests/unit/core/tree_requires_level_test.gd.uid @@ -0,0 +1 @@ +uid://c61pxo55ok5ph diff --git a/game/tests/unit/presentation/pacing_queue_test.gd b/game/tests/unit/presentation/pacing_queue_test.gd index 3287d9a7..ef0f17e8 100644 --- a/game/tests/unit/presentation/pacing_queue_test.gd +++ b/game/tests/unit/presentation/pacing_queue_test.gd @@ -19,19 +19,17 @@ func _init() -> void: var main: Node = load("res://presentation/main.tscn").instantiate() root.add_child(main) await process_frame - var iso = load("res://core/save_manager.gd").new("user://test_pacing_queue_save.json") + # 隔离换注(迭代 27 T1——SaveManager 升格 autoload 后改原地 attach:先删后建防陈旧档泄入, + # 读盘在 attach 内完成;meta 经 save 引用透传零重建) for p in ["user://test_pacing_queue_save.json", "user://test_pacing_queue_save.json.tmp"]: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) - iso = load("res://core/save_manager.gd").new("user://test_pacing_queue_save.json") - iso.load_from_disk() - iso.mark_first_adventure_done() - main.save_manager = iso - main.meta = load("res://core/meta_progression.gd").new(main.db, iso) + var sm = main.get_node("/root/SaveManager") + sm.attach_path("user://test_pacing_queue_save.json") + sm.mark_first_adventure_done() main._refresh_resume_button() # 行信号计数器(真实信号路径旁挂计数——零触碰既有接线) var rows_seen: Array = [] - main.adventure = null main._start_adventure_from_menu() await process_frame # 入口停点选路进首战(真实停点链) @@ -39,11 +37,11 @@ func _init() -> void: _problems.append("入口停点未呈现(前置失守)") _finish() return - main.runner.pick_next_node(int(main.runner.next_candidate_ids()[0])) + main.run_scope.runner.pick_next_node(int(main.run_scope.runner.next_candidate_ids()[0])) await process_frame # 等首战开打(board_changed 同步链——两帧余量) await process_frame - var battle = main.runner.current_battle() + var battle = main.run_scope.runner.current_battle() if battle == null: _problems.append("首战未开打(排播断言无载体)") _finish()