merge: 迭代13工程债与测试基建批落籍(D1探针三态+D2只读出口+D3 ArmedTimer+D4拆件1119+T5判不合+D5四件+D6爆破工兵包启用——六条目销账+P1甲/P2甲落文;worktree m4iter13-debt-20261004)

This commit is contained in:
DKChan committed 2026-10-04 08:35:09 +08:00
commit eff8cb8792
23 files changed
+799 -396

No files matched your search

+17 -15
View File
@@ -46,9 +46,11 @@
**现状**:同调批 T6 前置半日影响评估(P5 甲)破面判定=**大**:中立移除格态触排雷玩法(第三格态「已移除」——挖掘/插旗/和弦/邻接数字/清空判定全要问它)+战斗结算(完美排雷读数与旗判定基线需定义移除格语义)+清图判定(移除安全格改分母——BoardData 清空判定+MC 清图率读数口径)三面,且底稿欠规格两处口径(被移除雷格是否仍计入邻接数字/移除格数字呈现)实做需追加拍板。用户裁(2026-10-03):拆后批+blast_kit 行标 deferred 留位(effect_kinds cell_removal 已在 schema 枚举、行位保留,后批立项时换装 form=active_charged 即启)。后批开工前置=两处口径拍板+三面改动面任务书。 **现状**:同调批 T6 前置半日影响评估(P5 甲)破面判定=**大**:中立移除格态触排雷玩法(第三格态「已移除」——挖掘/插旗/和弦/邻接数字/清空判定全要问它)+战斗结算(完美排雷读数与旗判定基线需定义移除格语义)+清图判定(移除安全格改分母——BoardData 清空判定+MC 清图率读数口径)三面,且底稿欠规格两处口径(被移除雷格是否仍计入邻接数字/移除格数字呈现)实做需追加拍板。用户裁(2026-10-03):拆后批+blast_kit 行标 deferred 留位(effect_kinds cell_removal 已在 schema 枚举、行位保留,后批立项时换装 form=active_charged 即启)。后批开工前置=两处口径拍板+三面改动面任务书。
**归属**:后批(迭代 12 或独立 mini-loop,与反制批/双层 BD 批次排序待用户发令) **两口径裁决落文(2026-10-04 用户,任务书 v1.2 P1/P2)**:**P1 甲**=被移除雷格仍计入邻接数字——邻接数字=原始雷数历史真相冻结(digit_of 静态板值零重算);**P2 甲**=移除格保留原数字、视觉退色降档(CellUnit 呈现态,暗淡记忆态同族)。**实施归属 P3 甲=迭代 13 工程债与测试基建批随批做完**(超推荐案乙系知情裁决)。
**状态**:待立项(拆后批) **归属**:~~后批(迭代 12 或独立 mini-loop)~~→迭代 13 工程债与测试基建批(P3 甲裁 2026-10-04)
**状态**:**已落籍销账(2026-10-04,迭代 13 T7+T8,worktree m4iter13-debt-20261004 提交 89ed0d1)**——blast_kit 换装(form=active_charged+slot_cost 1+charges 2+effect_kinds 挂 cell_removal+清 deferred);cell_removal 语义=移除集仅「未揭示·无旗·是雷」→清图分母/连锁/C_dig 模拟天然零涉(判定逻辑零改动,P1 甲兑付),交互守卫两点(dig/flag sub=target_removed 零消耗拒绝);呈现=CellUnit removed_dim 退色暗幕(P2 甲)+_diggable 排除;充能编排零新代码(表驱动+营地同调位装配+use_aux consume 扣减全现成);MC 口径注记=换装后 form 非空被 transitional_rows 过滤,模拟投放池零涉零漂移(静态证明零跑测)。闸=冒烟 718/0+run_all 15+click 三种子+探针绿。
## 战斗循环 ## 战斗循环
@@ -235,9 +237,9 @@
**现状**:ui_click_probe 现行断言基于短局夹具,未适配完整局——结算演播硬锁 v3.1(锁期点击缓存+演播完重放)落地后,探针的合成点击时序与硬锁交互存在语义差(perf 审计 2026-10-02 实测双态全红归 T17 在飞批,落籍后未重适配)。需把探针点击注入改为完整局语境(停点/锁期/重放三态断言)。 **现状**:ui_click_probe 现行断言基于短局夹具,未适配完整局——结算演播硬锁 v3.1(锁期点击缓存+演播完重放)落地后,探针的合成点击时序与硬锁交互存在语义差(perf 审计 2026-10-02 实测双态全红归 T17 在飞批,落籍后未重适配)。需把探针点击注入改为完整局语境(停点/锁期/重放三态断言)。
**归属**:M-4 迭代 11 窗口评估(测试基建,与奖励重构读数批同窗排序) **归属**:~~M-4 迭代 11 窗口评估~~(**归属修订 2026-10-04**:迭代 11 已落籍,归属失效改判迭代 13 工程债与测试基建批——任务书 §一)
**状态**:待立项(挂账勿重开——2026-10-02 出口段定性,本条为补登记) **状态**:**已落籍销账(2026-10-04,迭代 13 T1,worktree m4iter13-debt-20261004 提交 cd7d3ea)**——探针分派链序修正(shop 停点先于 level_screen,迭代 11 商店房双 visible 实证烧预算根因)+停点/锁期/重放三态断言(_locktest_tick 脚本化:旗布尔=恰一次送达探测器)+PROBE_SEED 三种子绿(7/11/23)+演播期豁免保留注记;冒烟 718/0+run_all 15+t17_defer 绿。
#### 资产升档重产批(art-bible v3.0 中修挂账③,2026-09-29 登记) #### 资产升档重产批(art-bible v3.0 中修挂账③,2026-09-29 登记)
@@ -302,38 +304,38 @@
**现状**:三处武装确认语义异构(AP 条 chips+4s tween 自动回落 / 选路页放弃钮 3s _process 倒计时+危险红呼吸+减动效静态变体 / 战斗总结结算框纯载荷暂存无计时无视觉)——保语义收口需可插拔视觉+双计时机制+可选脉冲,共享件本体 40-60 行而每处仅省约 6 行,且动 press_confirm_for_probe/confirm_visible 探针语义,修复批 T4 降级不做。来源:代码质量审计(ap_bar_view/level_map_screen/main 三份手抄) **现状**:三处武装确认语义异构(AP 条 chips+4s tween 自动回落 / 选路页放弃钮 3s _process 倒计时+危险红呼吸+减动效静态变体 / 战斗总结结算框纯载荷暂存无计时无视觉)——保语义收口需可插拔视觉+双计时机制+可选脉冲,共享件本体 40-60 行而每处仅省约 6 行,且动 press_confirm_for_probe/confirm_visible 探针语义,修复批 T4 降级不做。来源:代码质量审计(ap_bar_view/level_map_screen/main 三份手抄)
**归属**:表现层打磨批(M-4 窗口) **归属**:~~表现层打磨批(M-4 窗口)~~→迭代 13 工程债批(P4 乙裁 2026-10-04)
**状态**:待办(2026-10-02 T4 降级登记,做了半套不如不做) **状态**:**已落籍销账(2026-10-04,迭代 13 T3,worktree m4iter13-debt-20261004 提交 fec3064)**——P4 乙=ArmedTimer helper(武装/回落/超时/减动效档四能力,motion_still 档判单点)三处计时机制收口:AP 条 tween 秒表→timer(三探针缝语义等价零动)、放弃钮 _process 倒计时→timer.tick+elapsed 相位(1Hz 呼吸视觉零变化)、结算框裸布尔→挂起态属性化(窗口 0);视觉三处各自保留;ui_click_probe :513-515 零改全绿实证。
#### main.gd 组合根二期瘦身(1237 → ~900) #### main.gd 组合根二期瘦身(1237 → ~900)
**现状**:T3 已迁排播泵/aux 两步交互/GP 影子账三件;到 ~900 还差三选一与替换子交互编排(约 115 行)、debug 模式接线、营地/商店投影 handler 等家族。来源:T3 执行报告 **现状**:T3 已迁排播泵/aux 两步交互/GP 影子账三件;到 ~900 还差三选一与替换子交互编排(约 115 行)、debug 模式接线、营地/商店投影 handler 等家族。来源:T3 执行报告。**行数基线校正(2026-10-04)**:1237 系 T3 拆后读数——开工日实测 1378(迭代 11/12 增量),批末 **1119**(净减 259)。
**归属**:下一表现层结构批 **归属**:~~下一表现层结构批~~→迭代 13 工程债批
**状态**:待办(2026-10-02 登记,ADR-0001 接线留组合根前提不变) **状态**:**已落籍销账(2026-10-04,迭代 13 T4,worktree m4iter13-debt-20261004 提交 4c79310)**——家族①=bonus_interact_controller.gd(241 行,三选一/替换子编排+TP0 复合键卡片装配)、家族③=camp_shop_presenter.gd(172 行,营地/商店投影与四写口);接线留组合根(ADR-0001,十三信号组合根接至 on_* 入口,meta/save_manager/runner 现值回调注入——探针 isolate_save 换注实证);家族②debug 接线=契约 3 组合根绑定面不迁移(节头注记记档)。**~900±50 锚差归因**:三家族可迁清单实测上限 ~336 行(任务书自估 315),锚位与族级清单不自洽——1333−315+胶水≈1018 已高于锚,净减 259 为清单内全量;再深切属新立项面。闸=冒烟 718/0 断言链全绿+三种子 click+run_all 15。
#### aux 替换子交互容器操作二期合并 #### aux 替换子交互容器操作二期合并
**现状**:通关渠道(carry 序号寻址+hp 账本收尾)与节点渠道(库存 handle 寻址+node 收尾)已合共用前置谓词,丢弃/授予/收尾三步仍分轨——强合需适配层,收益低。来源:T5 执行报告保守注记 **现状**:通关渠道(carry 序号寻址+hp 账本收尾)与节点渠道(库存 handle 寻址+node 收尾)已合共用前置谓词,丢弃/授予/收尾三步仍分轨——强合需适配层,收益低。来源:T5 执行报告保守注记
**归属**:随同调位实施批(attunement)顺路评估 **归属**:~~随同调位实施批(attunement)顺路评估~~(**归属变更 2026-10-04**:同调批已落籍未做此评估,改判迭代 13 工程债批——任务书 §一)
**状态**:待办 **状态**:**评估收口·判不合(2026-10-04,迭代 13 T5,合法出口非缩水)**——三轴语义相异(寻址 carry 序号 vs 库存句柄/持久域 跨关 vs 单关/收尾 断点快照 vs 不载),原实现已带三处「保守不合注记在案」(adventure_runner :479/:495/:566/:676);强合需适配层且触碰核心 RNG 邻接重构(契约 1 高危区)换 ~30-50 行收益。零代码,零 MC。
#### 探针白盒私有域访问改 *_probe() 只读出口 #### 探针白盒私有域访问改 *_probe() 只读出口
**现状**:ui_input_probe 等直入 BoardView._cells/_diggable/_layout 等私有域(重构即碎),fx_probe 已有只读出口先例。来源:代码质量审计(登记债务,修复批边界外) **现状**:ui_input_probe 等直入 BoardView._cells/_diggable/_layout 等私有域(重构即碎),fx_probe 已有只读出口先例。来源:代码质量审计(登记债务,修复批边界外)
**归属**:探针基建批 **归属**:~~探针基建批~~→迭代 13 工程债批
**状态**:待办 **状态**:**已落籍销账(2026-10-04,迭代 13 T2,worktree m4iter13-debt-20261004 提交 2241778)**——BoardView cells_probe/diggable_probe/hover_probe/fog_params_probe+level_map_screen layout_probe/canvas_probe 六只读出口(fx_probe 先例形态);直入点改写 26 处(任务书盘点 ~22+断言消息实参隐性直读 4——_hover 1/_fog_lantern·_fog_enabled 2/_cells 消息实参 3 内含);grep 复核零残留;06 原条目「BoardView._layout」系笔误(_layout/_canvas 属 level_map_screen)随批勘正。
#### 杂项尾差(一批打包) #### 杂项尾差(一批打包)
adventure_table.tres 补 bonus_candidate_count 字段落档(T5 注:脚本默认 3=现行值,落档属结构改动);grant_aux_item 返回值接留痕同型件 level_runner :438 区(T5 注);README.md:402 提及已删 _autoplay_battle_perfect 旧名(T6 注);critical-paths.md:6「切片 96/96」过期字样(T7 注);hero_info_panel_view:29 散文提及已删 PANEL_HERO 色名(T7 注)。来源:T4-T7 执行报告遗留观察 adventure_table.tres 补 bonus_candidate_count 字段落档(T5 注:脚本默认 3=现行值,落档属结构改动);grant_aux_item 返回值接留痕同型件 level_runner :438 区(T5 注);README.md:402 提及已删 _autoplay_battle_perfect 旧名(T6 注);critical-paths.md:6「切片 96/96」过期字样(T7 注);hero_info_panel_view:29 散文提及已删 PANEL_HERO 色名(T7 注)。来源:T4-T7 执行报告遗留观察
**归属**:下一清扫批顺路 **归属**:~~下一清扫批顺路~~→迭代 13 工程债批
**状态**:待办 **状态**:**已落籍销账(2026-10-04,迭代 13 T6,worktree m4iter13-debt-20261004 提交 d78b8d4)**——首件 adventure_table.tres bonus_candidate_count 核证已落档销账(迭代 7-11 间落地,字段在档/schema 校验在位/消费点已读表值);余四件全清=grant_aux_item 节点渠道返回值接留痕(镜像 :580 防御兜底范式)/README 旧名 _autoplay_battle_perfect 改 _decide_act 分派口径/critical-paths「切片 96/96」换代 718/0 口径/hero_info_panel_view 死色名 PANEL_HERO 注记校正。
+1 -1
View File
@@ -401,7 +401,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://smoke/s
**落地变更**: **落地变更**:
- `auto_player.gd` 增完美策略求解器(`perfect_action`/`_deduce`):single + subset 推理不动点(A1 F1 `measure_inferability` 同族规则集,**去 safe_set oracle**——约束未知邻格含雷格,比生成期闸保守);**零盲挖**(只挖已证安全格 = 构造保证零踩雷,扫描末断言兜底违例 quit(1))· **已证雷格全标旗**(旗先于挖——轮末 weaken + 魔法 首次充能,雷上旗不阻连锁)· 卡死且资源可 → `b1.player_request_refresh()` 轮内刷新 · 否则停手(残 AP 1:1 入 RAP)。输入只经 A2 查询面(reveal_record/is_revealed/has_flag),**禁读棋盘雷区真值**——测推理上界,非全知作弊。core 层零改动; - `auto_player.gd` 增完美策略求解器(`perfect_action`/`_deduce`):single + subset 推理不动点(A1 F1 `measure_inferability` 同族规则集,**去 safe_set oracle**——约束未知邻格含雷格,比生成期闸保守);**零盲挖**(只挖已证安全格 = 构造保证零踩雷,扫描末断言兜底违例 quit(1))· **已证雷格全标旗**(旗先于挖——轮末 weaken + 魔法 首次充能,雷上旗不阻连锁)· 卡死且资源可 → `b1.player_request_refresh()` 轮内刷新 · 否则停手(残 AP 1:1 入 RAP)。输入只经 A2 查询面(reveal_record/is_revealed/has_flag),**禁读棋盘雷区真值**——测推理上界,非全知作弊。core 层零改动;
- `window_selection` 增 `heal_no_waste` 选项(缺口 < 治疗量不用;完美档启用)+ 完美档轮次门解除(round_gate=0); - `window_selection` 增 `heal_no_waste` 选项(缺口 < 治疗量不用;完美档启用)+ 完美档轮次门解除(round_gate=0);
- `level_autoplay.gd` 增 `policy` 参数(默认 "normal"——tune_sim_controller/defense_scan/level_debug 既有调用零改动);完美档驱动 `_autoplay_battle_perfect`(move/dig/flag 走 A2 settle_batch、refresh 走 B1 编排、连续拒绝 ≥3 停手防死循环);st 增 `mine_hits`/`refreshes` 计数(mine_hit 证据行埋点); - `level_autoplay.gd` 增 `policy` 参数(默认 "normal"——tune_sim_controller/defense_scan/level_debug 既有调用零改动);完美档经 `policy="perfect"` 由 `_decide_act` 分派承载(原专职驱动 `_autoplay_battle_perfect` 已随样板收口并入分派——move/dig/flag 走 A2 settle_batch、refresh 走 B1 编排、连续拒绝 ≥3 停手防死循环);st 增 `mine_hits`/`refreshes` 计数(mine_hit 证据行埋点);
- `level_monte_carlo.gd` 增第 3 CLI 参 `[policy]`(normal/perfect/both,缺省 normal 兼容既有文档命令)+ 策略/踩雷/刷新列;脚注 F5 带文案同步 [30,50] 带锚定稿。 - `level_monte_carlo.gd` 增第 3 CLI 参 `[policy]`(normal/perfect/both,缺省 normal 兼容既有文档命令)+ 策略/踩雷/刷新列;脚注 F5 带文案同步 [30,50] 带锚定稿。
**完美档基线(N=30 · 种子 4200–4229 · 耗时 597.6s)**:**0 加成通关率 100%**(弱策略基线档 60%)、通关剩余 HP 均 **47.4/50**、整关承伤均 3.5(p90 18.0)、普通场承伤 0.5%(带 ≤20%)、Boss 场 5.3%(带 ≤50%)、踩雷 0.00/局(断言 ✓)、刷新 0.23/局、跳战率 20%。+50%~+200% 全档位恒 100% 满血通关(+200% 承伤均 0.4)——**技能轴上界完全碾压现行曲线:F5 带与 F6 S/K 区间对完美档零分辨力**(区间模型无策略水平轴的结构性证实;calibration_gap 获得机制解释与定量上界)。 **完美档基线(N=30 · 种子 4200–4229 · 耗时 597.6s)**:**0 加成通关率 100%**(弱策略基线档 60%)、通关剩余 HP 均 **47.4/50**、整关承伤均 3.5(p90 18.0)、普通场承伤 0.5%(带 ≤20%)、Boss 场 5.3%(带 ≤50%)、踩雷 0.00/局(断言 ✓)、刷新 0.23/局、跳战率 20%。+50%~+200% 全档位恒 100% 满血通关(+200% 承伤均 0.4)——**技能轴上界完全碾压现行曲线:F5 带与 F6 S/K 区间对完美档零分辨力**(区间模型无策略水平轴的结构性证实;calibration_gap 获得机制解释与定量上界)。
+3 -1
View File
@@ -594,7 +594,9 @@ func _resolve_node_replace(discard_handle: int) -> bool:
push_error("[冒险结构] resolve_node_replace 拒绝——句柄不在持有集: %d" % discard_handle) push_error("[冒险结构] resolve_node_replace 拒绝——句柄不在持有集: %d" % discard_handle)
return false return false
for iid in incoming: for iid in incoming:
_runner.grant_aux_item(String(iid), "reward_node") var rec: Dictionary = _runner.grant_aux_item(String(iid), "reward_node")
if rec.is_empty():
push_warning("[冒险结构] 替换子道具授予被拒(防御兜底留痕): %s" % String(iid))
# 放弃新件(discard_handle < 0)= 原持有集不动、新件不获取 # 放弃新件(discard_handle < 0)= 原持有集不动、新件不获取
_finish_node_bonus("aux_item", incoming.size()) _finish_node_bonus("aux_item", incoming.size())
return true return true
+7
View File
@@ -632,6 +632,13 @@ func use_aux(handle: int, target: Vector2i) -> Dictionary:
# 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道) # 掘进蓄能兑付(迭代 11 同调批 T6——临时 AP 当轮生效,grant_ap 同 sapper_order 通道)
if not action_economy.grant_ap(int(v.magnitude)): if not action_economy.grant_ap(int(v.magnitude)):
return {"ok": false, "reason": "行动点加量被拒(相位防御)"} return {"ok": false, "reason": "行动点加量被拒(相位防御)"}
if kinds.has("cell_removal"):
# 爆破工兵包兑付(迭代 13 T8/D6——中立移除雷格:未揭示/无旗/是雷过滤归排雷玩法;
# P1 甲邻接数字冻结零重算、清图分母零涉(雷格不入分母);空集拒绝零消耗——
# consume 之前 return(拒绝面注记扩「③内零消耗分支」),拒绝 reason 可呈现)
var blasted: Array = board.remove_mines_in(v.resolved)
if blasted.is_empty():
return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除雷格(须未揭示且未插旗)"}
var rec: Dictionary = aux_inventory.consume(handle) var rec: Dictionary = aux_inventory.consume(handle)
# 同调主动件充能扣减(迭代 11 第①批 T6——共享账本直改:adventure/level/settlement 同一字典引用) # 同调主动件充能扣减(迭代 11 第①批 T6——共享账本直改:adventure/level/settlement 同一字典引用)
if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement":
+27
View File
@@ -20,6 +20,7 @@ var action_economy: ActionEconomy
var _board: BoardData = null var _board: BoardData = null
var _revealed := {} # Vector2i → {"kind": "safe"/"mine", "digit": int} var _revealed := {} # Vector2i → {"kind": "safe"/"mine", "digit": int}
var _flags := {} # Vector2i → true(棋盘单态;跨轮保留随图存续) var _flags := {} # Vector2i → true(棋盘单态;跨轮保留随图存续)
var _removed := {} # Vector2i → true(迭代 13 T8/D6——中立移除雷格集;随 install/retire 清空)
var _hero := Vector2i.ZERO var _hero := Vector2i.ZERO
var _spawn: Dictionary[Vector2i, bool] = {} var _spawn: Dictionary[Vector2i, bool] = {}
@@ -73,6 +74,26 @@ func clear_map_cleared() -> bool:
return safe_remaining() == 0 return safe_remaining() == 0
# ---- 中立移除(迭代 13 T8/D6——P1 甲:邻接数字=原始雷数冻结零重算;P2 甲:移除格呈现退色降档;
# 移除集=未揭示、无旗、是雷——旗判定基线零回溯;雷格不入清图分母/连锁/C_dig 模拟,判定逻辑零改动) ----
func remove_mines_in(cells: Array) -> Array:
var out: Array = []
for c in cells:
var cell: Vector2i = c
if not _board.in_bounds(cell) or is_revealed(cell) or has_flag(cell) or not _board.is_mine(cell):
continue
if _removed.has(cell):
continue
_removed[cell] = true
out.append(cell)
return out
func is_removed(c: Vector2i) -> bool:
return _removed.has(c)
# 未开格存在查询(aux 道具 usable 预判消费——全板扫描只读面) # 未开格存在查询(aux 道具 usable 预判消费——全板扫描只读面)
func has_unrevealed() -> bool: func has_unrevealed() -> bool:
for y in _board.height: for y in _board.height:
@@ -100,6 +121,7 @@ func install_board(b: BoardData, during_round := false) -> Dictionary:
_board = b _board = b
_revealed.clear() _revealed.clear()
_flags.clear() _flags.clear()
_removed.clear()
_spawn = {} _spawn = {}
for c in b.spawn_cells(): for c in b.spawn_cells():
_spawn[c] = true _spawn[c] = true
@@ -127,6 +149,7 @@ func retire_board(during_round := false) -> Dictionary:
_board = null _board = null
_revealed.clear() _revealed.clear()
_flags.clear() _flags.clear()
_removed.clear()
return _outcome(true, "executed", "", "") return _outcome(true, "executed", "", "")
@@ -303,6 +326,8 @@ func _board_face_dig(c: Vector2i) -> String:
return "not_adjacent" return "not_adjacent"
if is_revealed(c): if is_revealed(c):
return "target_revealed" return "target_revealed"
if _removed.has(c):
return "target_removed" # 迭代 13 T8/D6——中立移除格不可交互(拒绝先于消耗,pattern 1 board_face 腿)
# 旗不拦挖掘(插旗不可逆批 2026-09-19):有旗格直接挖 = 对承诺的兑现,兑付改判归 战斗结算。 # 旗不拦挖掘(插旗不可逆批 2026-09-19):有旗格直接挖 = 对承诺的兑现,兑付改判归 战斗结算。
return "" return ""
@@ -356,6 +381,8 @@ func _do_flag_place(c: Vector2i) -> Dictionary:
sub = "target_revealed" sub = "target_revealed"
elif has_flag(c): elif has_flag(c):
sub = "flag_present" sub = "flag_present"
elif _removed.has(c):
sub = "target_removed" # 迭代 13 T8/D6——中立移除格不可交互
if sub != "": if sub != "":
return _outcome(false, "rejected", "board_face", sub) return _outcome(false, "rejected", "board_face", sub)
if not action_economy.try_spend("flag_place"): if not action_economy.try_spend("flag_place"):
+5 -3
View File
@@ -5,7 +5,7 @@
[resource] [resource]
script = ExtResource("1") script = ExtResource("1")
; 迭代 11 第①批 T1(2026-10-03):追加七件同调行(form 非空——永不入过渡渠道);值全 provisional 归 难度曲线与Boss。 ; 迭代 11 第①批 T1(2026-10-03):追加七件同调行(form 非空——永不入过渡渠道);值全 provisional 归 难度曲线与Boss。
; nest_pendulum/blast_kit 维持过渡行,T7 渠道退役时原位换装 form=active_charged;sapper_order/显影尘/地穴全图卷/罗盘 T7 逐件销账。 ; nest_pendulum 已换装 active_charged(迭代 11 第①批);blast_kit 已换装 cell_removal+active_charged(迭代 13 T8/D6——中立移除雷格启用);sapper_order/显影尘/地穴全图卷/罗盘 T7 逐件销账。
rows = [{ rows = [{
"item_id": "probe_dust", "item_id": "probe_dust",
"display_name": "显影尘", "display_name": "显影尘",
@@ -57,11 +57,13 @@ rows = [{
"item_id": "blast_kit", "item_id": "blast_kit",
"display_name": "爆破工兵包", "display_name": "爆破工兵包",
"dimension_tags": ["action", "risk"], "dimension_tags": ["action", "risk"],
"effect_kinds": ["ap_grant", "mine_reveal"], "effect_kinds": ["ap_grant", "mine_reveal", "cell_removal"],
"target": "area_3x3", "target": "area_3x3",
"magnitude": 4, "magnitude": 4,
"use_phase": "active_dig", "use_phase": "active_dig",
"deferred": true "form": "active_charged",
"slot_cost": 1,
"charges_per_adventure": 2
}, { }, {
"item_id": "venom_edge", "item_id": "venom_edge",
"display_name": "淬毒刃纹", "display_name": "淬毒刃纹",
+47
View File
@@ -0,0 +1,47 @@
# 武装确认计时器(迭代 13 T3/D3——P4 乙裁决:只统一计时+脉冲机制,视觉三处各自保留)。
# 收口债:三处二次确认计时机制异构(AP 条弃图刷新=tween 秒表 / 选路放弃钮=_process 帧驱动
# 倒计时 / 战斗总结结算框=纯载荷暂存裸布尔)——本件消掉「计时机制三种实现」漂移源。
# 保缝纪律:AP 条三探针缝(press_refresh_for_probe/confirm_visible/press_confirm_for_probe)
# 语义等价零动;放弃钮真实二击路径零改;结算框挂起态经组合根属性继续可读写(探针接缝)。
# 减动效档:档判据单点 motion_still()——脉冲相位由视觉层取 elapsed 自算(1Hz 连续呼吸
# 视觉零变化),减动效档视觉层走静态呈现(放弃钮静态半透先例)。
class_name ArmedTimer
extends RefCounted
signal expired # 窗口到点回落边沿(仅 window_s > 0 时发出;宿主接回落视觉复位)
var window_s := 0.0 # 超时窗口秒(<=0 = 无窗口——挂起直到显式回落,结算框语义)
var elapsed := 0.0 # 武装起算累计秒(视觉层呼吸相位取数;tick 累计)
var _armed := false
# 档判据单点(减动效档四能力之一——两处视觉层共用,替代各自内联 UiKit.motion_scale 判断)
static func motion_still() -> bool:
return UiKit.motion_scale < 1.0
func arm(window_override := -1.0) -> void:
if window_override >= 0.0:
window_s = window_override
_armed = true
elapsed = 0.0
func disarm() -> void:
_armed = false
elapsed = 0.0
func is_armed() -> bool:
return _armed
# 帧驱动(宿主 _process 委托;unarmed 或无窗口时零开销直返——结算框挂起态无需接 tick)
func tick(delta: float) -> void:
if not _armed or window_s <= 0.0:
return
elapsed += delta
if elapsed >= window_s:
disarm()
expired.emit()
+1
View File
@@ -0,0 +1 @@
uid://4avok0unhaek
@@ -95,6 +95,7 @@ func attempt_use(handle: int, cell: Variant = null) -> void:
_use_selection = 0 _use_selection = 0
refresh_slots() refresh_slots()
refresh_overlays() refresh_overlays()
board_view.refresh() # 迭代 13 T8/D6——移除格即时落图(无移除态的件 diff 零变化幂等)
var arow: Dictionary = db.aux_item_row(String(v.get("item_id", ""))) var arow: Dictionary = db.aux_item_row(String(v.get("item_id", "")))
toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info") toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info")
@@ -0,0 +1,241 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 提升三选一与替换子交互控制器(迭代 13 T4/D4——二期家族① 自 main.gd 迁出;规范 1.1 单一职责):
# 三选一停点编排(presented→pick→confirm/refresh→resolved→continue)+ 同调位替换子态
#(presented→chosen)+ TP0 令牌池卡片文案装配("kind|tier|relic_id" 复合键)+ 工坊显示名投影。
# 装配纪律(ADR-0001,仿 aux_interact_controller 先例):信号连接不在此发生——bonus_overlay
# 四信号与 adventure 两信号由组合根接至本类 on_* 入口;模态栈/演播后排窗/呈现刷新/终局面/
# 推进失败 fatal 经组合根回调注入;adventure/runner 随局更替——现值回调注入;兑付写调用唯一
# 经 adventure.pick_bonus/apply_node_bonus/resolve_bonus_replace(core 侧单口,本类零直写)。
extends RefCounted
const Narrator := preload("res://presentation/battle_narrator.gd")
var db
var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面,迭代13 T4 实证)
var bonus_overlay: BonusPickOverlay
var end_overlay: BattleEndOverlay
var board_view: BoardView
var toast: ToastStack
var _save_get: Callable # () → 存档承载现值(同上——Attunement.slots 读数面)
var _adventure_get: Callable # () → 冒险结构现值(组合根 lambda——随局更替)
var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——reward_node 继续推进)
var _adventure_mode_get: Callable # () → 冒险模式标记现值(组合根 lambda)
var _open_modal: Callable # 模态栈单口(组合根)
var _open_modal_after_playback: Callable # 演播后排窗单口(T17——组合根)
var _close_modal: Callable
var _refresh_presentations: Callable
var _fatal_screen: Callable
var _gp_summary: Callable # () → 战斗总结 GP 行文案(组合根 lambda——_last_gain_amount 取数面在根)
var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路)
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷)
var _last_replace_incoming := {} # 同上——incoming 记录
func _init(db_ref, meta_get: Callable, bonus_overlay_ref: BonusPickOverlay, end_overlay_ref: BattleEndOverlay,
board_view_ref: BoardView, toast_ref: ToastStack, save_get: Callable,
adventure_get: Callable, runner_get: Callable, adventure_mode_get: Callable,
open_modal: Callable, open_modal_after_playback: Callable, close_modal: Callable,
refresh_presentations: Callable, fatal_screen: Callable, gp_summary: Callable) -> void:
db = db_ref
_meta_get = meta_get
bonus_overlay = bonus_overlay_ref
end_overlay = end_overlay_ref
board_view = board_view_ref
toast = toast_ref
_save_get = save_get
_adventure_get = adventure_get
_runner_get = runner_get
_adventure_mode_get = adventure_mode_get
_open_modal = open_modal
_open_modal_after_playback = open_modal_after_playback
_close_modal = close_modal
_refresh_presentations = refresh_presentations
_fatal_screen = fatal_screen
_gp_summary = gp_summary
# 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一)
func has_pending() -> bool:
return not _pending_bonus.is_empty()
# 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派)
func resume_if_pending() -> bool:
if _pending_bonus.is_empty():
return false
show_pick()
return true
# 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石
# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约)
func on_presented(payload: Dictionary) -> void:
if int(payload.get("aux_cleared", 0)) > 0:
toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
_pending_bonus = payload.duplicate(true)
_bonus_context = "level_clear"
if bool(payload.get("replace_pending", false)):
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
show_pick()
return
end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
board_view.get_global_rect(),
"%s\n%s" % [String(_gp_summary.call()), Narrator.bonus_keystone_line(payload)])
_open_modal_after_playback.call(end_overlay)
_refresh_presentations.call()
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态)
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面,
# 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。
func present_node_bonus() -> void:
var adventure = _adventure_get.call()
_bonus_context = "reward_node"
var node_draw: Dictionary = adventure.node_bonus_candidates()
var node_cands: Array = node_draw.get("candidates", []) as Array
_open_modal.call(bonus_overlay)
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array))
adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array)
bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态)
func show_pick() -> void:
var payload: Dictionary = _pending_bonus
_open_modal.call(bonus_overlay)
bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
var adventure = _adventure_get.call()
bonus_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂
_refresh_presentations.call()
func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array:
# TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|relic_id" 复合键(同 kind
# 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed)
var out: Array = []
var aux_i := 0
for tok in candidates:
var kind := String((tok as Dictionary).get("kind", ""))
var tier := String((tok as Dictionary).get("tier", ""))
var rid := String((tok as Dictionary).get("relic_id", ""))
var cid := "%s|%s|%s" % [kind, tier, rid]
var text := ""
if kind == "relic":
text = Narrator.relic_card_text(db.relic_row(rid))
elif kind == "aux_item":
var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else ""
aux_i += 1
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)), _aux_card_line(iid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)))
out.append({"id": cid, "text": text})
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)
func _aux_card_line(iid: String) -> String:
if iid == "":
return ""
var row: Dictionary = db.aux_item_row(iid)
if row.is_empty():
return ""
var parts: Array[String] = []
for k in (row.get("effect_kinds", []) as Array):
if String(k) == "mine_reveal":
parts.append("真视")
elif String(k) == "reward_preview":
parts.append("预读")
elif String(k) == "ap_grant":
parts.append("AP +%d" % int(row.get("magnitude", 0)))
var tgt := String(row.get("target", "none"))
if tgt == "area_3x3":
parts.append("3×3")
elif tgt == "board_wide":
parts.append("全图")
return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)]
# 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源;通关停点经重发 bonus_choice_presented 自然重建,
# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
func on_refresh() -> void:
var adventure = _adventure_get.call()
if adventure == null:
return
if not adventure.refresh_bonus():
return
if _bonus_context == "reward_node":
var st: Dictionary = adventure.node_bonus_pending_state()
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array))
bonus_overlay.set_refresh_info(adventure.attune_refresh_left())
func on_pick_confirmed(item_id: String) -> void:
var adventure = _adventure_get.call()
if adventure == null:
return
# TP0 令牌池——卡片 id "kind|tier|relic_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段)
var parts := item_id.split("|")
var kind := parts[0]
var tier := parts[1] if parts.size() > 1 else ""
var rid := parts[2] if parts.size() > 2 else ""
var ok: bool = adventure.apply_node_bonus(kind, tier, rid) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier, rid)
if not ok:
push_error("[bonus_interact] 三选一兑付被拒——见输出日志")
func on_aux_replace_presented(items: Array, incoming: Dictionary) -> void:
_last_replace_items = items.duplicate(true)
_last_replace_incoming = incoming.duplicate(true)
if not bonus_overlay.visible:
_open_modal.call(bonus_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律)
_open_replace(items, incoming)
func _open_replace(items: Array, incoming: Dictionary) -> void:
var named: Array = []
for it in items:
var rec: Dictionary = it.duplicate()
rec["display_name"] = String(item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", ""))))
named.append(rec)
bonus_overlay.open_replace(named, String(item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))),
named.size(), Attunement.slots(db, _save_get.call())) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役)
func on_replace_chosen(discard: int) -> void:
var adventure = _adventure_get.call()
if adventure == null or not adventure.resolve_bonus_replace(discard):
push_error("[bonus_interact] 同调位替换受理被拒——见输出日志")
# 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续)
func on_resolved(result: Dictionary) -> void:
_pending_bonus = {}
bonus_overlay.show_result(Narrator.bonus_result_text(result))
_refresh_presentations.call()
# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
func on_continue() -> void:
_close_modal.call(bonus_overlay)
bonus_overlay.close_pick()
var adventure = _adventure_get.call()
if _bonus_context == "reward_node":
var runner = _runner_get.call()
if runner != null:
runner.continue_after_node()
return
if bool(_adventure_mode_get.call()) and adventure != null:
if not adventure.continue_to_next_level():
_fatal_screen.call("关卡推进失败——见输出日志")
# 工坊 item_id → 显示名投影(keystone_settle_text/替换子命名文案数据源——拼句归 Narrator)
func item_name_lookup() -> Dictionary:
var lookup := {}
var meta = _meta_get.call()
if meta != null:
for row in db.meta_workshop_items():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup
@@ -0,0 +1 @@
uid://cp308xdkheml3
+172
View File
@@ -0,0 +1,172 @@
# VERTICAL SLICE - NOT FOR PRODUCTION
# 营地/商店投影控制器(迭代 13 T4/D4——二期家族③ 自 main.gd 迁出;规范 1.1 单一职责):
# 营地面开合/快照投影/锻造·同调·天赋树·Fork 购买与切换 + 商店房购买/离开(pool 与 product 双面)。
# 装配纪律(ADR-0001,仿 aux_interact_controller 先例):信号连接不在此发生——campbtn/
# camp_overlay 四信号/shop_overlay 两信号由组合根接至本类 on_* 入口;模态栈/呈现刷新/续玩按钮
# 刷新经组合根回调注入;runner 随局更替——现值回调注入;写调用唯一经 MetaProgression 购买原语
# 与 LevelRunner.buy_*(core 侧单口,本类零直写——拒绝可见 pattern 1 兜底 toast 保留原地)。
extends RefCounted
var db
var _meta_get: Callable # () → 元进度账本现值(组合根 lambda——探针 isolate_save 换注面,迭代13 T4 实证)
var _save_get: Callable # () → 存档承载现值(同上——营地 recent 摘要读数面)
var camp_overlay: CampOverlay
var shop_overlay: ShopOverlay
var toast: ToastStack
var start_overlay: Control # 开始层互斥面(开合仿 TunePanel 先例——非模态且树序在后,规范 2.3)
var logger # 试玩日志(ux_reject 取数面)
var _runner_get: Callable # () → 关卡推进器现值(组合根 lambda——随局更替)
var _open_modal: Callable
var _close_modal: Callable
var _refresh_presentations: Callable
var _refresh_resume_button: Callable
func _init(db_ref, meta_get: Callable, save_get: Callable, camp_overlay_ref: CampOverlay,
shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control,
logger_ref, runner_get: Callable, open_modal: Callable, close_modal: Callable,
refresh_presentations: Callable, refresh_resume_button: Callable) -> void:
db = db_ref
_meta_get = meta_get
_save_get = save_get
camp_overlay = camp_overlay_ref
shop_overlay = shop_overlay_ref
toast = toast_ref
start_overlay = start_overlay_ref
logger = logger_ref
_runner_get = runner_get
_open_modal = open_modal
_close_modal = close_modal
_refresh_presentations = refresh_presentations
_refresh_resume_button = refresh_resume_button
# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后,
# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账)
func open_camp() -> void:
var meta = _meta_get.call()
if meta == null:
return
meta.refresh_workshop_unlocks()
start_overlay.visible = false
_open_modal.call(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈)
camp_overlay.open_camp(camp_data())
_refresh_presentations.call()
func on_camp_closed() -> void:
_close_modal.call(camp_overlay) # 模态栈单口(T3 收口批)
start_overlay.visible = true
_refresh_resume_button.call() # 迭代 8 卡②——回菜单按断点在档态刷新
_refresh_presentations.call() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression)
func camp_data() -> Dictionary:
var meta = _meta_get.call()
var forge_rows: Array[Dictionary] = []
if meta != null:
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = db.meta_forge_row(attr)
forge_rows.append({
"attr": attr,
"display_name": String(row.get("display_name", "")),
"level": meta.forge_level(attr),
"flat": int(row.get("flat_value", 0)),
"price": meta.forge_price(attr),
})
return {
"balance": meta.keystone_balance() if meta != null else 0,
"forge": forge_rows,
"workshop": meta.workshop_status() if meta != null else [],
"attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影
"tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影
"fork_groups": meta.tree_fork_groups() if meta != null else [],
"recent": _save_get.call().recent_summaries() if _save_get.call() != null else [],
}
func on_camp_buy(attr: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
if attr == "attune_refresh":
# Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;按钮禁用为常态防线,兜底拒绝可见 pattern 1)
if not meta.buy_attune_refresh():
toast.show_toast("钥石不足——买不起这次升级", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
return
if not meta.buy_forge(attr):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
toast.show_toast("钥石不足——买不起这件强化", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
# 同调装配切换(迭代 11 第①批 T4——装配关系唯一写口经 MetaProgression.toggle_attunement;满位兜底拒绝可见)
func on_camp_attune_toggle(item_id: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
if not meta.toggle_attunement(item_id):
toast.show_toast("同调位已满——先卸下再装配", "warn")
return
camp_overlay.refresh_camp(camp_data())
# 天赋树购买(迭代 12 T9——写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧)
func on_camp_tree_buy(node_id: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
if not ok:
toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
# A/B 免费切换(迭代 12 T8——同组换侧零币;未激活组走购买面)
func on_camp_fork_toggle(group: String, side: String) -> void:
var meta = _meta_get.call()
if meta == null:
return
if not meta.tree_toggle_fork(group, side):
toast.show_toast("切换被拒——未激活组请先购买", "warn")
return
camp_overlay.refresh_camp(camp_data())
_refresh_presentations.call()
func on_shop_buy(product_id: String) -> void:
var runner = _runner_get.call()
if runner == null:
return
if runner.is_shop_pool():
# pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用)
if not runner.buy_pool_item(product_id):
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
toast.show_toast("金币不足——买不起这件商品", "warn")
return
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
_refresh_presentations.call()
return
if not runner.buy_product(product_id):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
toast.show_toast("金币不足——买不起这件商品", "warn")
return
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
_refresh_presentations.call()
func on_shop_leave() -> void:
_close_modal.call(shop_overlay)
var runner = _runner_get.call()
if runner != null:
# 复用兑付确认纪律(接口漂移清偿):离开 ≠ 推进——确认后经 choices_presented 弹出选路口
runner.continue_after_node()
@@ -0,0 +1 @@
uid://b3t0ocw10p5rs
+56 -307
View File
@@ -38,6 +38,8 @@ const DebugSessionScript := preload("res://presentation/debug_session.gd")
const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批) const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批)
const AuxInteractController := preload("res://presentation/aux_interact_controller.gd") # 魔法道具两步交互(T3 拆件批) const AuxInteractController := preload("res://presentation/aux_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 拆件批)
const BonusInteractController := preload("res://presentation/bonus_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 SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款)
const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律) const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律)
@@ -53,12 +55,18 @@ var _level_active := false
var _seed_edit: LineEdit var _seed_edit: LineEdit
var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮) var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮)
var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化) var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化)
var _settlement_stop_pending := false # 卡⑥ 战斗胜利总结结算框挂起态(确认「下一节点」后进选路页) 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 _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷)
var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷)
var _pending_bonus := {} # 三选一停点载荷暂存(迭代 10——战斗总结结算确认后开三选一面;续玩停点复用)
var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用) var bonus_overlay: BonusPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用)
var _bonus_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路)
var _start_overlay: Control var _start_overlay: Control
var _end_overlay: BattleEndOverlay var _end_overlay: BattleEndOverlay
var _inter_overlay: Control var _inter_overlay: Control
@@ -83,6 +91,8 @@ var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道
# T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui) # T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui)
var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列)
var _aux = null # 魔法道具两步交互(aux_interact_controller.gd——本类只接线与转发意图) var _aux = null # 魔法道具两步交互(aux_interact_controller.gd——本类只接线与转发意图)
var _bonus = null # 三选一与替换子交互(bonus_interact_controller.gd——本类只接线与回调注入)
var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发)
var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂) var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂)
# GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况) # GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况)
@@ -251,13 +261,6 @@ func _build_ui() -> void:
node_overlay.continue_pressed.connect(_on_node_continue) node_overlay.continue_pressed.connect(_on_node_continue)
level_screen.route_chosen.connect(_on_route_chosen) level_screen.route_chosen.connect(_on_route_chosen)
level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119) level_screen.abandon_requested.connect(_on_abandon_requested) # 放弃入口(2026-09-21 用户拍板——台账 119)
shop_overlay.buy_requested.connect(_on_shop_buy)
shop_overlay.leave_pressed.connect(_on_shop_leave)
camp_overlay.buy_requested.connect(_on_camp_buy)
camp_overlay.attune_toggled.connect(_on_camp_attune_toggle)
camp_overlay.tree_buy_requested.connect(_on_camp_tree_buy)
camp_overlay.tree_fork_toggled.connect(_on_camp_fork_toggle)
camp_overlay.closed.connect(_on_camp_closed)
_end_overlay.review_pressed.connect(_on_end_review) _end_overlay.review_pressed.connect(_on_end_review)
_end_overlay.continue_pressed.connect(_on_battle_end_continue) _end_overlay.continue_pressed.connect(_on_battle_end_continue)
tune_panel.closed.connect(_close_tune_panel) tune_panel.closed.connect(_close_tune_panel)
@@ -269,10 +272,17 @@ func _build_ui() -> void:
# 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进) # 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进)
bonus_overlay = BonusPickOverlay.new() bonus_overlay = BonusPickOverlay.new()
add_child(bonus_overlay) add_child(bonus_overlay)
bonus_overlay.pick_confirmed.connect(_on_bonus_pick_confirmed) # 提升三选一与替换子交互(T4/D4 拆件批——迁 bonus_interact_controller.gd;本类只接线与回调注入,
bonus_overlay.replace_chosen.connect(_on_bonus_replace_chosen) # ADR-0001——模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调)
bonus_overlay.continue_pressed.connect(_on_bonus_continue) _bonus = BonusInteractController.new(db, func() -> Variant: return meta, bonus_overlay, _end_overlay, board_view,
bonus_overlay.refresh_requested.connect(_on_bonus_refresh) board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return adventure,
func() -> Variant: return runner, func() -> Variant: return _adventure_mode,
_open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations,
_fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount}))
bonus_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed)
bonus_overlay.replace_chosen.connect(_bonus.on_replace_chosen)
bonus_overlay.continue_pressed.connect(_bonus.on_continue)
bonus_overlay.refresh_requested.connect(_bonus.on_refresh)
move_child(bonus_overlay, _end_overlay.get_index()) move_child(bonus_overlay, _end_overlay.get_index())
var start: Dictionary = UiAssembler.build_start_overlay(self, db) var start: Dictionary = UiAssembler.build_start_overlay(self, db)
@@ -284,7 +294,19 @@ func _build_ui() -> void:
start.tunebtn.pressed.connect(_show_tune_panel) start.tunebtn.pressed.connect(_show_tune_panel)
start.dbgbtn.pressed.connect(_start_debug_levels) start.dbgbtn.pressed.connect(_start_debug_levels)
start.dbgbtn.visible = OS.get_environment("DS_DEBUG") == "1" # 迭代 8 卡④——调试入口默认隐藏门控(DS_DEBUG=1 点亮) start.dbgbtn.visible = OS.get_environment("DS_DEBUG") == "1" # 迭代 8 卡④——调试入口默认隐藏门控(DS_DEBUG=1 点亮)
start.campbtn.pressed.connect(_open_camp) # 营地/商店投影(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,
_open_modal, _close_modal, _refresh_presentations, _refresh_resume_button)
shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy)
shop_overlay.leave_pressed.connect(_camp_pres.on_shop_leave)
camp_overlay.buy_requested.connect(_camp_pres.on_camp_buy)
camp_overlay.attune_toggled.connect(_camp_pres.on_camp_attune_toggle)
camp_overlay.tree_buy_requested.connect(_camp_pres.on_camp_tree_buy)
camp_overlay.tree_fork_toggled.connect(_camp_pres.on_camp_fork_toggle)
camp_overlay.closed.connect(_camp_pres.on_camp_closed)
start.campbtn.pressed.connect(_camp_pres.open_camp)
start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口
start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单
@@ -427,9 +449,9 @@ func _wire_adventure() -> void:
adventure.choices_presented.connect(_on_choices_presented) adventure.choices_presented.connect(_on_choices_presented)
adventure.wallet_changed.connect(_on_wallet_changed) adventure.wallet_changed.connect(_on_wallet_changed)
# 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线 # 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线
adventure.bonus_choice_presented.connect(_on_bonus_presented) adventure.bonus_choice_presented.connect(_bonus.on_presented)
adventure.bonus_resolved.connect(_on_bonus_resolved) adventure.bonus_resolved.connect(_bonus.on_resolved)
adventure.aux_replace_presented.connect(_on_aux_replace_presented) adventure.aux_replace_presented.connect(_bonus.on_aux_replace_presented)
adventure.adventure_finished.connect(_on_adventure_finished) adventure.adventure_finished.connect(_on_adventure_finished)
adventure.level_started.connect(_on_adventure_level_started) adventure.level_started.connect(_on_adventure_level_started)
adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径)
@@ -482,15 +504,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
_show_board_fx_for_node(t) _show_board_fx_for_node(t)
log_view.append_bb(Narrator.node_banner(runner, node, t)) log_view.append_bb(Narrator.node_banner(runner, node, t))
elif t == "reward": elif t == "reward":
# 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面, # 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;BonusPickOverlay 共用面)。
# 道具件满位替换子交互同构)。原「魔法道具投放渠道」三选一(迭代 7 过渡形态)随批退役。 # T4/D4 拆件:呈现编排归 bonus_interact_controller.present_node_bonus(_bonus_context 随迁)。
_bonus_context = "reward_node" _bonus.present_node_bonus()
var node_draw: Dictionary = adventure.node_bonus_candidates()
var node_cands: Array = node_draw.get("candidates", []) as Array
_open_modal(bonus_overlay)
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(node_cands, node_draw.get("aux_detail", []) as Array))
adventure.present_node_bonus(node_cands, node_draw.get("aux_detail", []) as Array)
bonus_overlay.set_refresh_info(adventure.attune_refresh_left()) # 迭代 11 第①批 T5b——刷新余量投喂
log_view.append_bb(Narrator.reward_line()) log_view.append_bb(Narrator.reward_line())
elif t == "rest": elif t == "rest":
var healed: int = int(info.get("healed", 0)) var healed: int = int(info.get("healed", 0))
@@ -513,151 +529,6 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void:
# ---- 冒险模式终局面(三选一选择面 / 冒险结算屏——迭代 10 奖励重构批)---- # ---- 冒险模式终局面(三选一选择面 / 冒险结算屏——迭代 10 奖励重构批)----
# 三选一停点(非末关通关恰一次):Boss 胜利先经战斗总结结算面(settlement-summary v1.1——钥石
# 增量行在其上),确认后进三选一面(level-clear.md v2);续玩停点恢复同入口(零自动推进契约)
func _on_bonus_presented(payload: Dictionary) -> void:
if int(payload.get("aux_cleared", 0)) > 0:
board_zone.toast.show_toast("上关剩余 %d 件魔法道具已随关卡结束清空" % int(payload.get("aux_cleared", 0)), "info") # 迭代 7 T4 清空反馈
_pending_bonus = payload.duplicate(true)
_bonus_context = "level_clear"
if bool(payload.get("replace_pending", false)):
# 续玩:替换子交互在途中断——重建三选一面后直落替换子态(候选不重抽)
_show_bonus_pick()
return
_end_overlay.open_victory(Narrator.battle_end_text("victory", false), "继续",
board_view.get_global_rect(),
"%s\n%s" % [_gp_summary_text({"gp_dropped": _last_gain_amount}), Narrator.bonus_keystone_line(payload)])
_open_modal_after_playback(_end_overlay)
_refresh_presentations()
# 三选一面开出(战斗总结结算确认 / 续玩停点恢复;替换在途由 aux_replace_presented 续发直落子态)
func _show_bonus_pick() -> void:
var payload: Dictionary = _pending_bonus
_open_modal(bonus_overlay)
bonus_overlay.open_pick(Narrator.bonus_pick_header(payload), _bonus_card_texts(payload.get("candidates", []) as Array, payload.get("aux_detail", []) as Array))
bonus_overlay.set_refresh_info(adventure.attune_refresh_left() if adventure != null else 0) # 迭代 11 第①批 T5b——刷新余量投喂
_refresh_presentations()
var _last_replace_items: Array = [] # 替换子态续玩重建缓存(aux_replace_presented 载荷)
var _last_replace_incoming := {} # 同上——incoming 记录
func _bonus_card_texts(candidates: Array, aux_detail: Array = []) -> Array:
# TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|relic_id" 复合键(同 kind
# 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(_on_bonus_pick_confirmed)
var out: Array = []
var aux_i := 0
for tok in candidates:
var kind := String((tok as Dictionary).get("kind", ""))
var tier := String((tok as Dictionary).get("tier", ""))
var rid := String((tok as Dictionary).get("relic_id", ""))
var cid := "%s|%s|%s" % [kind, tier, rid]
var text := ""
if kind == "relic":
text = Narrator.relic_card_text(db.relic_row(rid))
elif kind == "aux_item":
var iid := String(aux_detail[aux_i]) if aux_i < aux_detail.size() else ""
aux_i += 1
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)), _aux_card_line(iid))
else:
text = Narrator.bonus_card_text(kind, tier, int(db.adventure.bonus_value(kind, tier)))
out.append({"id": cid, "text": text})
return out
# 道具件卡片行(掷定前置——具体道具名 + 效果摘要;空 iid = 无预掷回退态显示通用文案)
func _aux_card_line(iid: String) -> String:
if iid == "":
return ""
var row: Dictionary = db.aux_item_row(iid)
if row.is_empty():
return ""
var parts: Array[String] = []
for k in (row.get("effect_kinds", []) as Array):
if String(k) == "mine_reveal":
parts.append("真视")
elif String(k) == "reward_preview":
parts.append("预读")
elif String(k) == "ap_grant":
parts.append("AP +%d" % int(row.get("magnitude", 0)))
var tgt := String(row.get("target", "none"))
if tgt == "area_3x3":
parts.append("3×3")
elif tgt == "board_wide":
parts.append("全图")
return "%s · %s" % [String(row.get("display_name", iid)), " ".join(parts)]
# 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源;通关停点经重发 bonus_choice_presented 自然重建,
# 节点停点在此读在途态重 present——零自动推进契约不破,停点仍在途)
func _on_bonus_refresh() -> void:
if adventure == null:
return
if not adventure.refresh_bonus():
return
if _bonus_context == "reward_node":
var st: Dictionary = adventure.node_bonus_pending_state()
bonus_overlay.open_pick(Narrator.node_bonus_header(), _bonus_card_texts(st.get("candidates", []) as Array, st.get("aux_detail", []) as Array))
bonus_overlay.set_refresh_info(adventure.attune_refresh_left())
func _on_bonus_pick_confirmed(item_id: String) -> void:
if adventure == null:
return
# TP0 令牌池——卡片 id "kind|tier|relic_id" 拆键(缺 | = 旧载荷防御归普通档;relic 三段)
var parts := item_id.split("|")
var kind := parts[0]
var tier := parts[1] if parts.size() > 1 else ""
var rid := parts[2] if parts.size() > 2 else ""
var ok: bool = adventure.apply_node_bonus(kind, tier, rid) if _bonus_context == "reward_node" else adventure.pick_bonus(kind, tier, rid)
if not ok:
push_error("[组合根] 三选一兑付被拒——见输出日志")
func _on_aux_replace_presented(items: Array, incoming: Dictionary) -> void:
_last_replace_items = items.duplicate(true)
_last_replace_incoming = incoming.duplicate(true)
if not bonus_overlay.visible:
_open_modal(bonus_overlay) # 续玩替换在途直落——三选一面未开时先入栈(模态栈纪律)
_open_bonus_replace(items, incoming)
func _open_bonus_replace(items: Array, incoming: Dictionary) -> void:
var named: Array = []
for it in items:
var rec: Dictionary = it.duplicate()
rec["display_name"] = String(_item_name_lookup().get(String(rec.get("item_id", "")), String(rec.get("item_id", ""))))
named.append(rec)
bonus_overlay.open_replace(named, String(_item_name_lookup().get(String(incoming.get("item_id", "")), String(incoming.get("item_id", "")))),
named.size(), Attunement.slots(db, save_manager)) # 迭代 11 第①批 T7——呈现读数源切谓词表求值(过渡键退役)
func _on_bonus_replace_chosen(discard: int) -> void:
if adventure == null or not adventure.resolve_bonus_replace(discard):
push_error("[组合根] 同调位替换受理被拒——见输出日志")
# 兑付反馈(bonus_resolved 到达——结果行上屏,唯一出口 = 继续)
func _on_bonus_resolved(result: Dictionary) -> void:
_pending_bonus = {}
bonus_overlay.show_result(Narrator.bonus_result_text(result))
_refresh_presentations()
# 继续钮:收三选一面 → 上下文分派(通关停点=推进下一关 / 奖励节点=兑付确认回图选路——零自动推进)
func _on_bonus_continue() -> void:
_close_modal(bonus_overlay)
bonus_overlay.close_pick()
if _bonus_context == "reward_node":
if runner != null:
runner.continue_after_node()
return
if _adventure_mode and adventure != null:
if not adventure.continue_to_next_level():
_fatal_screen("关卡推进失败——见输出日志")
# 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案
func _on_adventure_finished(summary: Dictionary) -> void: func _on_adventure_finished(summary: Dictionary) -> void:
_level_active = false _level_active = false
@@ -673,7 +544,7 @@ func _on_adventure_finished(summary: Dictionary) -> void:
detail.assign(meta.settle_adventure(summary)) detail.assign(meta.settle_adventure(summary))
var text := Narrator.adventure_summary_text(summary) var text := Narrator.adventure_summary_text(summary)
if not detail.is_empty(): if not detail.is_empty():
text += "\n" + Narrator.keystone_settle_text(detail, _item_name_lookup()) text += "\n" + Narrator.keystone_settle_text(detail, _bonus.item_name_lookup())
if result == "completed": if result == "completed":
_end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(), _end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(),
"本局 GP 入账 · 合计 %d" % _adventure_gp_final) # 迭代 6 T7 卡⑩——GP 行只上冒险终局面板(C5) "本局 GP 入账 · 合计 %d" % _adventure_gp_final) # 迭代 6 T7 卡⑩——GP 行只上冒险终局面板(C5)
@@ -686,22 +557,12 @@ func _on_adventure_finished(summary: Dictionary) -> void:
_refresh_presentations() _refresh_presentations()
# 工坊 item_id → 显示名投影(keystone_settle_text 文案数据源——装配归本根,拼句归 Narrator)
func _item_name_lookup() -> Dictionary:
var lookup := {}
if meta != null:
for row in db.meta_workshop_items():
lookup[String(row.get("item_id", ""))] = String(row.get("display_name", ""))
return lookup
func _on_battle_end_continue() -> void: func _on_battle_end_continue() -> void:
# 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批) # 终局出口:返回开始界面(冒险摘要留档试玩日志;钥石已在收束时入存档——存档序列化批)
_close_modal(_end_overlay) _close_modal(_end_overlay)
_end_overlay.close() _end_overlay.close()
# 迭代 10——三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面 # 迭代 10——三选一停点挂起分支:战斗总结结算(Boss 胜利钥石增量行)确认后进三选一面
if not _pending_bonus.is_empty(): if _bonus.resume_if_pending(): # T4/D4 拆件——三选一停点在途恢复入口(bonus_interact_controller)
_show_bonus_pick()
return return
# 卡⑥ 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现) # 卡⑥ 挂起分派:战斗胜利总结结算框的「下一节点」≠ 终局——进选路页(暂存候选恢复呈现)
if _settlement_stop_pending: if _settlement_stop_pending:
@@ -732,7 +593,10 @@ func _on_end_review() -> void:
# ==================== 调试模式(连续两层——编排归 debug_session.gd; # ==================== 调试模式(连续两层——编排归 debug_session.gd;
# 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用)==================== # 单场遭遇用户入口已删(2026-09-13 用户裁决)——debug_session.start_single 原语仅探针使用)
# T4/D3 收口注记:本节处理器(battle_created/battle_ready/override/seed)系组合根绑定面——
# settlement 写与 _wire_battle=ADR-0001 组合根 essence,override/seed 取数面在组合根 UI 态
#(tune_panel/_seed_edit)——不迁移,信号连接留本根(契约 3)。====================
func _start_debug_levels() -> void: func _start_debug_levels() -> void:
tune_panel.reset_sliders() tune_panel.reset_sliders()
@@ -1020,25 +884,6 @@ func _on_abandon_requested() -> void:
# ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)---- # ---- 营地覆盖层与钥石账本(元进度实现批——写调用唯一归组合根)----
# 打开营地(主菜单入口——开合与开始层互斥仿 TunePanel 先例:开始覆盖层非模态且树序在后,
# 规范 2.3 遮挡纪律;打开时先刷工坊解锁——达标未解锁就地入账)
func _open_camp() -> void:
if meta == null:
return
meta.refresh_workshop_unlocks()
_start_overlay.visible = false
_open_modal(camp_overlay) # 模态栈单口(T3 收口批——登记层开合一律走栈)
camp_overlay.open_camp(_camp_data())
_refresh_presentations()
func _on_camp_closed() -> void:
_close_modal(camp_overlay) # 模态栈单口(T3 收口批)
_start_overlay.visible = true
_refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新
_refresh_presentations() # 关闭后刷新顶栏(工坊解锁可能变了余额侧读数面)
# ---- 最小设置面(迭代 8 卡④——入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)---- # ---- 最小设置面(迭代 8 卡④——入口/开合仿营地先例:开始层互斥;生效与持久化归 SettingsStore)----
func _open_settings() -> void: func _open_settings() -> void:
@@ -1052,110 +897,14 @@ func _on_settings_closed() -> void:
_refresh_resume_button() _refresh_resume_button()
# 营地快照投影(纯读——锻造三行/工坊六行/余额/最近冒险;拼句归 CampOverlay,账面归 MetaProgression) # ---- 探针缝保缝(T4/D4 拆件——click/ui_probe 营地段直呼组合根面;实现归 camp_shop_presenter.gd)----
func _on_camp_closed() -> void:
_camp_pres.on_camp_closed()
func _camp_data() -> Dictionary: func _camp_data() -> Dictionary:
var forge_rows: Array[Dictionary] = [] return _camp_pres.camp_data()
if meta != null:
for attr in MetaTable.FORGE_ATTRS:
var row: Dictionary = db.meta_forge_row(attr)
forge_rows.append({
"attr": attr,
"display_name": String(row.get("display_name", "")),
"level": meta.forge_level(attr),
"flat": int(row.get("flat_value", 0)),
"price": meta.forge_price(attr),
})
return {
"balance": meta.keystone_balance() if meta != null else 0,
"forge": forge_rows,
"workshop": meta.workshop_status() if meta != null else [],
"attune": meta.attune_camp_status() if meta != null else {}, # 迭代 11 第①批 T4——同调装配面投影
"tree": meta.tree_camp_rows() if meta != null else [], # 迭代 12 T9——天赋树面投影
"fork_groups": meta.tree_fork_groups() if meta != null else [],
"recent": save_manager.recent_summaries() if save_manager != null else [],
}
func _on_camp_buy(attr: String) -> void:
if meta == null:
return
if attr == "attune_refresh":
# Y 刷新升级行购买(迭代 11 第①批 T4——P1 甲;按钮禁用为常态防线,兜底拒绝可见 pattern 1)
if not meta.buy_attune_refresh():
board_zone.toast.show_toast("钥石不足——买不起这次升级", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
return
if not meta.buy_forge(attr):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
board_zone.toast.show_toast("钥石不足——买不起这件强化", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
# 同调装配切换(迭代 11 第①批 T4——装配关系唯一写口经 MetaProgression.toggle_attunement;满位兜底拒绝可见)
func _on_camp_attune_toggle(item_id: String) -> void:
if meta == null:
return
if not meta.toggle_attunement(item_id):
board_zone.toast.show_toast("同调位已满——先卸下再装配", "warn")
return
camp_overlay.refresh_camp(_camp_data())
# 天赋树购买(迭代 12 T9——写口唯一归 MetaProgression.tree_buy/tree_buy_fork;Fork 节点购=激活侧)
func _on_camp_tree_buy(node_id: String) -> void:
if meta == null:
return
var ok: bool = meta.tree_buy_fork(node_id) if String(db.tree_node(node_id).get("kind", "")) == "fork" else meta.tree_buy(node_id)
if not ok:
board_zone.toast.show_toast("购买被拒——前置/行门槛/余额不足", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
# A/B 免费切换(迭代 12 T8——同组换侧零币;未激活组走购买面)
func _on_camp_fork_toggle(group: String, side: String) -> void:
if meta == null:
return
if not meta.tree_toggle_fork(group, side):
board_zone.toast.show_toast("切换被拒——未激活组请先购买", "warn")
return
camp_overlay.refresh_camp(_camp_data())
_refresh_presentations()
# ---- 商店覆盖层与 GP 钱包(GP+商店房实施批——写调用唯一归组合根)----
func _on_shop_buy(product_id: String) -> void:
if runner == null:
return
if runner.is_shop_pool():
# pool 面:买 1 → 核心全重抽——货架整架重投喂(重抽反馈;余额不足核心拒绝零副作用)
if not runner.buy_pool_item(product_id):
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
board_zone.toast.show_toast("金币不足——买不起这件商品", "warn")
return
shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp))
_refresh_presentations()
return
if not runner.buy_product(product_id):
# 余额不足(按钮禁用为常态防线——兜底拒绝可见,pattern 1)
logger.log_ux("ux_reject", {"source": "wallet", "sub_reason": "insufficient_gp"})
board_zone.toast.show_toast("金币不足——买不起这件商品", "warn")
return
shop_overlay.refresh_state(runner.shop_products(), int(runner.gp))
_refresh_presentations()
func _on_shop_leave() -> void:
_close_modal(shop_overlay)
if runner != null:
# 复用兑付确认纪律(接口漂移清偿):离开 ≠ 推进——确认后经 choices_presented 弹出选路口
runner.continue_after_node()
func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void:
+13 -10
View File
@@ -31,12 +31,15 @@ var _rap_label: Label
var _rap_feedback: Label var _rap_feedback: Label
var _last_rap := 0 # 上次投影值(清零反馈判据——禁解析自身 label 文本当状态) var _last_rap := 0 # 上次投影值(清零反馈判据——禁解析自身 label 文本当状态)
var refresh_cost: int = 0 # 表值驱动(装配经 set_refresh_cost 注入 ActionEconomyTable.rap_refresh_cost) var refresh_cost: int = 0 # 表值驱动(装配经 set_refresh_cost 注入 ActionEconomyTable.rap_refresh_cost)
var _confirm_armed := false var _arm_timer := ArmedTimer.new() # 弃图刷新确认窗(T3/D3 P4 乙——tween 秒表机制收口;窗口=REFRESH_CONFIRM_WINDOW_S)
var _arm_tween: Tween
const REFRESH_CONFIRM_WINDOW_S := 4.0 # 弃图刷新确认窗(原 tween_interval 字面量升常量——语义零变化)
func _init() -> void: func _init() -> void:
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
_arm_timer.expired.connect(_disarm)
var sb := UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(8, 4, 8, 4)) var sb := UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(8, 4, 8, 4))
add_theme_stylebox_override("panel", sb) add_theme_stylebox_override("panel", sb)
@@ -186,7 +189,7 @@ func confirm_visible() -> bool:
func press_confirm_for_probe() -> void: func press_confirm_for_probe() -> void:
if _confirm_armed: if _arm_timer.is_armed():
_disarm_and_fire() _disarm_and_fire()
@@ -200,12 +203,7 @@ func _on_refresh_pressed() -> void:
if _btn_refresh.disabled: if _btn_refresh.disabled:
return return
_confirm_row.visible = true _confirm_row.visible = true
_confirm_armed = true _arm_timer.arm(REFRESH_CONFIRM_WINDOW_S)
if _arm_tween != null and _arm_tween.is_valid():
_arm_tween.kill()
_arm_tween = create_tween()
_arm_tween.tween_interval(4.0)
_arm_tween.tween_callback(_disarm)
func _disarm_and_fire() -> void: func _disarm_and_fire() -> void:
@@ -214,10 +212,15 @@ func _disarm_and_fire() -> void:
func _disarm() -> void: func _disarm() -> void:
_confirm_armed = false _arm_timer.disarm()
_confirm_row.visible = false _confirm_row.visible = false
# 帧驱动(T3/D3——超时回落走 ArmedTimer;unarmed 零开销直返)
func _process(delta: float) -> void:
_arm_timer.tick(delta)
# 资源面拒绝反馈(pattern 1 resource_face 腿):AP 数字脉冲 # 资源面拒绝反馈(pattern 1 resource_face 腿):AP 数字脉冲
func pulse_ap() -> void: func pulse_ap() -> void:
_pulse(_ap_label) _pulse(_ap_label)
@@ -118,6 +118,10 @@ var fog_state := 0:
if fog_state != v: if fog_state != v:
fog_state = v fog_state = v
queue_redraw() queue_redraw()
var removed_dim := false: # 中立移除退色层(T8/D6——BoardView paint 按排雷玩法 is_removed 写入)
set(v):
removed_dim = v
queue_redraw()
func _draw() -> void: func _draw() -> void:
@@ -256,6 +260,10 @@ func _draw() -> void:
draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), FOG_MEMORY_PLATE, true) draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), FOG_MEMORY_PLATE, true)
if glyph != "": if glyph != "":
_draw_glyph(glyph, FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源) _draw_glyph(glyph, FOG_MEMORY_INK, maxi(18, int(ceil(edge * 0.5)))) # 字号下限 18(§7.4 冻结公式同源)
# ⑦ 中立移除退色层(迭代 13 T8/D6——P2 甲退色降档:暗幕压顶全层覆盖,暗淡记忆态同族呈现语义;
# 顶层于迷雾之上——移除格不再参与迷雾语义。本实现移除集=未揭示雷格(无数字无旗印))
if removed_dim:
draw_rect(Rect2(Vector2.ZERO, Vector2(edge, edge)), Color(0.12, 0.10, 0.09, 0.62), true)
# 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿): # 南立面(D 档件一——墙格南缘内侧,自底边向上:双段暗带在下、口沿高光线贴其上沿):
+30 -1
View File
@@ -316,7 +316,7 @@ func refresh() -> void:
var hero: Vector2i = _minesweeper.hero_pos() var hero: Vector2i = _minesweeper.hero_pos()
_diggable.clear() _diggable.clear()
for n in _board.neighbors8(hero): for n in _board.neighbors8(hero):
if not _minesweeper.is_revealed(n): if not _minesweeper.is_revealed(n) and not _minesweeper.is_removed(n):
_diggable[n] = true _diggable[n] = true
for y in _board.height: for y in _board.height:
for x in _board.width: for x in _board.width:
@@ -684,6 +684,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
return return
var revealed: bool = _minesweeper.is_revealed(c) var revealed: bool = _minesweeper.is_revealed(c)
var flag: bool = _minesweeper.has_flag(c) var flag: bool = _minesweeper.has_flag(c)
var removed: bool = _minesweeper.is_removed(c) # 迭代 13 T8/D6——中立移除格(P2 甲退色降档)
var is_hero: bool = c == hero var is_hero: bool = c == hero
var glyph := "" var glyph := ""
var bg: Color var bg: Color
@@ -691,6 +692,10 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
# 旗槽统一维护(T17 修复):旗格=封印旗纹理;其余格清 null(插旗格挖开后残留在 tex_flag # 旗槽统一维护(T17 修复):旗格=封印旗纹理;其余格清 null(插旗格挖开后残留在 tex_flag
# 会让 _draw 无条件把旗印画在已开格上——tex_flag 绘制无 revealed 条件)。缺盘走字形回退。 # 会让 _draw 无条件把旗印画在已开格上——tex_flag 绘制无 revealed 条件)。缺盘走字形回退。
u.tex_flag = _flag_tex if (flag and not revealed and _flag_tex != null) else null u.tex_flag = _flag_tex if (flag and not revealed and _flag_tex != null) else null
if removed:
revealed = false # 移除格固定未开呈现基座(退色暗幕层承载;雷格本无数字——P1 甲周边数字冻结)
flag = false
u.tex_flag = null
if revealed: if revealed:
var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面 var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面
if rec.get("kind", "") == "mine": if rec.get("kind", "") == "mine":
@@ -809,6 +814,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void:
u.fog_state = (2 if revealed else 1) if fogged else 0 u.fog_state = (2 if revealed else 1) if fogged else 0
else: else:
u.fog_state = 0 u.fog_state = 0
u.removed_dim = removed
u.queue_redraw() u.queue_redraw()
@@ -1117,3 +1123,26 @@ func _make_cursor_texture(glyph: String) -> ImageTexture:
if f != null: if f != null:
f.draw_string(img, Vector2(2, 18), glyph, HORIZONTAL_ALIGNMENT_LEFT, 20, 18, COL_DIGIT) f.draw_string(img, Vector2(2, 18), glyph, HORIZONTAL_ALIGNMENT_LEFT, 20, 18, COL_DIGIT)
return ImageTexture.create_from_image(img) return ImageTexture.create_from_image(img)
# ---- 探针只读出口(D2 改造 2026-10-04——_cells/_diggable 私有容器直入退役;形态仿 fx_probe 先例:只读快照) ----
# 格件映射只读快照(Vector2i → CellUnit;键面同构——断言语义零变化;破私有面的只是容器字典,
# 格件字段本为公有呈现面,快照值即原格件引用)
func cells_probe() -> Dictionary:
return _cells.duplicate()
# 可挖域只读快照(Vector2i → true;8 向相邻未开格=呼吸微光作用集)
func diggable_probe() -> Dictionary:
return _diggable.duplicate()
# 悬停格只读出口(case① 悬停到达断言消费面——_hover 私有域直入退役,D2 清单外同类项随批收口)
func hover_probe() -> Vector2i:
return _hover
# 迷雾参数只读快照(case⑮ 参数漂移断言消费面——_fog_lantern/_fog_enabled 私有域直入退役,D2 清单外同类项随批收口)
func fog_params_probe() -> Dictionary:
return {"lantern": _fog_lantern, "enabled": _fog_enabled}
@@ -28,7 +28,7 @@ var _relic_list: VBoxContainer
func _init() -> void: func _init() -> void:
custom_minimum_size = Vector2(240, 0) custom_minimum_size = Vector2(240, 0)
clip_contents = true clip_contents = true
# 主面板穿石板戏服(样张 v2 冻结批 T2——纹理本色≈PANEL_HERO;数据框仍素面 §7.1;缺位回退 flat) # 主面板穿石板戏服(样张 v2 冻结批 T2——纹理本色;数据框仍素面 §7.1;缺位回退 flat)
var sb := UiKit.panel_skinned(Vector4(8, 6, 8, 6)) var sb := UiKit.panel_skinned(Vector4(8, 6, 8, 6))
add_theme_stylebox_override("panel", sb) add_theme_stylebox_override("panel", sb)
var col := VBoxContainer.new() var col := VBoxContainer.new()
+26 -15
View File
@@ -61,14 +61,14 @@ var _cand_ids: Array[int] = [] # 本停点候选 id 集(图上命
var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它) var _content_size := Vector2(620, 240) # 图内容包络(open_choices 按图拟合——画布 min 尺寸随它)
var _hover_id := -1 # 悬停预承诺候选(-1 = 无) var _hover_id := -1 # 悬停预承诺候选(-1 = 无)
var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开) var abandon_btn: Button # 放弃入口(可见性归组合根 set_abandon_available——只在冒险模式开)
var _abandon_armed := false # 二次确认态(首击置位、二击提交、超时/重开回落) var _abandon_timer := ArmedTimer.new() # 放弃确认窗(T3/D3 P4 乙——_process 帧驱动倒计时机制收口;窗口=CONFIRM_WINDOW_S)
var _confirm_left := 0.0 # 确认态剩余秒数(_process 倒计时+脉冲驱动)
var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全) var _node_icons := {} # 节点图标贴图(load_tex 守卫——缺位 null → 字形回退,冒烟/无素材检出安全)
var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点) var _noise_rect: TextureRect # 屏底石纹(换层色板渐变承载面——null = 纯色回退无挂点)
var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用) var _tint_level := 0 # 当前已应用 tint 的层号(0 = 未应用)
func _init() -> void: func _init() -> void:
_abandon_timer.expired.connect(_reset_abandon_confirm)
# 节点图标贴图装载(样张 v1 冻结形制——load_tex 守卫,缺位回退现行字形) # 节点图标贴图装载(样张 v1 冻结形制——load_tex 守卫,缺位回退现行字形)
for t in TEX_NODE_ICONS: for t in TEX_NODE_ICONS:
_node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t]) _node_icons[t] = UiKit.load_tex(TEX_NODE_ICONS[t])
@@ -201,34 +201,33 @@ func set_abandon_available(v: bool) -> void:
# 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。 # 放弃二次确认(布局决策 3):首击转确认态(文案+危险红脉冲约 3 秒超时回落),二击才提交。
# 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。 # 单击不提交——ui_probe 战斗中 pressed.emit() 单击路径保持「未受理」语义(战斗中拒绝归组合根)。
func _on_abandon_pressed() -> void: func _on_abandon_pressed() -> void:
if not _abandon_armed: if not _abandon_timer.is_armed():
_abandon_armed = true _abandon_timer.arm(CONFIRM_WINDOW_S)
_confirm_left = CONFIRM_WINDOW_S
abandon_btn.text = "再点一次确认放弃——不可逆" abandon_btn.text = "再点一次确认放弃——不可逆"
if ArmedTimer.motion_still():
abandon_btn.modulate = Color(1, 1, 1, 0.75) # 减动效静态基色(脉冲层不再驱动)
return return
_reset_abandon_confirm() _reset_abandon_confirm()
abandon_requested.emit() abandon_requested.emit()
func _reset_abandon_confirm() -> void: func _reset_abandon_confirm() -> void:
_abandon_armed = false _abandon_timer.disarm()
_confirm_left = 0.0
abandon_btn.modulate = Color(1, 1, 1, 1) abandon_btn.modulate = Color(1, 1, 1, 1)
abandon_btn.text = "放弃" # 打磨包 2026-09-29——文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed) abandon_btn.text = "放弃" # 打磨包 2026-09-29——文案收敛 level-map 4 字上限(确认态转义面见 _on_abandon_pressed)
func _process(delta: float) -> void: func _process(delta: float) -> void:
if not _abandon_armed: if not _abandon_timer.is_armed():
return return
_confirm_left -= delta _abandon_timer.tick(delta)
if _confirm_left <= 0.0: if not _abandon_timer.is_armed():
_reset_abandon_confirm() return # 窗口到点回落(expired 信号已驱动 _reset_abandon_confirm)
return # 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪——档判据单点 motion_still)
# 危险红边框脉冲(单段呼吸约 1Hz——闪烁基线 ≤3 次/秒内;减动效 = 静态半透不闪) if ArmedTimer.motion_still():
if UiKit.motion_scale < 1.0:
abandon_btn.modulate = Color(1, 1, 1, 0.75) abandon_btn.modulate = Color(1, 1, 1, 0.75)
else: else:
var k := 0.5 + 0.5 * sin(_confirm_left * TAU) var k := 0.5 + 0.5 * sin(_abandon_timer.elapsed * TAU)
abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k) abandon_btn.modulate = Color(1, 1, 1, 0.65 + 0.35 * k)
@@ -468,6 +467,18 @@ func _layout(nodes: Array, canvas: Control) -> Dictionary:
return pos return pos
# ---- 探针只读出口(D2 改造 2026-10-04——_layout/_canvas 私有域直入退役;形态仿 fx_probe 先例:只读快照) ----
# 选路布局只读快照(node → 局部位置;画布固定取现行 _canvas,调用侧零私有域触达)
func layout_probe(nodes: Array) -> Dictionary:
return _layout(nodes, _canvas)
# 路由画布只读快照(origin=全局原点;min_size=最小拟合尺寸——画布拟合断言消费面)
func canvas_probe() -> Dictionary:
return {"origin": _canvas.get_global_position(), "min_size": _canvas.custom_minimum_size}
# 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层 # 分层(layout/fit 共享件):layer = 入口 BFS 深度;同层 slot 垂直排布;未达节点兜底排末层
func _assign_layers(nodes: Array, layer: Dictionary, slot: Dictionary) -> void: func _assign_layers(nodes: Array, layer: Dictionary, slot: Dictionary) -> void:
var layer_counts := {} var layer_counts := {}
+104 -11
View File
@@ -30,6 +30,9 @@ var _tested_tooltip := false
var _inter_confirms := 0 # 大奖励屏确认计数(冒险推进停点——首见确认推进 L2,再见 = L2 通关收口) var _inter_confirms := 0 # 大奖励屏确认计数(冒险推进停点——首见确认推进 L2,再见 = L2 通关收口)
var _settlement_confirms := 0 # 卡⑥ 胜利结算框确认计数(观测面——每场普通战斗胜利恰一) var _settlement_confirms := 0 # 卡⑥ 胜利结算框确认计数(观测面——每场普通战斗胜利恰一)
var _route_clicks := 0 # 选路覆盖层点击计数(岔路使用率观测面) var _route_clicks := 0 # 选路覆盖层点击计数(岔路使用率观测面)
var _locktest_state := 0 # T1 D1 锁期/重放断言状态机(0=待注入 1=等重放 2=完结)——每 run 一次
var _locktest_cell := Vector2i(-9, -9)
var _locktest_frames := 0
func _ready() -> void: func _ready() -> void:
@@ -174,7 +177,7 @@ func _ready() -> void:
var first_cell_ms := t_entry var first_cell_ms := t_entry
if first_cell_ms > 500: if first_cell_ms > 500:
_problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms) _problems.append("进入战斗屏首格可点超时 %dms > 500ms" % first_cell_ms)
if main.board_view._cells.is_empty(): if main.board_view.cells_probe().is_empty():
_problems.append("首格未可点(cells=0)") _problems.append("首格未可点(cells=0)")
print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [ print("[click_probe] 首格可点计时 %dms(进入战斗屏口径,≤500ms AC)· 场景冷启动 %dms(含 UI 构建 %dms,观测项)" % [
first_cell_ms, t_total, t_ui_ready]) first_cell_ms, t_total, t_ui_ready])
@@ -190,9 +193,12 @@ func _ready() -> void:
_flags_placed.clear() _flags_placed.clear()
continue continue
if main.bonus_overlay.visible: if main.bonus_overlay.visible:
# 迭代 10 三选一停点(BonusPickOverlay——奖励节点/通关兑付共用面;T17 硬锁批适配—— # 迭代 10 三选一停点(BonusPickOverlay——奖励节点/通关兑付共用面;T17 硬锁批适配——
# 本探针此前未适配该层,走到即卡死)。三态分派:替换子态(满位——放弃新件)/ # 本探针此前未适配该层,走到即卡死)。三态分派:替换子态(满位——放弃新件)/
# result 态(兑付反馈——点继续推进)/ 待选态(点首件+确认)。真实信号路径全线。 # result 态(兑付反馈——点继续推进)/ 待选态(点首件+确认)。真实信号路径全线。
# T1 D1——三选一停点输入锁定断言(停点态断言面:替换子/待选/兑付三态共用此层)。
if not main._input_locked():
_problems.append("三选一停点打开时输入未锁定")
if main.bonus_overlay._replace_box.visible: if main.bonus_overlay._replace_box.visible:
main.bonus_overlay.replace_chosen.emit(-1) main.bonus_overlay.replace_chosen.emit(-1)
elif main.bonus_overlay._continue_btn.visible: elif main.bonus_overlay._continue_btn.visible:
@@ -204,17 +210,20 @@ func _ready() -> void:
main.bonus_overlay.press_continue() # 停点已消费的残面——继续关屏 main.bonus_overlay.press_continue() # 停点已消费的残面——继续关屏
_flags_placed.clear() _flags_placed.clear()
continue continue
if main.shop_overlay.visible:
# 商店房覆盖层(GP+商店房批——L2 商店为 Boss 前必经位):零购离开(真实 leave 信号路径)放行选路。
# T1 D1 分派链序修正:商店层=选路页上的模态覆盖(迭代 11 商店房重构后停点呈现为
# level_screen+shop 双 visible)——必须先于 level_screen 分派,否则下层候选点击
# 空转烧尽预算(实证:lv2 商店停点双 visible 循环 12000 帧 done=false)。
main.shop_overlay.leave_pressed.emit()
_flags_placed.clear()
continue
if main.level_screen.visible: if main.level_screen.visible:
# 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造) # 节点图选路口(零自动推进停点)——图上命中路径交替选候选(岔路使用行为;布局决策 2 改造)
main.level_screen.press_candidate(_route_clicks % 2) main.level_screen.press_candidate(_route_clicks % 2)
_route_clicks += 1 _route_clicks += 1
_flags_placed.clear() _flags_placed.clear()
continue continue
if main.shop_overlay.visible:
# 商店房覆盖层(GP+商店房批——L2 商店为 Boss 前必经位):零购离开(真实 leave 信号路径)放行选路
main.shop_overlay.leave_pressed.emit()
_flags_placed.clear()
continue
if main._inter_overlay.visible: if main._inter_overlay.visible:
# 大奖励屏(冒险级停点——非末关通关恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已通关,收口 # 大奖励屏(冒险级停点——非末关通关恰一次):首见确认推进 L2(真实按钮信号路径);再见 = L2 已通关,收口
if not main._input_locked(): if not main._input_locked():
@@ -281,6 +290,8 @@ func _ready() -> void:
# 排播演播期豁免(T17 硬锁 v3 随批——合法停顿态):演播期棋盘锁生效、probe 点击被吞、 # 排播演播期豁免(T17 硬锁 v3 随批——合法停顿态):演播期棋盘锁生效、probe 点击被吞、
# 逻辑态恒定属预期。判据=纯队列(与 main 锁同源)。Waiting 收尾等待期**不豁免**—— # 逻辑态恒定属预期。判据=纯队列(与 main 锁同源)。Waiting 收尾等待期**不豁免**——
# 锁若把收尾链卡死,此处 stall 照报(探测面)。 # 锁若把收尾链卡死,此处 stall 照报(探测面)。
# T1 D1 处置=保留豁免+注记:冻结/缓存/重放语义不再靠豁免绕行,由 _locktest_tick
# 脚本化断言正面承载(每 run 一次;逻辑态恒定本豁免继续作 stall 判据)。
if main._row_queue.is_empty(): if main._row_queue.is_empty():
_stall_frames += 1 _stall_frames += 1
if _stall_frames == 30: if _stall_frames == 30:
@@ -290,9 +301,14 @@ func _ready() -> void:
_stall_frames = 0 _stall_frames = 0
_ap_spent_this_round = 0 _ap_spent_this_round = 0
_last_state = st _last_state = st
_locktest_tick()
if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active": if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active":
_run_modal_probes_once() # T1 D1——锁测在飞期(state 1)冻结探针动作面:排空帧 autoplay 的免费 move 会抢在
_play_one_action() # 帧末 deferred 重放之前改变英雄位(flag 拒收 not_adjacent 实证)——「注入→重放」
# 窗口内局面零变化正是 v3.1 被测语义(演播期冻结),动作面恢复待断言收口。
if _locktest_state != 1:
_run_modal_probes_once()
_play_one_action()
# ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提 # ---- 预算耗尽收口(T17 硬锁批适配):选件变强后 probe 可能活过预算不再战败——断点段前提
# (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至 # (冒险终局面在案)须主动满足。收口驱动:任何停点层逐个推进(复用主循环分支语义)至
# level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。 # level_screen 选路停点→真实放弃二击→终局面。战斗中则继续 autoplay 打完。
@@ -527,6 +543,8 @@ func _play_one_action() -> void:
var settlement = main.settlement var settlement = main.settlement
if settlement == null: if settlement == null:
return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固) return # 结算竞态防抖:battle_finished 与本轮动作调度同帧到达时 settlement 可能已被置空(基线既有噪声——加固)
if not main._row_queue.is_empty():
return # T1 D1——演播期不出手:此时点击只会进锁期缓存被「最后一次」覆盖(原逐帧连点语义无行为覆盖价值,且毁锁期断言注入的独占前提)
var minesweeper = settlement.minesweeper var minesweeper = settlement.minesweeper
# 行为①:随机插旗(右键——未开相邻格),约 20% 轮次 # 行为①:随机插旗(右键——未开相邻格),约 20% 轮次
if _ap_spent_this_round == 0 and _rand.randf() < 0.2: if _ap_spent_this_round == 0 and _rand.randf() < 0.2:
@@ -581,6 +599,81 @@ func _click_stop() -> void:
main.ap_bar._btn_stop.pressed.emit() main.ap_bar._btn_stop.pressed.emit()
# ---- T1 D1:锁期/重放断言(每 run 一次,脚本化场景——演播期豁免的断言化承载) ----
# 锁期断言(排播队列活跃=锁面):注入旗击 → 直落被冻结不受理(旗未置)+ `_locked_click`
# 缓存命中且为本注入(「最后一次」语义——注入帧内无并发点击)。
# 重放断言(演播完边沿 pump_ended→_replay_locked_click 重入唯一动作通道):缓存清空 +
# 受理恰一次——旗布尔=单次送达探测器(双发=两次翻转回 false、零发=false,均暴露)。
# 前提消失守卫:等重放期间出现停点层/战斗收场(注入时点恰逢终结演播)→ 静默弃测不计
# problem(旗动作不杀伤,正常局不触发;断言实现形态取舍在代拍白名单内)。
func _locktest_tick() -> void:
if _locktest_state == 2:
return
var settlement = main.settlement
if settlement == null or settlement.minesweeper.current_board() == null:
return
if _locktest_state == 0:
if main._row_queue.is_empty() or main._input_locked():
return
if String(settlement.battle_result) != "" or String(settlement.action_economy.phase) != "Active":
return
if int(settlement.action_economy.ap) < 1:
return
var target := _locktest_pick_cell(settlement.minesweeper)
if target.x < 0 or settlement.minesweeper.has_flag(target):
return # 无合法邻格/目标被并发旗行为占位——下帧重挑
_locktest_cell = target
_locktest_frames = 0
main.board_view.click_cell(target, MOUSE_BUTTON_RIGHT)
if settlement.minesweeper.has_flag(target):
_problems.append("锁期断言:演播期点击未被冻结(旗即时受理——v3 冻结失守)")
_locktest_state = 2
return
if main._locked_click.is_empty() or main._locked_click.get("cell") != target:
_problems.append("锁期断言:演播期点击未入缓存(v3.1 缓存失守)")
_locktest_state = 2
return
_locktest_state = 1
elif _locktest_state == 1:
if main.bonus_overlay.visible or main._inter_overlay.visible or main._end_overlay.is_open() \
or String(settlement.battle_result) != "":
_locktest_state = 2 # 前提消失守卫:终结演播竞态——静默弃测
return
if not main._row_queue.is_empty():
_locktest_frames = 0 # 演播未完——重放耐心只在排空后起算(链式大演播数百帧不占预算)
return
_locktest_frames += 1
if _locktest_frames < 3:
return # 排空边沿的 deferred 重放(pump_ended 帧末执行)在途宽限
if _locktest_frames > 90:
_problems.append("重放断言:演播完边沿后缓存未清(%s)" % str(main._locked_click))
_locktest_state = 2
return
if main._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" % [
str(settlement.battle_result), str(settlement.action_economy.phase),
int(settlement.action_economy.ap), str(_locktest_cell),
str(settlement.minesweeper.is_revealed(_locktest_cell)),
b.width, b.height, str(settlement.minesweeper.hero_pos())])
_problems.append("重放断言:锁期缓存点击未受理恰一次(旗未置位——零发或双发)")
_locktest_state = 2
main.board_view.click_cell(_locktest_cell, MOUSE_BUTTON_RIGHT) # 拔旗恢复现场(尽力而为不判定)
func _locktest_pick_cell(minesweeper) -> Vector2i:
var hero: Vector2i = minesweeper.hero_pos()
for dy in range(-1, 2):
for dx in range(-1, 2):
if dx == 0 and dy == 0:
continue
var c: Vector2i = hero + Vector2i(dx, dy)
if minesweeper.current_board().in_bounds(c) and not minesweeper.is_revealed(c) and not minesweeper.has_flag(c):
return c
return Vector2i(-1, -1)
func _report_stall(st: Dictionary) -> void: func _report_stall(st: Dictionary) -> void:
var why := "" var why := ""
if st.phase == "Waiting": if st.phase == "Waiting":
+34 -28
View File
@@ -45,8 +45,8 @@ func _ready() -> void:
for n in nds: for n in nds:
if main.runner.node_id_of(n) == int(cands[0]): if main.runner.node_id_of(n) == int(cands[0]):
tgt = n tgt = n
var npos: Vector2 = ro._layout(nds, ro._canvas)[tgt] var npos: Vector2 = ro.layout_probe(nds)[tgt]
var gpos: Vector2 = ro._canvas.get_global_position() + npos var gpos: Vector2 = ro.canvas_probe()["origin"] + npos
var evm := InputEventMouseMotion.new() var evm := InputEventMouseMotion.new()
evm.position = gpos evm.position = gpos
evm.global_position = gpos evm.global_position = gpos
@@ -97,12 +97,13 @@ func _ready() -> void:
# 常数锚(画布拟合探针):70/122/26/8 与 presentation/views/level_map_screen.gd 的 _content_size # 常数锚(画布拟合探针):70/122/26/8 与 presentation/views/level_map_screen.gd 的 _content_size
# 公式(70 + maxl*122 + NODE_HALF.x + 8)同源复刻——改常数须两侧同步(本探针只读断言层,真源在屏) # 公式(70 + maxl*122 + NODE_HALF.x + 8)同源复刻——改常数须两侧同步(本探针只读断言层,真源在屏)
var need_x2: float = 70.0 + maxl2 * 122.0 + 26.0 + 8.0 var need_x2: float = 70.0 + maxl2 * 122.0 + 26.0 + 8.0
if float(ro2._canvas.custom_minimum_size.x) < need_x2 - 0.5: var cvmin: Dictionary = ro2.canvas_probe()
_pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [ro2._canvas.custom_minimum_size.x, need_x2]) 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 bv = main.board_view
var ms = main.settlement.minesweeper if main.settlement != null else null var ms = main.settlement.minesweeper if main.settlement != null else null
if ms == null or bv._cells.is_empty(): if ms == null or bv.cells_probe().is_empty():
_pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv._cells.size(), str(main.settlement)]) _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.settlement)])
return return
bv.action_requested.connect(func(c: Vector2i, kind: String): _actions.append([c, kind])) bv.action_requested.connect(func(c: Vector2i, kind: String): _actions.append([c, kind]))
bv.chord_attempted.connect(func(c: Vector2i): _chords.append(c)) bv.chord_attempted.connect(func(c: Vector2i): _chords.append(c))
@@ -130,7 +131,7 @@ func _ready() -> void:
# 并流批(ADR-0007 裁丙 2026-09-27)后首战板面种子随全局序漂移——_diggable(主角 8 邻未开域) # 并流批(ADR-0007 裁丙 2026-09-27)后首战板面种子随全局序漂移——_diggable(主角 8 邻未开域)
# 不足两枚时,沿已开区真点击移位到「≥2 未开邻」的前沿格再选;移位失败即收手交由选取失败分支报状态 # 不足两枚时,沿已开区真点击移位到「≥2 未开邻」的前沿格再选;移位失败即收手交由选取失败分支报状态
for _expand in range(16): for _expand in range(16):
if bv._diggable.size() >= 2: if bv.diggable_probe().size() >= 2:
break break
var mv_t := Vector2i(-9, -9) var mv_t := Vector2i(-9, -9)
for y in bv._board.height: for y in bv._board.height:
@@ -157,7 +158,7 @@ func _ready() -> void:
break break
var flag_t := Vector2i(-9, -9) var flag_t := Vector2i(-9, -9)
var dig_t := Vector2i(-9, -9) var dig_t := Vector2i(-9, -9)
for c in bv._diggable: for c in bv.diggable_probe():
if not ms.has_flag(c): if not ms.has_flag(c):
if flag_t.x == -9: if flag_t.x == -9:
flag_t = c flag_t = c
@@ -172,9 +173,9 @@ func _ready() -> void:
# ---- case① 悬停可挖域:motion → 悬停态到达(金框 + 十字光标)---- # ---- case① 悬停可挖域:motion → 悬停态到达(金框 + 十字光标)----
_push_motion(_pos(bv, flag_t)) _push_motion(_pos(bv, flag_t))
await get_tree().process_frame await get_tree().process_frame
if bv._hover != flag_t: if bv.hover_probe() != flag_t:
_problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv._hover, flag_t]) _problems.append("case① 悬停未到达(_hover=%s 应=%s)" % [bv.hover_probe(), flag_t])
var u1 = bv._cells[flag_t] var u1 = bv.cells_probe()[flag_t]
if not u1.hl_visible or u1.hl_color != UiKit.GOLD: if not u1.hl_visible or u1.hl_color != UiKit.GOLD:
_problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.hl_visible), str(u1.hl_color)]) _problems.append("case① 悬停金框缺失(visible=%s color=%s)" % [str(u1.hl_visible), str(u1.hl_color)])
if bv.mouse_default_cursor_shape != Input.CURSOR_CROSS: if bv.mouse_default_cursor_shape != Input.CURSOR_CROSS:
@@ -184,14 +185,14 @@ func _ready() -> void:
await get_tree().process_frame await get_tree().process_frame
if _forbids.is_empty() or _forbids[_forbids.size() - 1] != forbid_t: if _forbids.is_empty() or _forbids[_forbids.size() - 1] != forbid_t:
_problems.append("case② hover_forbidden 未达(%s)" % [str(_forbids)]) _problems.append("case② hover_forbidden 未达(%s)" % [str(_forbids)])
if bv._cells[forbid_t].hl_color != UiKit.RED_FORBID: if bv.cells_probe()[forbid_t].hl_color != UiKit.RED_FORBID:
_problems.append("case② 禁入红框缺失(color=%s)" % str(bv._cells[forbid_t].hl_color)) _problems.append("case② 禁入红框缺失(color=%s)" % str(bv.cells_probe()[forbid_t].hl_color))
# ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ---- # ---- case③ 悬停已开格:路径预览点迹 + 移动光标 ----
_push_motion(_pos(bv, reveal_t)) _push_motion(_pos(bv, reveal_t))
await get_tree().process_frame await get_tree().process_frame
var dots := 0 var dots := 0
for c in bv._cells: for c in bv.cells_probe():
if bv._cells[c].dot_visible: if bv.cells_probe()[c].dot_visible:
dots += 1 dots += 1
if dots < 1: if dots < 1:
_problems.append("case③ 路径预览点迹未亮(dots=0)") _problems.append("case③ 路径预览点迹未亮(dots=0)")
@@ -208,7 +209,7 @@ func _ready() -> void:
# ---- case④b 旗纹理槽回归断言(P0-1 修复批 2026-10-02):T13 批曾在 paint 尾部无条件 # ---- case④b 旗纹理槽回归断言(P0-1 修复批 2026-10-02):T13 批曾在 paint 尾部无条件
# `u.tex_flag = null` 覆盖 T17 批的条件赋值=素材在位时插旗不可见(headless 缺盘走字形回退 # `u.tex_flag = null` 覆盖 T17 批的条件赋值=素材在位时插旗不可见(headless 缺盘走字形回退
# 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ---- # 掩盖)——有盘断言纹理在槽、缺盘断言字形回退,两分支防双写踩踏复活 ----
var flag_unit = bv._cells.get(flag_t) var flag_unit = bv.cells_probe().get(flag_t)
if bv._flag_tex != null: if bv._flag_tex != null:
if flag_unit == null or flag_unit.tex_flag != bv._flag_tex: if flag_unit == null or flag_unit.tex_flag != bv._flag_tex:
_problems.append("case④b 旗纹理槽未挂封印旗纹理(tex_flag=%s)" % ["无单元" if flag_unit == null else str(flag_unit.tex_flag)]) _problems.append("case④b 旗纹理槽未挂封印旗纹理(tex_flag=%s)" % ["无单元" if flag_unit == null else str(flag_unit.tex_flag)])
@@ -298,7 +299,7 @@ func _ready() -> void:
# 必须回退「人」(数字格仍显数字——回退判据与抑制判据同源);验毕还原纹理重绘。 ---- # 必须回退「人」(数字格仍显数字——回退判据与抑制判据同源);验毕还原纹理重绘。 ----
if bv._world.hero != null: if bv._world.hero != null:
var hp11: Vector2i = ms.hero_pos() var hp11: Vector2i = ms.hero_pos()
var u11 = bv._cells[hp11] var u11 = bv.cells_probe()[hp11]
var tex_saved: Texture2D = bv._hero_tex_idle var tex_saved: Texture2D = bv._hero_tex_idle
bv._hero_tex_idle = null bv._hero_tex_idle = null
bv.refresh() bv.refresh()
@@ -315,23 +316,25 @@ func _ready() -> void:
if mark_t.x >= 0: if mark_t.x >= 0:
bv.set_variant_mark_resolver(func(_c): return "flat_damage") bv.set_variant_mark_resolver(func(_c): return "flat_damage")
bv.refresh() bv.refresh()
if String(bv._cells[mark_t].mutation_mark) != "flat_damage": if String(bv.cells_probe()[mark_t].mutation_mark) != "flat_damage":
_problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv._cells[mark_t].mutation_mark)) _problems.append("case⑭ 变异角标未落位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].mutation_mark))
bv.set_variant_mark_resolver(Callable()) bv.set_variant_mark_resolver(Callable())
bv.refresh() bv.refresh()
if String(bv._cells[mark_t].mutation_mark) != "": if String(bv.cells_probe()[mark_t].mutation_mark) != "":
_problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv._cells[mark_t].mutation_mark)) _problems.append("case⑭ 角标未随 resolver 复位(mutation_mark=%s)" % str(bv.cells_probe()[mark_t].mutation_mark))
else: else:
_problems.append("case⑭ 无未开格可验(板面异常)") _problems.append("case⑭ 无未开格可验(板面异常)")
# ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位; # ---- case⑮ 视野迷雾(迭代 11 第④批 T5——三态呈态/半径圈/移动跟随/开关两态零扰动/提灯插值位;
# 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ---- # 纯呈现零逻辑——迷雾开关切换前后 minesweeper 状态摘要逐位一致) ----
if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(bv._fog_lantern) != 0 or not bv._fog_enabled: var fogp: Dictionary = bv.fog_params_probe()
if int(bv.FOG_RADIUS) != 8 or int(bv.FOG_LANTERN_BONUS) != 2 or int(fogp["lantern"]) != 0 or not fogp["enabled"]:
_problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)" _problems.append("case⑮ 迷雾参数漂移(R=%s bonus=%s lantern=%s enabled=%s 应=8/2/0/true)"
% [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(bv._fog_lantern), str(bv._fog_enabled)]) % [str(bv.FOG_RADIUS), str(bv.FOG_LANTERN_BONUS), str(fogp["lantern"]), str(fogp["enabled"])])
else: else:
var hero15: Vector2i = ms.hero_pos() var hero15: Vector2i = ms.hero_pos()
var b15 = ms.current_board() var b15 = ms.current_board()
var fog_ok := true var fog_ok := true
var cells_f1: Dictionary = bv.cells_probe()
for y in b15.height: for y in b15.height:
for x in b15.width: for x in b15.width:
var c15 := Vector2i(x, y) var c15 := Vector2i(x, y)
@@ -339,7 +342,7 @@ func _ready() -> void:
var expect15: int = 0 var expect15: int = 0
if d15 > bv.FOG_RADIUS: if d15 > bv.FOG_RADIUS:
expect15 = 2 if ms.is_revealed(c15) else 1 expect15 = 2 if ms.is_revealed(c15) else 1
var got15: int = int(bv._cells[c15].fog_state) var got15: int = int(cells_f1[c15].fog_state)
if got15 != expect15: if got15 != expect15:
fog_ok = false fog_ok = false
_problems.append("case⑮ 迷雾呈态不符(%s dist=%d got=%d expect=%d)" % [str(c15), d15, got15, expect15]) _problems.append("case⑮ 迷雾呈态不符(%s dist=%d got=%d expect=%d)" % [str(c15), d15, got15, expect15])
@@ -348,11 +351,12 @@ func _ready() -> void:
break break
# 半径边界格恰在圈内(曼哈顿距离恰 =R 的格 fog_state=0——存在性校验,12×12 板必有) # 半径边界格恰在圈内(曼哈顿距离恰 =R 的格 fog_state=0——存在性校验,12×12 板必有)
var has_boundary := false var has_boundary := false
var cells_f2: Dictionary = bv.cells_probe()
for y in b15.height: for y in b15.height:
for x in b15.width: for x in b15.width:
if absi(x - hero15.x) + absi(y - hero15.y) == int(bv.FOG_RADIUS): if absi(x - hero15.x) + absi(y - hero15.y) == int(bv.FOG_RADIUS):
has_boundary = true has_boundary = true
if int(bv._cells[Vector2i(x, y)].fog_state) != 0: if int(cells_f2[Vector2i(x, y)].fog_state) != 0:
_problems.append("case⑮ 边界圈内格被误遮(dist==R 应 fog_state=0)") _problems.append("case⑮ 边界圈内格被误遮(dist==R 应 fog_state=0)")
break break
if has_boundary: if has_boundary:
@@ -369,9 +373,10 @@ func _ready() -> void:
digest_before.append(ms.is_revealed(c16)) digest_before.append(ms.is_revealed(c16))
bv.set_fog_enabled(false) bv.set_fog_enabled(false)
var all_clear := true var all_clear := true
var cells_f3: Dictionary = bv.cells_probe()
for y in b15.height: for y in b15.height:
for x in b15.width: for x in b15.width:
if int(bv._cells[Vector2i(x, y)].fog_state) != 0: if int(cells_f3[Vector2i(x, y)].fog_state) != 0:
all_clear = false all_clear = false
if not all_clear: if not all_clear:
_problems.append("case⑮ 迷雾关未全板归零(off 态应 fog_state=0)") _problems.append("case⑮ 迷雾关未全板归零(off 态应 fog_state=0)")
@@ -383,7 +388,7 @@ func _ready() -> void:
if digest_off != digest_before: if digest_off != digest_before:
_problems.append("case⑮ 迷雾开关改写逻辑态(两态结算逐位一致 🔴 契约破)") _problems.append("case⑮ 迷雾开关改写逻辑态(两态结算逐位一致 🔴 契约破)")
bv.set_fog_enabled(true) bv.set_fog_enabled(true)
if int(bv._cells[Vector2i(0, 0)].fog_state) == 0 \ if int(bv.cells_probe()[Vector2i(0, 0)].fog_state) == 0 \
and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS): and absi(0 - hero15.x) + absi(0 - hero15.y) > int(bv.FOG_RADIUS):
_problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)") _problems.append("case⑮ 迷雾复开后远角未复原遮蔽(fog_state=0)")
if _problems.is_empty(): if _problems.is_empty():
@@ -405,10 +410,11 @@ func _has_action(cell: Vector2i, kind: String) -> bool:
func _pick_forbidden(bv, ms) -> Vector2i: func _pick_forbidden(bv, ms) -> Vector2i:
var b = ms.current_board() var b = ms.current_board()
var digp: Dictionary = bv.diggable_probe()
for y in b.height: for y in b.height:
for x in b.width: for x in b.width:
var c := Vector2i(x, y) var c := Vector2i(x, y)
if not ms.is_revealed(c) and not bv._diggable.has(c): if not ms.is_revealed(c) and not digp.has(c):
return c return c
return Vector2i(-9, -9) return Vector2i(-9, -9)
+2 -2
View File
@@ -82,7 +82,7 @@ func _ready() -> void:
if main.adventure == null or String(main.adventure.state) != "running": if main.adventure == null or String(main.adventure.state) != "running":
problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)")
# ---- 既有断言(回归基线;A 区进度条断言随 停点前移批 2026-09-23 退役——节点链进度由关卡画面独立屏承载)---- # ---- 既有断言(回归基线;A 区进度条断言随 停点前移批 2026-09-23 退役——节点链进度由关卡画面独立屏承载)----
if main.board_view._cells.is_empty(): if main.board_view.cells_probe().is_empty():
problems.append("棋盘格未生成(cells=0)") problems.append("棋盘格未生成(cells=0)")
if main.board_view.board() == null: if main.board_view.board() == null:
problems.append("board 未装载") problems.append("board 未装载")
@@ -228,7 +228,7 @@ func _ready() -> void:
main._close_modal(main._end_overlay) main._close_modal(main._end_overlay)
if problems.is_empty(): if problems.is_empty():
print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [ print("[ui_probe] PASS — cells=%d board=%dx%d hp='%s' enemies=%d ap_dots=%d" % [
main.board_view._cells.size(), main.board_view.board().width, main.board_view.board().height, main.board_view.cells_probe().size(), main.board_view.board().width, main.board_view.board().height,
main.info_panel.attack_text(), main.info_panel.attack_text(),
main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()]) main.stage.enemy_box.get_child_count(), main.ap_bar._dots.get_child_count()])
get_tree().quit(0) get_tree().quit(0)
+1 -1
View File
@@ -3,7 +3,7 @@
**Purpose**: QA 交接前 15 分钟内人工跑完的关键路径清单。 **Purpose**: QA 交接前 15 分钟内人工跑完的关键路径清单。
**Run via**: `/smoke-check`(读本文件)。 **Run via**: `/smoke-check`(读本文件)。
**Update**: 新核心系统落地后增补条目。 **Update**: 新核心系统落地后增补条目。
**分工注记**:自动化冒烟(切片 96/96)覆盖规则正确性;本清单面向**表现与手感**(自动化探针不可 G-W-T 的部分)。 **分工注记**:自动化冒烟(全量断言,现行口径 718/0——2026-10-04 迭代 13 T6 随批换代,随批末闸走)覆盖规则正确性;本清单面向**表现与手感**(自动化探针不可 G-W-T 的部分)。
## Core Stability(恒跑) ## Core Stability(恒跑)