feat(迭代27 T1): 波一 场景拆分+局生命周期物化——逐位一致对拍全同

P0-1 三屏场景化(契约2 只拆不重写):
- menu_screen/camp_screen/adventure_screen 三 .tscn 屏壳+壳脚本(视图代码原样构建,
  40+ views 零 .tscn 化);adventure 屏壳内含 level_map/battle 两容器
- main.gd _build_ui 解体=三屏实例化+模态装配+接线;组合根句柄全部保留(探针挂载
  假设 main.X 一级路径零迁移);树序=屏壳在前全模态之下(视觉共存对逐对验算)
- 屏壳根 mouse_filter=IGNORE(ui_input 实证:全屏 STOP 壳吃下层模态鼠标输入)

P0-2 会话物化(裁点①甲 autoload+手工清单消亡):
- run_scope.gd 节点持全部局态(adventure/runner/settlement/7+ 局态旗/账本);
  _begin_session 建会话、终局整节点 queue_free()——_reset_run_scope 三项+
  _on_battle_end_continue 置空四件手工清单删除
- SaveManager/MetaProgression 升格 autoload 3/4 号位(类名加 Core 后缀沿 RngService
  先例;--script 模式编译器不解析裸 autoload 标识符——组合根留读视图属性 get_node 形态)
- 探针隔离改 attach_path 原地重置(set_master_seed 先例);10 探针+pacing 测试迁移

验收(V1 核心=逐位一致对拍):
- parity_anchor 三变体 base/attune/shop×40 局 RUNSUM+JSONL 双面逐位全同(零行为漂移)
- 冒烟 1055→1077/0(+V2 屏壳独立实例化 11 断言+V3 两连局复位 11 断言)
- run_all 25 套+probe click 全绿+render_gate golden 0.00%×3
This commit is contained in:
DKChan committed 2026-10-14 20:28:53 +08:00
1 parent fc7fee03bd
commit 7a0d57ddb0
30 files changed
+683 -364

No files matched your search

+15 -4
View File
@@ -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()
+18 -2
View File
@@ -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"):
+266 -250
View File
File diff suppressed because it is too large. Load diff
+24
View File
@@ -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 账本收口波将退役)
+1
View File
@@ -0,0 +1 @@
uid://d05dkfqnbqdlx
@@ -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)
@@ -0,0 +1 @@
uid://csw5cl1xa7qu7
@@ -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")
+14
View File
@@ -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)
@@ -0,0 +1 @@
uid://dw1k6rn5w8gso
+12
View File
@@ -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")
+31
View File
@@ -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
@@ -0,0 +1 @@
uid://dqijp10rs1kaj
+12
View File
@@ -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")
+3 -7
View File
@@ -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,
}
@@ -0,0 +1 @@
uid://dd071t8g8cjb3
+2
View File
@@ -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]
+2 -2
View File
@@ -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
+12 -3
View File
@@ -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 名 = <tag>_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:
+118
View File
@@ -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
+7 -7
View File
@@ -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 耗尽/停手→五行段同步跑完→行全在队)
+42 -42
View File
@@ -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" % [
+14 -14
View File
@@ -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——契约复原语义显式重算)")
+1 -1
View File
@@ -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])
+12 -8
View File
@@ -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"):
+2 -2
View File
@@ -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():
+13 -13
View File
@@ -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())
@@ -0,0 +1 @@
uid://dwsud4v5wh2av
@@ -0,0 +1 @@
uid://c61pxo55ok5ph
@@ -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()