diff --git a/design/gdd/06-待办清单.md b/design/gdd/06-待办清单.md index 31c2488c..e8c0e717 100644 --- a/design/gdd/06-待办清单.md +++ b/design/gdd/06-待办清单.md @@ -624,13 +624,18 @@ **状态**:备忘(不阻塞,不排期) + --- -#### 代码架构技术债修复批——需求卡片已写(2026-10-14 登记) +#### 迭代 27 代码架构技术债修复批——GDD 反哺点两条(2026-10-14 随批登记,待用户逐点裁决另批落文) -**现状**:[代码架构与实现质量审计报告-2026-10-14](../research/代码架构与实现质量审计报告-2026-10-14.md)(双席审计,基线 97de02c;同日第三方逐条复核:主体结论全部成立,五处计数级瑕疵已修正落文)确诊 2 条 P0(全产品单场景/局态手工复位清单)+ 6 条 P1(_settle 巨函数 259 行/刷新双账/GP 影子账/共享可变字典跨三层/组合根不可测/类型闸缺位)+ 7 组 P2 卫生件。用户已裁:全部技术债修复允许放行单一大迭代(12-14 个工作日,2026-10-14 裁决记录)。需求卡片=[需求卡片-代码架构技术债修复批-2026-10-14](../research/需求卡片-代码架构技术债修复批-2026-10-14.md)(**v1.2**,五波:场景拆分+局生命周期 → _settle 拆段 → 账本收口 → 组合根抽取+模态内聚 → 类型闸+卫生件;核心验收=重构前后同种子完整局输出逐位一致)。 +**现状**:五波全量入库(单会话交付;逐位一致对拍三变体五跑全同;实现报告=design/qa/iter27-实现报告-2026-10-14.md)。实现与设计权威文档的差异/缺口如下,经裁决后另批落文——**机械面随批已动的只有本条与 07/roadmap**。 -**归属**:工程债/重构批(core/presentation 结构面;零行为变更契约) +**归属**:工程债/重构批收口反哺(不阻塞,不排期) -**状态**:**🔄 U0 已开工(2026-10-14 用户令编入迭代 27 卡①+同日 /goal+iter 27 发令)**——卡片 v1.2 三点已裁全按推荐案(①SaveManager/MetaProgression=第三个 autoload/②Ledger=单类持四账/③_settle=审计五段),零待裁点;批入库后本条销号 +| # | 文档 | 反哺点 | +|---|---|---| +| 1 | design/dev/architecture/architecture.md + ADR 系列 | 场景分层(三屏壳+组合根句柄面)/会话生命周期(RunScope 物化)/结算五段 Segment(core/settlement/)/RunLedger 四账所有权/ModalFlow 模态编排层与互斥域 tags 协议——现行陈述滞后于实态 | +| 2 | design/dev/code-conventions.md | §4 语言纪律机器化(S9+audit_type_hints.py 三规则);`:= {}` 推断形态豁免口径;`# type-hint: ignore` 行级白名单与 `# ADR-0007-exempt` S1/S2 标记豁免通道;--script 模式禁裸 autoload 标识符纪律(新);Variant 参数化口径与元素精化后续 | +**状态**:备忘(反哺点待裁决;代拍板追认单十五项见实现报告 §三;残留人闸=07 ㉖ 五件登记不阻塞) diff --git a/design/gdd/07-体验验证清单.md b/design/gdd/07-体验验证清单.md index d20e6079..7a096b96 100644 --- a/design/gdd/07-体验验证清单.md +++ b/design/gdd/07-体验验证清单.md @@ -272,4 +272,21 @@ | v0.7 | 2026-10-13 | 新增 ㉓ 敌人表内容修批三观测目检节(迭代 24 卡①随批登记:新招可见性/甲型体感/第一关节奏——登记不阻塞按 2026-10-12 真人检测立规) | | v0.8 | 2026-10-13 | 新增 ㉔ UI 全局主题批全屏巡检目检节(迭代 25 卡①随批登记:引擎默认灰清零巡检/音量滑轨两段可辨/专项观察点二(滑轨柄抢焦点·滚动条槽可见性)/预盖件无实役注记——登记不阻塞;能力验证半=冒烟 V1-V6) | | v0.9 | 2026-10-14 | 新增 ㉕ UI 交互与观感批逐屏巡检目检节(迭代 26 卡①随批登记:按钮五态反馈/键盘焦点环/全局 Nearest 巡检(含暗角与渐变白名单判据)/布局均衡(商店居中+营地四栏等高)/诊断钮清零——登记不阻塞;能力验证半=冒烟 V1-V6+T4 结构断言) | +<<<<<<< HEAD | v1.0 | 2026-10-14 | **qa 收尾批·人闸核销(用户 2026-10-14 回报实机目检全部完成)**:⑮真人半、⑳、㉑、㉒、㉓、㉔、㉕ 七节逐节加「✅ 已核销(2026-10-14)」注记——用户目检回报完成、未呈报问题;iter19 acceptance-package 存活八项与 iter20 五件(行为变化速览节)同批核销;迭代 18 清单 18 项同批核销。注:⑯⑰⑱⑲ 等早期人闸节此前已随各批核销或归 07 独立跟踪,本条不涉及 | +======= +| v1.0 | 2026-10-14 | 新增 ㉖ 代码架构技术债修复批全流程走查目检节(迭代 27 卡①随批登记:全流程观感零变化走查/续玩断点恢复/调试浮窗对局中可开不锁棋盘〔:407-408 事故回归〕/刷新余量显示一致/DS_DEBUG 启动浮窗+两屏角标——登记不阻塞;能力验证半=冒烟 V1-V9+parity 三变体逐位一致对拍五跑全同) | + + +## ㉖ 代码架构技术债修复批——全流程走查(迭代 27 卡①随批登记;能力验证半=冒烟 V1-V9+parity 三变体逐位一致对拍五跑全同) + +> 场景拆分+会话物化+战斗域重构+账本收口+模态协议内聚后,玩家可见面唯一验收=观感零变化。登记不阻塞(2026-10-12 真人检测立规)。 + +| # | 目检项 | 判据 | +|---|---|---| +| 1 | 全流程走查 | 开始菜单→营地四柜台→开新冒险→选路→战斗(挖/旗/刷新/停手)→三选一→商店→Boss→终局——逐屏观感与重构前一致(演出时序/文案/布局零漂移) | +| 2 | 续玩断点恢复 | 停点退出→「继续上局」文案与恢复落点正确(run-scope 物化后断点链回归) | +| 3 | 调试浮窗对局中可开不锁棋盘 | 对局中开浮窗→棋盘可继续操作(:407-408 永久锁死事故回归——互斥域 tags 化后钉面) | +| 4 | 刷新余量显示 | 刷新受理后 HUD 余量递减且与受理次数一致(双账删副本后 HUD 单源读快照) | +| 5 | DS_DEBUG=1 启动 | 浮窗启动即开+两屏角标点亮可点(同步面随 ModalFlow 迁移后行为钉面) | +>>>>>>> iter27-techdebt-20261014 diff --git a/design/qa/iter27-实现报告-2026-10-14.md b/design/qa/iter27-实现报告-2026-10-14.md new file mode 100644 index 00000000..46876bbf --- /dev/null +++ b/design/qa/iter27-实现报告-2026-10-14.md @@ -0,0 +1,93 @@ +# 迭代 27 实现报告——代码架构技术债修复批(五波全量入库) + +- 日期:2026-10-14(单会话 /goal+iter U0 交付,worktree g-plan2-iter27-techdebt 分支 iter27-techdebt-20261014) +- 需求卡片:`design/research/需求卡片-代码架构技术债修复批-2026-10-14.md`(v1.2——三点待裁 2026-10-14 用户三裁全按推荐案:①SaveManager/MetaProgression=第三个 autoload/②Ledger=单类持四账/③_settle=审计五段) +- 来源审计:代码架构与实现质量审计报告-2026-10-14(双席基线 97de02c;**本批收口时按退役规则 git rm——吸收即死,结论迁本报告与卡片**) +- 核心验收(V1):**重构前后同种子完整局输出逐位一致**——parity_anchor 三变体(base/attune/shop)×40 局×RUNSUM+JSONL 剔 ts 双面哈希对拍,**五波出口五跑全同**(锚冻结于 T0,种子面 71000/72000/73000) + +## 一、五波交付 + +### 波一 场景拆分+局生命周期物化(P0-1/P0-2,7a0d57d) + +- 三屏壳场景 `presentation/scenes/`:menu_screen/camp_screen/adventure_screen(内含 level_map/battle 两容器)——视图代码原样构建(装配器/视图类零 .tscn 化,契约 2);组合根句柄全部保留(探针挂载假设 main.X 一级路径零迁移);树序=屏壳在前全模态之下(视觉共存对逐对验算:legend/battle、debug/level_screen、shop/level_screen 等全部保序)。 +- 会话物化 `run_scope.gd`:adventure/runner/settlement/局态旗/账本随会话节点承载;`_begin_session` 建会话(_ready 即建闲时态=永非 null)、终局整节点 queue_free()——`_reset_run_scope` 三项+`_on_battle_end_continue` 置空四件手工清单消亡。 +- SaveManager/MetaProgression 升格 autoload(裁点①甲;类名加 Core 后缀沿 RngService 先例);探针隔离改 attach_path 原地重置(set_master_seed 先例);**发现并修复**:屏壳根全屏 Control 默认 STOP 吃掉下层模态鼠标输入(ui_input 实证)——三屏壳根 mouse_filter=IGNORE+V2 回归闸。 +- V2 屏壳独立实例化 11 断言+V3 两连局复位 11 断言进 smoke。 + +### 波二 _settle 五段拆 Segment(P1-1,26301ba) + +- `core/settlement/` 五段类:GrantSegment(授予段体+旗判随段迁入+counter_add 五位/grant_material 三位分支单点化)/AuraSegment/CombatSegment/DecaySegment/VerdictSegment(返回裁定,终局早退语义留宿主);编排器 1725→1325 行退为段序编排+行发射原语单点。 +- 姊妹病灶:`_resolve_hero_attack` 148 行拆四步(ctx 命中面→基值装配→掷点兑现→交付),ctx/redemption 字典穿线算序逐字保留。 +- V4 settle_segments_test 16 断言进 run_all。 + +### 波三 账本收口(P1-2/P1-3/P1-4,6fd7d78) + +- `core/run_ledger.gd` RunLedger 单类持四账(裁点②甲):attune_charges/vision_counter/adventure_counters/bonus;战斗侧只拿 read/append 原语;AR 持账→LR set_run_ledger→enc 单键下传;编排器三裸引用成员退役;调试/单场缺省新账=零扰动面(vision_counter 缺省空=不计,原 enc 缺键守卫语义保留)。 +- 刷新双账:main 副本删除;presentation_snapshot 成文面升键 refresh_used(can_refresh 邻位)——HUD 余量/遥询单源读快照;V5 快照一致性断言×3。 +- GP 影子账:summary 增 gp_final 键(close_wallet 清账前快照);gp_shadow_ledger.gd 全件退役;终局面板 GP 行取数=summary 键;ui_probe 断言换代。 + +### 波四 组合根可测化+模态协议内聚(P1-5/P2-7,8b9e556) + +- **重估结论(卡片 T4 前置义务)**:场景拆分落地后,模式状态机已随 RunScope 物化、屏构建已随屏壳迁出——抽取面收敛为**模态编排域**:`modal_flow.gd` ModalFlow 纯逻辑类(依赖仅 ModalStack+查询 Callable,审计处方形态)——开合编排(挂起兜底/排播排空/遮挡记账/开合同步)/输入锁谓词(三例外注入)/双形制角标谓词。 +- **V6 测试抓出并修正一处真分歧**:挂起兜底递归后误加 return(原 main 语义=先结挂起后继续开本层两拍都走)——单测缝的结构价值实证。 +- ModalStack 互斥域随层登记:register(m, tags)+TAG_NON_LOCKING/TAG_SCREEN_STATE 封闭词表+has_open_excluding_tags 组查询;原调用点排除式拼装(:404-409 输入锁等五处)退役;main._modals=flow.stack(ui_struct _list 断言缝保留)。 +- 控制器构造收拢 context:curio 16 参→CurioFlowContext/camp 13 参→CampFlowContext(字段逐一同名,控制器可独立实例化)。 + +### 波五 类型闸+卫生件(P1-6/P2 组,b605022+T6 笔) + +- 类型闸:`tools/audit_type_hints.py` 机器闸(R1 无类型容器注解/R2 有值返回缺类型/R3 参数缺类型+行级白名单通道);368 处容器注解机械参数化(Array[Variant]/Dictionary[Variant, Variant] 零行为面)、13 处参数真类型;project.godot 拉起 untyped_declaration=1;static_scan 新 S9 规则。 +- 卫生件:死代码 13 函数删除(审计 16 处复核后其中 ui_kit:486 一处经实读为现役非死码,见追认单)+FeatsEngine 整文件退役;字符串 Callable×10 位换方法引用形态(编译期方法存在性校验);冒险状态机/standee 主角三态 enum 化(外部 String 契约逐位一致);ConfigDB `_fatal` 退出归根(核心层去 quit/crash,置 `_fatal_flag`,退出决策归组合根既有 fail-fast 通道)+resolver 诊断/数据通道全面 Callable 化(ConfigDb↔Resolver 类型环断根)。 + +## 二、闸记录(每波出口+批末) + +| 闸 | 基线(T0 冻结) | 波一 | 波二 | 波三 | 波四 | 波五 | +|---|---|---|---|---|---|---| +| 逐位一致对拍(V1 核心) | 锚冻结 | ✓ 全同 | ✓ 全同 | ✓ 全同 | ✓ 全同 | ✓ 全同 | +| 冒烟 | 1055/0 | 1077/0 | 1077/0 | 1079/0 | 1079/0 | 1081/0 | +| run_all | 25 套 | 25 | 26 | 26 | 27 | 27 | +| probe_gate --click | 绿 | 绿 | 绿 | 绿 | 绿 | 绿 | +| render_gate golden | 0.00% | 0.00% | 0.00% | 全绿 | 全绿 | 全绿 | +| static_scan | (历史 cd 错位恒跳过) | — | — | — | — | S1/S2/S4/S9 全绿 | + +批末(T6 后)run_all 27 套全绿+冒烟 1081/0+probe/render 全绿+对拍全同(T6 对拍见 /tmp/parity_check6.log 记录,exit 0)。 + +## 三、代拍板追认单(白名单九项执行实录+计划外决断,待用户逐项追认) + +1. **屏场景命名/落位**(白名单①):`presentation/scenes/{menu,camp,adventure}_screen.tscn`——三屏壳与卡片三屏一一对应。 +2. **屏壳内部分层**(白名单②):adventure 屏壳内 battle_root+level_screen 平级(无同时可见态,层内先后零视觉差)。 +3. **Segment 五类命名/落位**(白名单③):`core/settlement/{grant,aura,combat,decay,verdict}_segment.gd`,类名 XxxSegment。 +4. **Ledger 命名/原语形状**(白名单④):`core/run_ledger.gd` RunLedger;原语=attune_charge_has/set/consume、vision_count_add、counters_seed_snapshot/merge_export、bonus_add_axis(各站点原语义逐字收拢)。 +5. **run-scope 物化形态**(白名单⑤):RunScope Node 挂 main;_ready 即建闲时态(永非 null——全组合根免空判);会话更替=旧节点 queue_free+新节点同拍建。 +6. **探针迁移最小改动**(白名单⑥):组合根句柄保留策略使 10 探针中 7 个零迁移、3 个仅 run_scope 路径换代;pacing 测试改 attach 隔离。 +7. **类型标注写法**(白名单⑦):368 处机械参数化取 `Array[Variant]`/`Dictionary[Variant, Variant]` 零行为面口径——**元素精化(如 Array[Dictionary])留后续批渐进**(机器闸已封住新增无类型)。 +8. **死代码删除切分**(白名单⑧):随 T6 单笔提交。 +9. **enum 命名**(白名单⑨):adventure `State`/standee `HeroState`(+_HS_NAME/_HS_VALUE 双向映射常量)。 +10. **〔计划外〕P2-2 autoload 直取降格实现**:审计处方「编译期全局名」与本仓 --script 模式编译器不解析裸 autoload 标识符**硬冲突**(run_all.gd 头注在案+pacing 实测 Parse Error)——降格为路径常量单点(AUTOLOAD_PATH_* 四常量)+T1 读视图属性;拼写风险收敛一处,类型面保留。 +11. **〔计划外〕board_generator 四 db 参白名单**:generate/_attempt/legality/_finalize 的 db 参保持鸭子型(测量面 _BoardRowStub 冒烟夹具注入,typed 参拒 stub)——`# type-hint: ignore` 显式白名单通道首用。 +12. **〔计划外〕static_scan 历史 cd 错位修复**:9-24 布局迁移后 `dirname/..` 落 game/ 致 `game/core` 守卫恒跳过=静默空转遗产——修 `/../..` 后 S1-S4/S9 首次实跑,暴露并处置两笔存量(S1/S2 main `_seed_value` randi=ADR-0007 文档化豁免面→标记豁免通道;S9 即本批闸)。 +13. **〔计划外〕V6 测试抓出挂起兜底分歧**:ModalFlow 初版在挂起兜底后误加 return(原 main 语义两拍都走)——以原语义为准修正;该处即单测缝价值的直接实证。 +14. **〔计划外〕审计「死代码 16 处」复核为 13 处**:ui_kit.gd:486 一处实读为现役代码(审计抽样误差),不删;其余 12 函数+FeatsEngine 整文件照删。 +15. **〔计划外〕刷新遥测取数面换代**:`ux_refresh_attempt` 的 round_refresh_index 从 main 副本改读快照(P1-2 连带,载荷键与数值语义零变化)。 + +## 四、GDD 反哺点(两条,已登记 06,待用户逐点裁决另批落文) + +| # | 文档 | 反哺点 | +|---|---|---| +| 1 | design/dev/architecture/architecture.md + ADR 系列 | 场景分层陈述(三屏壳+组合根句柄面);会话生命周期(RunScope 物化+_begin_session/_end_session 语义);结算五段 Segment 结构(core/settlement/);RunLedger 四账所有权模型;ModalFlow 模态编排层与互斥域 tags 协议——architecture 现行陈述均滞后于实态 | +| 2 | design/dev/code-conventions.md | §4 语言纪律机器化(S9+audit_type_hints.py 三规则);`:= {}` 推断形态=类型推断合法面不算债的口径;`# type-hint: ignore` 行级白名单通道;`# ADR-0007-exempt` S1/S2 标记豁免通道;--script 模式禁裸 autoload 标识符纪律(新);Variant 参数化口径与元素精化后续 | + +## 五、实机目检总清单(登记 07 ㉖——真人检测不阻塞,2026-10-12 立规) + +1. 全流程操作观感零变化走查:开始菜单→营地(四柜台)→开新冒险→选路→战斗(挖/旗/刷新/停手)→三选一→商店→Boss→终局逐屏过一遍(场景拆分+会话物化后唯一人闸面)。 +2. 续玩断点恢复:停点退出→继续上局(断点文案/恢复落点)。 +3. 调试浮窗对局中可开不锁棋盘(:407-408 事故回归——互斥域 tags 化后行为钉面)。 +4. 刷新按钮余量显示与受理一致(双账删副本后 HUD 单源读快照)。 +5. DS_DEBUG=1 启动即开浮窗+两屏角标(同步面随 ModalFlow 迁移)。 + +## 六、残留与去向 + +- 残留人闸:07 ㉖ 五件(登记不阻塞)。 +- 代拍板追认单十五项(上表)待用户逐项追认。 +- GDD 反哺点两条(06 在册)待裁决另批落文。 +- 审计报告本体已按退役规则 git rm(吸收即死;修正后计数已由需求卡片 v1.2 承载)。 +- 元素精化(Array[Variant]→具体元素型)为类型闸后续渐进面(06 反哺点②附带)。 diff --git a/design/qa/iter27-探针迁移对照-2026-10-14.md b/design/qa/iter27-探针迁移对照-2026-10-14.md new file mode 100644 index 00000000..bbbcd9e9 --- /dev/null +++ b/design/qa/iter27-探针迁移对照-2026-10-14.md @@ -0,0 +1,28 @@ +# 迭代 27 T0 交付:smoke 探针挂载假设对照表(2026-10-14) + +卡片 T0.3 交付物。11 个挂载面 = 10 个 smoke 探针 + 1 个单测套件(pacing_queue_test,全库唯一直接 load main.tscn 的单元测试)。 +迁移原则(卡片 🔴 探针契约零破坏):**「实例化 main.tscn 得到完整产品树 + 组合根 API」这一挂载假设本身保留**—— +场景拆分后 main.tscn 仍是组合根(屏场景为其子节点挂载),探针入口零改动; +迁移面 = 探针内部对 main 私有成员的树寻址/状态探视,随成员落位逐个改写(改写即新结构的回归断言)。 +另两个挂载假设探针(adventure_monte_carlo/level_autoplay,SceneTree 直驱 level_runner 不挂 main.tscn)不在迁移面。 + +| # | 探针 | 挂载行 | 挂 main.tscn 得到什么 | 成员依赖面(实测 grep) | 迁移方向(T1 落位后改写) | +|---|---|---|---|---|---| +| 1 | ui_probe.gd | :15 | 完整产品树+驱动 `_start_adventure_from_menu()`(唯一试玩入口) | `_start_overlay/_battle_root/_row_queue/_debug/_is_settling/_on_adventure_finished/_on_camp_closed/_on_wallet_changed/_reset_run_scope/_adventure_gp_final/_adventure_gp_view/_close_modal` + `adventure/ap_bar/board_view/board_zone/camp_overlay/db/debug_overlay/info_panel/level_screen/log_view/meta/runner/settlement/stage` | 屏成员→屏场景属性路径;局态成员→run-scope 节点路径;`_reset_run_scope` 断言随手工清单消亡换代为 run 节点释放断言 | +| 2 | ui_click_probe.gd | :45 | 全输入链走查(行进/战斗/营地/商店点击 10935 预算) | 最广:`_adventure_mode/_battle_root/_camp_data/_end_overlay/_exit_level_screen/_input_locked/_inter_overlay/_last_gain_amount/_locked_click/_on_camp_closed/_refresh_resume_button/_refresh_used_round/_row_queue/_settlement_stop_pending/_start_adventure_from_menu/_start_overlay` + 全部屏成员 | `_input_locked` 输入锁随 T4 模态协议内聚落位;`_refresh_used_round` 随 T3 双账删除改快照通道断言;余同 #1 | +| 3 | ui_struct_probe.gd | :25 | 模态栈纪律走查(开合/遮挡/输入锁) | `_close_debug_overlay/_end_overlay/_gp_summary_text/_level_active/_modals/_on_camp_closed/_on_career_closed/_on_settings_closed/_on_status_closed/_open_modal/_open_settings/_open_status_panel/_refresh_resume_button/_resume_from_menu/_show_debug_overlay/_start_adventure_from_menu/_start_overlay` + 屏成员 | `_modals`(ModalStack)随 T4 内聚后寻址换代;模态层清单断言随屏场景化改挂屏壳 | +| 4 | ui_layout_probe.gd | :17 | 四区布局几何断言 | `_start_adventure_from_menu/board_view/board_zone/debug_overlay/info_panel/get_child/get_children/save_manager/stage` | board_zone/info_panel/stage 随屏场景化改屏壳属性路径;`get_child` 序数寻址须换名寻址(子场景化后序号漂移) | +| 5 | ui_input_probe.gd | :27 | 输入锁+宝珠辉光链路 | `_input_locked/_start_adventure_from_menu/_sync_treasure_glow/board_view/board_zone/build_adventure_params/debug_overlay/runner/save_manager/settlement/status_panel` | `_sync_treasure_glow`/`build_adventure_params` 随局态物化落位;余同 #1/#2 | +| 6 | ui_audit_probe.gd | :27 | 全屏巡检(12 模态层/入口可达) | `_close_debug_overlay/_end_overlay/_on_settings_closed/_open_settings/_refresh_resume_button/_show_debug_overlay/_start_overlay/board_view/camp_overlay/confirm_dialog/curio_overlay/legend_overlay/level_screen/save_manager/shop_overlay` | 模态层清单随屏场景化改挂屏壳;余同 #1 | +| 7 | ui_tune_probe.gd | :36 | 调试浮窗只读巡检(种子框/批量模拟) | `_close_debug_overlay/_input_locked/_seed_value/_show_debug_overlay/_start_adventure_from_menu/_start_overlay/adventure/board_zone/confirm_dialog/db/debug_overlay/level_screen/save_manager/settlement` | `_seed_value` 随种子框落位改路径;debug_overlay 归属屏待 T1 定(全局层 vs 屏内层) | +| 8 | shot_probe.gd | :35 | 截图(四屏+营地/设置) | `_start_overlay/board_zone/camp_overlay/career_overlay/level_screen/settings_overlay/settings_store/stage/status_panel` | 屏成员→屏场景路径;settings_store 随升格 autoload 改全局访问 | +| 9 | fx_shot_probe.gd | :28 | 特效截图(棋盘 fx) | `_start_overlay/level_screen/settings_store/settlement` | 同 #8 | +| 10 | t17_defer_probe.gd | :15 | 延迟模态时序断言(结算行分帧) | `_deferred_modal/_end_overlay/_row_pacer/_start_overlay/db/level_screen/meta/save_manager/settings_store/settlement` | `_row_pacer/_deferred_modal` 随 T4 模态协议落位;meta 随升格 autoload 改全局 | +| 11 | tests/unit/presentation/pacing_queue_test.gd | :19 | 行队列节奏单测(真实冒险至首战结算) | `_flush_row_queue/_refresh_resume_button/_row_queue/_start_adventure_from_menu/adventure/db/level_screen/log_view/meta/pace_delay_for/runner/save_manager` | `_row_queue/_flush_row_queue/pace_delay_for` 随 T4 抽取落位(该单测即 V6 新纯逻辑类直接 new() 的前哨);余同 #1 | + +**共性迁移点**(T1 施工照单改写,改写后逐探针全绿=V2 闸): +1. `meta`/`save_manager`/`settings_store` → 裁点① 第三/四 autoload 后改全局名访问(探针里的 `main.meta` 全数换代)。 +2. `runner/adventure/settlement/_adventure_mode/_row_queue` 等局态 → run-scope 节点(`main.run_scope.X` 形态,命名属代拍板白名单#5)。 +3. `_start_overlay/camp_overlay/level_screen/...` 屏成员 → 屏场景属性(组合根保留装配属性,路径一级化)。 +4. `get_child`/子序数寻址(ui_layout #4)→ 名寻址(子场景化后序数漂移是 breaking 面)。 +5. 手工复位断言(ui_probe `_reset_run_scope`)→ run 节点 `queue_free()` 复位断言(V3 配套)。 diff --git a/design/research/代码架构与实现质量审计报告-2026-10-14.md b/design/research/代码架构与实现质量审计报告-2026-10-14.md deleted file mode 100644 index e4f58c76..00000000 --- a/design/research/代码架构与实现质量审计报告-2026-10-14.md +++ /dev/null @@ -1,132 +0,0 @@ -# 代码架构与实现质量审计报告(2026-10-14) - -**审计对象**:`game/` 目录全部实现(约 114 个 GDScript 脚本 / 3.4 万行,不含 tests) -**审计方法**:godot-master 技能库(GD-Agentic-Skills 的本地镜像,Godot 4.7+ 专家知识库)双席审计——甲席走 Analyst 工作流审架构,乙席走 Auditor 工作流审实现规范与设计模式;两席均为**只读静态实读**,未运行游戏。实现席实跑了技能库自带扫描器(信号审计 / 类型标注审计),内存类扫描器需 Godot 运行时,按只读纪律跳过并注明。 -**审计日期**:2026-10-14(基线 master 97de02c) -**基线事实**:全项目真实游戏场景只有 1 个(`presentation/main.tscn`,其余 13 个 .tscn 全在 smoke/ 下是测试探针);节点几乎全部由 GDScript 程序化构建,产品代码 `instantiate()` 调用数为 0;autoload 仅 2 个(ConfigDB / RngService);目录分层 core / data / presentation / assets / tests / smoke。 - ---- - -## 总评 - -**core 层实现质量显著高于垂直切片平均水准,两席审计独立确认核心域健康**;问题集中在场景层与生命周期层。两个 P0 是同一根因的两面:**整条应用生命周期(菜单/营地/冒险/战斗/每局复位)压缩在 main.gd 一个 1385 行的组合根里,靠手工清单维持局间隔离**——领域侧已经长出三层编排器(AdventureRunner 跨局持久 / LevelRunner 一关一生 / BattleOrchestrator 一战一生),场景层没有任何切缝与之对应。一句话定性:**领域分层是洋葱,场景组织是石头。** - ---- - -## P0 发现(2 条,同根) - -### P0-1 全产品单场景:场景作为「组合单元/生命周期单元」完全缺席 - -- **证据**:`project.godot`(main_scene 唯一);`presentation/main.tscn` 仅 1 个 Control 节点挂 main.gd;`presentation/main.gd:204-361` `_build_ui` 程序化构建全部四区 + 12 个模态层。 -- **为什么是问题**:Godot 中场景是复用、组合与生命周期的基本单元——节点进/出树(`_enter_tree`/`_exit_tree`/NOTIFICATION_READY)本可免费承载「每局/每屏」的状态建立与销毁。现在一局结束什么都不销毁,全靠手动清理;任何视图都无法 F6 独立运行或单独复用;13 个 smoke 探针里 10 个只能挂载整个 main.tscn 才能测任何 UI(`smoke/ui_probe.gd:15` 等)。 -- **修复方向**:按「屏」切场景,第一步只拆屏容器 .tscn,视图继续代码构建,不要求全面 .tscn 化。 - -### P0-2 每局状态靠手工复位清单,无局级生命周期边界 - -- **证据**:`presentation/main.gd:647-657`(`_on_battle_end_continue` 手工置空 runner/settlement/adventure/_adventure_mode 并调 debug_overlay.reset_sliders());`main.gd:555-559` `_reset_run_scope` 只清 3 项;局态旗至少 7 个散布类顶(`main.gd:53-56,76-78,129-131`)。这类手工清单已有前科:`main.gd:790-792` 注释记录了续玩分支 runner 未就绪导致的实机崩值。 -- **为什么是问题**:场景机制「打包一组节点,`free()` 即整体回收」正是为消灭这种清单而存在;清单是随新增状态线性膨胀的手工契约,每加一个跨局字段就多一个静默泄漏点,且泄漏表现为低频难复现的脏状态而非显式报错。 -- **修复方向**:把「一局」物化为可整体 `queue_free()` 的节点(或至少一个 run-scope 对象持全部局态),终局释放即复位。 - ---- - -## P1 发现(6 条) - -### P1-1 战斗结算巨函数:259 行、嵌套 7 层、同构兑付块重复 ≥5 处(双席独立命中同一病灶) - -- **证据**:`core/battle_orchestrator.gd:632` `_settle()` = 259 行;`:562-563` 嵌套达 7 层(该文件 ≥5 层缩进行共 80 行,全项目最高)。重复块实读确认:`counter_add` 兑付三行组 ×5(`:682-684, 741-743, 776-778, 838 附近, 1192-1193`);`grant_material` 二次兑付块 ×3(约 `:688-694, :789-792, :1206-1212`);`reveal_tiles` 检查 ×3。效果 kind 字符串分派(`get("kind","") ==`)散布 20 处于结算段内。姊妹病灶:`core/damage_controller.gd:272` `_resolve_hero_attack()` = 148 行。全文件 1725 行、类级状态约 40 个变量(:62-98)、fan-out 13 个 core 类。 -- **为什么是问题**:编排器**不是**纯上帝对象(伤害已单据化进 DamageController,敌人/状态/奖励各自成类),但效果引擎只做了「匹配」数据驱动,「施加」按既定裁决留在发射点——实际效果是新增一种 effect kind 要同时改引擎词表 + 编排器内联分支,编排器增量复杂度与内容量线性挂钩。 -- **修复方向**:不推倒重写——五段结算各抽成 Segment 类(grant/aura/combat/decay/verdict),编排器退为段序编排 + 行发射,20 处 kind 分支随段归位。 - -### P1-2 刷新次数双账本——所有权三问(谁拥有/谁可改/谁知道)不及格 - -- **证据**:权威计数在 `core/battle_orchestrator.gd:73`(`_refresh_used_this_round`,受理门槛 :1522);呈现/遥测另计一份 `presentation/main.gd:129`(`_refresh_used_round`),HUD 余量 = 表上限 − main 自己的计数(`main.gd:1240`)。 -- **为什么是问题**:core 是裁判、main 是记账员,两条路一旦分叉(新增受理路径、调试注入、断点恢复),HUD 显示与真实可刷次数静默不一致。 -- **修复方向**:`presentation_snapshot()` 已可携带本轮已刷次数——删 main.gd 副本,HUD 只读快照。 - -### P1-3 GP 影子账本:表现层持有权威态镜像,根因在 summary 载荷不全 - -- **证据**:`presentation/gp_shadow_ledger.gd:9-19`(表现侧镜像钱包余额);根因链:`core/adventure_runner.gd:1046`(close_wallet() 清零先于 adventure_finished.emit)+ `adventure_runner.gd:1022-1038`(summary 十一个键里没有 final GP),呈现层被迫在清账前从事件流自抄一份。 -- **为什么是问题**:表现层持久化核心态违反「表现禁存数据」;镜像与权威只靠 wallet_changed 事件序维持一致(影子账本文件头自己承认是手工镜像)。该上抛的数据应随信号载荷走,不该让下游自建账本补抄。 -- **修复方向**:summary 增加 `gp_final` 键(清账前快照),删影子账本。 - -### P1-4 共享可变字典跨三层:所有权「名义单点、实际三处可写」 - -- **证据**:`core/level_runner.gd:501-521` begin_battle 的 encounter 载荷携带 bonus_ref/adventure_counters/attune_charges/vision_counter 四个共享 dict 引用;`core/battle_orchestrator.gd:79,86-88` 持引用直写(_attune_charges_ref 注释自认「use_treasure 消费直改」);`level_runner.gd:50` 账本同为字典引用共享。 -- **为什么是问题**:这是确定性设计(回灌/导出时点已文档化),但共享可变引用在函数签名上不可见——「冒险加算区归 AdventureRunner 所有」只是头注契约,LevelRunner 与 BattleOrchestrator 都握着同一本账可写。 -- **修复方向**:共享账收拢为显式 Ledger 类,战斗侧只拿 read+append 原语;战末 export/开局 seed 已有的时点语义不变。 - -### P1-5 组合根(main.gd)不可独立测试:核心读面靠注释纪律维持 - -- **证据**:控制器不可独立实例化——`presentation/curio_interact_controller.gd:39-57` 构造 16 参(10 个组合根 Callable)、`presentation/camp_shop_presenter.gd:25-40` 13 参;测试代价:`tests/unit/presentation/pacing_queue_test.gd:17-18` 必须 load 整个 main.tscn 挂整树并篡改 main 内部字段才能驱动。25 个测试套件中 core/data 侧 22 个直接 new() 即测,presentation 侧只剩整树集成一条路。 -- **为什么是问题**:组合根本身没有单测缝;豁免面与 20+ 访问器(settlement.minesweeper.* 等)无结构强制,全靠 ADR 注释和评审盯。 -- **修复方向**:把 main 的「模式状态机 + 输入锁 + 模态编排」抽成无 UI 纯逻辑类(依赖仅 ModalStack + 查询 Callable),控制器构造参数收拢为 context 对象——不改行为即可获得单测缝。**注:本条应在场景拆分落地后重新评估,main.gd 缩水后抽取面可能大幅变小。** - -### P1-6 静态类型闸缺位:类型规范有文、机器无闸 - -- **证据**:无类型 Array/Dictionary 游戏代码 73 处(core 53 / data 9 / presentation 11,如 `core/adventure_runner.gd:78,95`、`core/effect_engine.gd:94-95`、`presentation/main.gd:69`);core 无返回类型函数 12 处(`core/reward_economy.gd:109,124,136,151` 等);无类型参数(`core/attunement.gd:12`、`core/board_generator.gd:18,83,284,334`)。`project.godot` 全文 36 行没有任何 gdscript warning 配置——`untyped_declaration` 默认 Ignore。 -- **为什么是问题**:仓库 code-conventions §4.2 有明文(Dictionary 取值参与算术前显式声明类型),但无机器闸则规范只靠评审;Variant 容器在结算热循环里吃类型检查开销且放行拼写错。 -- **修复方向**:project.godot 提升 warning 级别让存量显形,新码一律收口;存量 73 处随批清理。**本条必须在其他重构全部落地之后做**,否则新重构代码又会产生新违规。 - ---- - -## P2 卫生件(建议打包随批,不需逐条裁决) - -1. **死代码 16 处**:`core/battle_orchestrator.gd:216` `_attune_row`、`core/damage_controller.gd:102` `_fx_dispatch`(活实现 _fx_fact 的退役孪生)、`core/board_data.gd:61` `clear_variants`、`core/effect_engine.gd:417` `combo_of`、`core/treasure_items.gd:68,83`、`core/minesweeper_rules.gd:41`、`core/feats_engine.gd:13,38`、`core/config_db.gd:253,273`、`core/save_manager.gd:419`、`presentation/curio_interact_controller.gd:58`、`presentation/views/stage_world.gd:17`、`presentation/views/curio_pick_overlay.gd:170`、`presentation/views/ui_kit.gd:486`。其中 **FeatsEngine 整文件零调用**(仅注释提及,`data/schemas/feat_table.gd:22` 自注已随重设计退役)但 `class_name` 仍全局注册。 -2. **绝对路径取 autoload ×4**:`presentation/main.gd:174,190,481,1351` 用 `get_node("/root/…")` 字符串路径取 ConfigDB/RngService——Godot 4 有编译期注入的全局名,字符串路径改名/拼写零防护。 -3. **字符串式 Callable ×9**:`core/battle_orchestrator.gd:105,390,391`、`core/damage_controller.gd:98`、`core/adventure_runner.gd:940,951`、`core/level_runner.gd:166`、`presentation/main.gd:191`——`Callable(self, "方法名字符串")` 绕过编译期方法存在性校验,应改 `self._method` 引用形式。 -4. **裸字符串状态机 ×16**:`core/adventure_runner.gd:120,159,176,263,269,277,402,894,974,1049,1051,1071`(state = "idle"/"running"/"settling");`presentation/views/standee_body.gd:333,373,380,387`。本仓 `core/action_economy.gd:12-23` 已有「内部 enum + match + 外部 String 契约 getter」正解示范,应套用。 -5. **百行级装配/校验长函数 17 个**:`presentation/main.gd:204` `_build_ui()` 160 行、`level_map_screen.gd:72` 145 行、`camp_overlay_view.gd:55` 142 行、`debug_overlay.gd:159` 114 行、schema 校验器群(treasure_item_table.gd:87 = 149 行等)。线性装配圈复杂度不高,但区块级改动每次都在巨函数里动刀。**注:main._build_ui 在场景拆分中自然解体,不必单独修。** -6. **ConfigDB 越权退出 + resolver 类型软环**:`core/config_db.gd:75-82` `_fatal` 直接 quit(1)/OS.crash(fail-fast 政策对,执行位置应归根——`main.gd:176-179` 已有同通道 fatal 屏);`config_db.gd:57-58` 持有并构造 ConfigDbResolver,resolver 反向 class_name 引用 ConfigDb——双向类型耦合,resolver 对门面改持 Callable 诊断通道即解(validator 已是此形态)。 -7. **模态「开态=可见性」协议**:`presentation/modal_stack.gd:39-63` has_open() 靠遍历 visible 位;`main.gd:404-409` 输入锁 = 栈查询 + 三例外 + 排除式;`main.gd:407-408` 注释记录过「DS_DEBUG 默认开 × 遮挡复活 × has_open 组合 = 棋盘永久锁死」事故。12 个层的状态散布在各自 visible 位上,约束只存在于调用点拼装。修法:ModalStack 内聚「互斥域/排除组」概念,开态谓词随层登记。 - ---- - -## 用户场景预想评估(审计席专门裁断) - -用户预想:开始菜单做根场景 + 营地/冒险分开 + 冒险内分关卡/战斗两大块。 - -**裁断:方向支持,三条全部成立;需三处修正、一处明确反对。** - -1. **支持「开始菜单做根场景」**——修正:拆场景后 SaveManager/MetaProgression 不能再由 main.gd new() 出来(`main.gd:182-189`),必须提升为跨场景持有者(第三个 autoload 或根场景常驻子节点),否则换场景即丢档。RngService 已有先例,模式现成。 -2. **支持「营地与冒险分开」**——营地全部读写已收口在 MetaProgression/SaveManager 访问器(camp_shop_presenter 零直写),营地当前只是覆盖层(camp_overlay),升格独立场景不碰任何 core 接口,切缝天然干净。 -3. **支持「冒险内关卡/战斗分块」**——最有力的论据是域侧三层生命周期已经存在,场景层只是补镜像:`level_screen` 已是独立屏雏形(`main.gd:76,259-263`,屏级互斥单出口 `main.gd:978-990`),`_battle_root` 是现成战斗屏宿主(`main.gd:77,207`)。预想与代码现实几乎一一对齐。 -4. **明确反对一条**:不要借机把 40+ 个 views/*.gd 转成 .tscn。「程序化构建」与「场景切分」是两个独立决策——.tscn 根节点 + `_ready` 程序化内容完全合法;全面 .tscn 化会破坏全部探针契约(`board_view.gd:8-14` 对探针零改造契约)且收益为零。 -5. **迁移安全网**:13 个 smoke 探针中 10 个假设「挂载 main.tscn 即全应用」,拆分时这批探针的挂载假设同批迁移,它们就是切分作业的回归保障。建议时序:垂直切片验证期「只拆不重写」——root 壳 + menu.tscn + camp 场景 + adventure.tscn(内含 level_map/battle 两容器),新内容一律进新场景,战斗屏视图代码原样搬。 - ---- - -## 实现规范面补充结论与扫描器运行记录 - -**四大高危面几乎无坑、无 P0 命中**:信号面、Tween 生命周期、数值单一事实源、autoload 卫生。 - -**扫描器实跑记录**(实现席如实登记): -- 技能库 `auditor_audit_signals.py`(python 只读输出):74 处 lambda 连接(presentation 4 / smoke 62 / tests 8)。presentation 4 处逐一实读确认无泄漏(发射者与 lambda 同子树同生共死,refresh 先 queue_free 子树并清登记数组,`camp_overlay_view.gd:227-230`);smoke 62 处为一次性 headless 探针进程,不进游戏包。 -- 技能库 `auditor_audit_type_hints.py`:229 处(untyped Array 209 / Dictionary 20 / string connect 0);按目录 core 53 / data 9 / presentation 11 / smoke 133 / tests 23。 -- 手工 grep 等价检查(如实注明非扫描器):Godot 3 四参 connect 0;字符串 `.connect(` 0;`.disconnect(` 0;CONNECT_* 标志 0;绝对路径 get_node 4(见 P2-2);`Callable(self,"…")` 9(见 P2-3);call_deferred 10 处实读均为帧沿触发非初始化创可贴;await 1 处(dev 工具);全站 `_physics_process` 中 await 0;create_tween 20+ 处实读确认节点绑定+回调清理;print 3 处全在 dev 工具。 -- 跳过并注明:`auditor_audit_memory_fragmentation.gd`、`auditor_purge_report_generator.gd` 需 Godot 运行时,按只读纪律未跑。 -- 误报排除一例:battle_orchestrator 的 round_started/board_changed(:30-31)初判疑为死信号,实读确认 :447,488,1516 有发射、三层转发链全活,非发现。 - ---- - -## 做对了的事(防止报告纯负面失真) - -1. **信号严格向上,core 零反向依赖**:全 core/ 无一处 presentation 引用、无 get_node/树访问;runner 转发族用命名方法 Callable(`core/adventure_runner.gd:918-924` 等,头注写明禁 lambda 捕获 self 成环);信号连接唯一归组合根(ADR-0001 落实到位)。 -2. **Autoload 克制且纯净**:仅两个、纯数据/服务形态、无子节点无 UI 知识;RngService 统一单盐流 + 全事件日志(`core/rng_service.gd:95-108`)使全游戏同种子逐位复现——确定性架构的业内水准做法,也是本次全部重构验收手段的基础。 -3. **效果引擎护栏设计**:事件/效果/事实/算子四词表封闭常量 + 加载期 schema 校验致命 + 组合规则成文(`core/effect_engine.gd:24-91,362-433`),失败模式 fail-fast 而非静默。 -4. **读面成文快照收口**:presentation_snapshot()/diagnostic_snapshot() 成文出口替代链式穿透,协作对象私有化 + 明文豁免面——封装边界是写下来的契约而非默契。 -5. **Tween 生命周期纪律全站一致**:节点绑定 create_tween + tween_callback(queue_free) + is_instance_valid 守卫 + rebuild 重入先杀——技能库最常判死刑的陷阱本项目零命中。 -6. **测试策略真实分层**:25 个 headless 套件直接实例化 core 类、表值现读不写死;确定性 RNG 使「逐位一致」断言成为可能,这在随机系统里是稀缺的测试能力。 - ---- - -## 修复排期估算与裁决记录 - -**逐项估算**:场景拆分 + 局生命周期物化 + 探针迁移 ≈ 4-5d;`_settle` 拆段 ≈ 1.5d;共享账本 Ledger 类化 ≈ 1d;类型闸拉起 + 73 处收口 ≈ 1.5d;组合根可测化抽取 ≈ 1-1.5d(场景拆分后重估);模态协议内聚 ≈ 0.5-1d;卫生件包 ≈ 1.5d。**合计约 12-14 个工作日。** - -**裁决记录(2026-10-14 用户)**:全部技术债修复**允许放行单一大迭代**,12-14 个工作日在允许时间范围内。 - -**批内执行顺序约束**(审计席建议):①场景拆分与局生命周期先行(P0 根因,且决定 main.gd 终态形状);②战斗域重构(_settle 拆段 + Ledger 类化)随后;③组合根抽取在场景拆分后重估再动;④类型闸收尾(其他重构落地后才拉得起 Error 级);⑤卫生件随批顺手带。最大两件(场景拆分动 main.gd 1385 行 / _settle 拆段动战斗结算心脏)各自需要全量冒烟回归,同批内应分波提交、逐波全绿,避免回归时二分定位跨双巨改。 - -**核心验收手段**:确定性 RNG 架构给了杀手级验收——同种子逐位一致回放(重构前后跑完整局输出逐位一致);场景批验收 = 10 个探针迁移后全绿 + run_all 25 套;战斗域批验收 = 逐位一致 + 冒烟全量。 - -**写卡前待裁三点**(grilling 时闭):①SaveManager/MetaProgression 升格形态(第三个 autoload vs 根场景常驻节点);②共享账本 Ledger 类接口口径;③`_settle` 段边界切法。 diff --git a/design/research/需求卡片-代码架构技术债修复批-2026-10-14.md b/design/research/需求卡片-代码架构技术债修复批-2026-10-14.md index 5aeddf87..4c37b96e 100644 --- a/design/research/需求卡片-代码架构技术债修复批-2026-10-14.md +++ b/design/research/需求卡片-代码架构技术债修复批-2026-10-14.md @@ -1,7 +1,7 @@ # 需求卡片:代码架构技术债修复批(场景拆分 · 局生命周期 · 战斗结算拆段 · 账本收口 · 类型闸 · 卫生件) - 版本:v1.2(2026-10-14 成文 v1.0;同日用户令**编入迭代 27 卡①**——roadmap 迭代行已更新,M4 数值批顺延 28+。v1.1→v1.2:**§10 三点待裁全裁**(2026-10-14 用户三裁,全按推荐案:①第三个 autoload/②单类持四账/③审计五段)——零待裁点,卡片成立) -- 来源:[代码架构与实现质量审计报告-2026-10-14](代码架构与实现质量审计报告-2026-10-14.md)(双席审计,基线 master 97de02c;**同日已经第三方逐条复核:主体结论全部成立,五处计数级瑕疵已随批修正落文**) +- 来源:代码架构与实现质量审计报告-2026-10-14(双席审计,基线 master 97de02c;同日第三方逐条复核五处计数级瑕疵修正落文;**该报告已按退役规则于本批收口 git rm——吸收即死,修正后计数由本卡承载,收口结案=design/qa/iter27-实现报告-2026-10-14.md**) - 归属:工程债/重构批(凭审计证据直接写卡,无设计定稿前置——2026-10-10 立规:缺陷修复/工程债/重构类工程需求无设计前置要求) - 排期:**已编入迭代 27 卡①**(2026-10-14 用户令;用户裁决记录:全部技术债修复允许放行单一大迭代,12-14 个工作日在允许时间范围内;**开工门=U0 已满足**——三点已裁(见版本行)+用户 /goal+iter 27 发令,U0=2026-10-14 开工;M4 数值批顺延 28+) diff --git a/game/core/action_economy.gd b/game/core/action_economy.gd index 28bea1dd..a9ca8e0f 100644 --- a/game/core/action_economy.gd +++ b/game/core/action_economy.gd @@ -30,7 +30,7 @@ var _rap := 0 var rap: int: get: return _rap var _last_round_end: Dictionary[String, String] = {} # {path_tag, continuation_hint}——排雷玩法 组包 round_end 的合法数据源 -var last_round_end: Dictionary: +var last_round_end: Dictionary[Variant, Variant]: get: return _last_round_end var _settlement_consumed := false # AC-14 门:notify_settlement_done 消费后才受理 round_start var _next_round_mode := "" # 调试观测量(同板续轮 / 换新图) diff --git a/game/core/adventure_runner.gd b/game/core/adventure_runner.gd index 083de223..d76be229 100644 --- a/game/core/adventure_runner.gd +++ b/game/core/adventure_runner.gd @@ -54,9 +54,13 @@ signal wallet_changed(event: String, gp: int, extra: Dictionary) var db: ConfigDb var rng: RngServiceCore -var state := "idle" # idle / running / settling(冒险结构.md 三态) +enum State { IDLE, RUNNING, SETTLING } # 会话状态机(迭代 27 T6——裸字符串 enum 化:内部 enum,外部 String 契约 getter 词表逐位一致) +var _state: State = State.IDLE +var state: String: # 外部契约面(只读——外部零写点实证;"idle"/"running"/"settling" 零变化) + get: + return State.keys()[_state].to_lower() var ledger := {"hero_hp": 0, "gp": 0} # 冒险账本(生命 + GP 钱包——经 start_level_external 注入 runner 共享) -var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 统一冒险加算区(商店购买与三选一属性件同一本账;四轴 +ap——迭代 10 奖励重构) +var run_ledger := RunLedger.new() # 共享四本账(迭代 27 T3——P1-4 收口:attune_charges/vision_counter/adventure_counters/bonus 单类持账;裁点②甲) var purchased := {} # product_id → 已购档数(跨关延续、冒险收束清零——递增定价输入) var level_index := 0 # 当前关卡序号(1 起) var levels_total := 0 # 序列总关数(读 level 表行数) @@ -75,13 +79,10 @@ var _base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} var _first_script := false # 首次脚本模式(关卡节点图生成 v0.4——true = 全关零抽签取池内模板 0;测量夹具恒 false) # 永久强化 flat 注入(元进度实现批——冒险开始参数面 params.hero_override 存入,共享给 LevelRunner;全零 = 白板) var _last_treasure_cleared := 0 # additive(迭代 7 T4——最近一次关卡末清空计数,summary 取数面) var _treasure_unlocked := {} # aux 解锁门控集(迭代 7 T5——params.treasure_unlocked 接收面) -var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——params.attunement 接收面,start_level 外下传 LevelRunner) +var _attunement: Array[Variant] = [] # 同调装配集(迭代 11 第①批 T5——params.attunement 接收面,start_level 外下传 LevelRunner) var _attune_refresh_x := 0 # 刷新件余量(迭代 11 第①批 T5b P1 甲——每碰三选一重置=X 表值;优先消耗、不结转) var _attune_refresh_y := 0 # Y 冒险周期池(开局参数面注入,用完即无——营地升级行表值) var _curio_paid_refreshes := 0 # 本次停点已付费刷新次数(2026-10-06 用户令对齐商店 MC-1——GP 刷新价曲线输入,随停点创建清零=与商店进店清零同构) -var attune_charges := {} # 同调主动件冒险充能账本(迭代 11 第①批 T6——跨关卡共享 dict;settlement use_treasure 扣减直改同引用;断点随 attunement_charges 键入档) -var vision_counter := {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——跨关卡共享 dict;编排器受理成功 +1;断点随 vision_counter_used 键入档,收束聚合入档 save.add_vision_used) -var adventure_counters := {} # adventure 域计数器账本(迭代 20 T6——攒层/付费计数等跨战持久件账;效果引擎 rebuild 回灌/战末导出回写共享引用;断点随 adventure_counters 键入档) # ---- 凭证件三本新账(迭代 20 T4——奇物.md 凭证件表新 5 件;F-1 刷新写错账修复承载面)---- var _refresh_stock := 0 # 刷新存量账(冒险级——刷新券加仓;**不经 _attune_refresh_x**:X 每停点重置会吞存量=旧写错账缺陷 F-1) var _pending_rap := 0 # 储备金袋待注入(节点级——下一战 RAP +N,战首注入后清零) @@ -92,12 +93,12 @@ var _save_manager = null # 断点承载注入(迭代 8 卡① var _resume_bp := {} # 续玩快照暂存(resume_adventure → _start_current_level 单跳传递) # ---- 三选一停点态(迭代 10 奖励重构批)---- var _curio_taken := {} # 件获取次数计数(kind 维度派生账本——断点附随件与摘要读数承载;池库存正主=_curio_deck) -var _curio_deck: Array = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账;迭代 12 T1 起规则改写型特性带=relic 令牌随表追加) +var _curio_deck: Array[Variant] = [] # 令牌池(TP0 v2.2——持久多重集,元素 {"kind","tier"};洗牌抽候选/选中踢池未选回池/池耗即枯竭;商店货架同库共账;迭代 12 T1 起规则改写型特性带=relic 令牌随表追加) var _feats: Array[String] = [] # 特性持有集(迭代 12 T1——局内随机渠道获得、冒险级跨关携带、收束清零;🔴 层级纪律承载面:不入同调位不入局外树) -var _tree_rules: Array = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) +var _tree_rules: Array[Variant] = [] # 树 rule 键集(迭代 12 T10——Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面) var _curio_pending := {} # 停点态:{"candidates": Array, "payload": Dictionary, "chosen"/"incoming": 替换在途}——空 = 无停点 var _curio_result := {} # 已兑付待推进态(curio_resolved 载荷副本——中断续玩不重兑付) -var _keystone_drops: Array = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源) +var _keystone_drops: Array[Variant] = [] # Boss 逐杀掉落明细(additive 摘要键 keystone_drops——结算明细/复盘归因数据源) var _node_curio_pending := {} # 奖励节点三选一停点态(迭代 10 追加批——共享池;断点不载:续玩重建到选路停点重走,恢复的计数器保证同候选重抽) var _feat_ticket_pity := 0 # 门票保底计数(T5——展示次数:节点开面/货架抽签/刷新重展各计一次;触发清零) var _feat_ticket_sold := 0 # 门票限购计数(T5——选中/买下即 +1;拿到即开不进持有集) @@ -123,7 +124,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: master_seed = seed_val rng.new_adventure(seed_val) ledger = {"hero_hp": 0, "gp": 0} - bonus = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} + run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} purchased = {} keystone = 0 summary = {} @@ -140,23 +141,23 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool: _attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive(迭代 11 第①批 T5) _attune_refresh_y = maxi(0, int(params.get("attune_refresh_y", 0))) # additive(迭代 11 第①批 T5b——Y 冒险周期池) _seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义) - vision_counter = {"used": 0} # 迭代 22 秘宝批 T3——真视使用计数冒险开始归零(跨局累计归生涯档) + run_ledger.vision_counter = {"used": 0} # 迭代 22 秘宝批 T3——真视使用计数冒险开始归零(跨局累计归生涯档) # 刷新练习开局注入(迭代 22 秘宝批 T3——refresh_stock_grant 装配即得一次性加仓: # 装配集 kind 分发扫描;只入存量账 _refresh_stock、临时账不动=V8 契约;resume 不重注入〔存量随断点已在〕) for iid_v2 in _attunement: - var rp_row: Dictionary = db.treasure_item_row(String(iid_v2)) + var rp_row: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v2)) if (rp_row.get("effect_kinds", []) as Array).has("refresh_stock_grant"): _refresh_stock += int(rp_row.get("magnitude", 0)) # 特性开局注入(迭代 12 T7——MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派 # 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕) - var feat_seed: Array = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else [] + var feat_seed: Array[Variant] = params.get("relics_seed", []) if params.get("relics_seed", []) is Array else [] _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # 迭代 12 T10——A/B 面 rule 键集 if not feat_seed.is_empty(): _feats = [] for r in feat_seed: _feats.append(String(r)) ledger["hero_hp"] = effective_max_hp() # 生命账本置满(生效上限口径——T17 修复:含调试浮窗 pct,与战斗内同源) - state = "running" + _state = State.RUNNING return _start_current_level() @@ -182,8 +183,8 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: master_seed = int(bp.get("master_seed", 0)) rng.restore_adventure(master_seed, int(bp.get("rng_counter", 0))) ledger = {"hero_hp": int(bp.get("hero_hp", 0)), "gp": int(bp.get("gp", 0))} - var bp_bonus: Dictionary = bp.get("bonus", {}) as Dictionary - bonus = {"atk": maxi(0, int(bp_bonus.get("atk", 0))), + var bp_bonus: Dictionary[Variant, Variant] = bp.get("bonus", {}) as Dictionary + run_ledger.bonus = {"atk": maxi(0, int(bp_bonus.get("atk", 0))), "max_hp": maxi(0, int(bp_bonus.get("max_hp", 0))), "armor_class": maxi(0, int(bp_bonus.get("armor_class", 0))), "ap": maxi(0, int(bp_bonus.get("ap", 0)))} @@ -194,7 +195,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: level_index = int(bp.get("level_index", 1)) _pending_next_level = false _reset_curio_state() - var bp_taken: Dictionary = bp.get("bonus_taken", {}) as Dictionary + var bp_taken: Dictionary[Variant, Variant] = bp.get("bonus_taken", {}) as Dictionary for id in _curio_taken.keys(): _curio_taken[id] = maxi(0, int(bp_taken.get(id, 0))) for id in bp_taken.keys(): @@ -203,9 +204,9 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: # 迭代 12 T1——relic 令牌 feat_id 随还原) var bp_deck = bp.get("bonus_deck", null) if bp_deck is Array: - var restored: Array = [] + var restored: Array[Variant] = [] for t in (bp_deck as Array): - var tok: Dictionary = t as Dictionary + var tok: Dictionary[Variant, Variant] = t as Dictionary var rtok := {"kind": String(tok.get("kind", "")), "tier": String(tok.get("tier", ""))} if String(rtok["kind"]) == "relic": rtok["feat_id"] = String(tok.get("feat_id", "")) @@ -234,21 +235,21 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: _seed_attune_charges() # 迭代 11 第①批 T6——冒险开始充能账本按装备主动件重置(每冒险充能语义) _tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # 迭代 12 T10——A/B 面 rule 键集(尾裁微批b 2026-10-08 卡⓪受理闭环:续玩参数面同 start_adventure :135 同式,消费面 set_tree_rules 下传现成零改) # 三选一停点续玩分支(迭代 10——选择态/已兑付态不重跑关卡;奖励选择态不重抽、已兑付不重付) - var bp_pending: Dictionary = bp.get("bonus_pending", {}) as Dictionary + var bp_pending: Dictionary[Variant, Variant] = bp.get("bonus_pending", {}) as Dictionary _feat_ticket_pity = int(bp.get("feat_ticket_pity", 0)) # T5 扩列——旧档缺键缺省 0(存档兼容) _feat_ticket_sold = int(bp.get("feat_ticket_sold", 0)) # 迭代 20 T6/T7——adventure 域计数器回填(缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先) var bp_counters = bp.get("adventure_counters", null) if bp_counters is Dictionary: - adventure_counters = (bp_counters as Dictionary).duplicate(true) + run_ledger.adventure_counters = (bp_counters as Dictionary).duplicate(true) _refresh_stock = maxi(0, int(bp.get("refresh_stock", 0))) # 迭代 20 T4/T7——旧档缺键缺省 0 - vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0) + run_ledger.vision_counter = {"used": maxi(0, int(bp.get("vision_counter_used", 0)))} # 迭代 22 秘宝批 T3——真视计数随断点回填(缺键=旧断点缺省 0) _battery_pending = maxi(0, int(bp.get("battery_pending", 0))) - var bp_result: Dictionary = bp.get("bonus_result", {}) as Dictionary + var bp_result: Dictionary[Variant, Variant] = bp.get("bonus_result", {}) as Dictionary if not bp_pending.is_empty(): _pending_next_level = true # 候选令牌化迁移(TP0——旧版断点候选为 kind 字符串,按普通档令牌防御归一;单版本周期口径) - var migrated: Array = [] + var migrated: Array[Variant] = [] for c in (bp_pending.get("candidates", []) as Array): if c is Dictionary: # T4——feat_id 随迁移(通关渠道候选=纯专长令牌,身份键 relic|tier|feat_id 三段全要) @@ -259,22 +260,22 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool: else: migrated.append({"kind": String(c), "tier": db.adventure.TIER_COMMON}) _curio_pending = {"candidates": migrated} - var payload: Dictionary = (bp_pending.get("payload", {}) as Dictionary).duplicate(true) - state = "running" + var payload: Dictionary[Variant, Variant] = (bp_pending.get("payload", {}) as Dictionary).duplicate(true) + _state = State.RUNNING curio_choice_presented.emit(payload) return true if not bp_result.is_empty(): _pending_next_level = true _curio_result = bp_result.duplicate(true) - state = "running" + _state = State.RUNNING curio_resolved.emit(_curio_result.duplicate(true)) return true _resume_bp = bp.duplicate(true) # 迭代 11 第①批 T6——充能账本随断点回局(缺键 = 旧档/无断点,保持 _seed_attune_charges 种子态) var bp_charges = bp.get("attunement_charges", null) if bp_charges is Dictionary: - attune_charges = (bp_charges as Dictionary).duplicate(true) - state = "running" + run_ledger.attune_charges = (bp_charges as Dictionary).duplicate(true) + _state = State.RUNNING return _start_current_level() @@ -288,7 +289,7 @@ func _reset_curio_state() -> void: _curio_result = {} _keystone_drops = [] _node_curio_pending = {} - adventure_counters = {} + run_ledger.adventure_counters = {} _refresh_stock = 0 _pending_rap = 0 _battery_pending = 0 @@ -302,7 +303,7 @@ func _reset_curio_state() -> void: # 回退映射 rarest→rare/其余→excellent,T3 行换代后回退分支自然休眠);relic 入普通档随新档位框架 # 解禁〔旧「永不入普通档」系 TP0 三档时代约束,四级框架未沿用——奇物.md 池结构节无此限制〕) func _build_curio_deck() -> Array: - var deck: Array = [] + var deck: Array[Variant] = [] for kind in db.adventure.CURIO_KINDS: for tier in db.adventure.TIERS: for i in int(db.adventure.curio_quota(String(kind), String(tier))): @@ -333,12 +334,12 @@ func set_hero_override(atk_pct: int, hp_pct: int) -> void: # 逐字一致。旧口径两处只算 flat 不乘 pct:调生命上限 26→104 时开局血仍 26(账本 26 / 战斗内 104)、 # 通关 20% 回血封顶按 26 算。pct=0(模拟/冒烟默认)时乘法恒等——MC 锚零漂移。 func effective_max_hp() -> int: - var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + int(bonus.get("max_hp", 0)) + var base: int = int(db.hero.max_hp) + int(_base_override.get("max_hp", 0)) + int(run_ledger.bonus.get("max_hp", 0)) return maxi(1, int(round(float(base) * (100.0 + float(_override[1])) / 100.0))) # 断点承载注入(迭代 8 卡①——组合根冒险开始时调用;模拟/冒烟不注入 = 断点面整体静默) -func set_save_manager(sm) -> void: +func set_save_manager(sm: SaveManagerCore) -> void: _save_manager = sm @@ -385,7 +386,7 @@ func skip_empty_curio() -> bool: # 候选令牌匹配(身份键恒等——属性加成型特性 kind+tier、规则改写型特性 feat_id;迭代 12 T1 规则改写型特性带) func _find_token(candidates: Array, kind: String, tier: String, feat_id: String = "") -> Dictionary: for t in candidates: - var tok: Dictionary = t as Dictionary + var tok: Dictionary[Variant, Variant] = t as Dictionary if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier: continue if kind == "relic" and String(tok.get("feat_id", "")) != feat_id: @@ -402,7 +403,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool: if state != "running" or _curio_pending.is_empty(): push_error("[冒险结构] pick_curio 拒绝——无待选停点(state=%s)" % state) return false - var candidates: Array = _curio_pending.get("candidates", []) as Array + var candidates: Array[Variant] = _curio_pending.get("candidates", []) as Array if _find_token(candidates, kind, tier, feat_id).is_empty(): push_error("[冒险结构] pick_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id]) return false @@ -421,7 +422,7 @@ func pick_curio(kind: String, tier: String = "", feat_id: String = "") -> bool: func _feat_payload_keys(feat_id: String) -> Dictionary: if feat_id == "": return {} - var row: Dictionary = db.feat_row(feat_id) + var row: Dictionary[Variant, Variant] = db.feat_row(feat_id) return {"feat_id": feat_id, "feat_name": String(row.get("display_name", "")), "feat_archetype": String(row.get("archetype", ""))} @@ -467,7 +468,7 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = # adventure_table.CURIO_APPLY_ROUTES 声明路由(bonus 落点键),加新加算类令牌零代码) var sink := db.adventure.curio_apply_route(kind) if sink != "": - bonus[sink] = int(bonus.get(sink, 0)) + value + run_ledger.bonus[sink] = int(run_ledger.bonus.get(sink, 0)) + value _curio_taken[kind] = int(_curio_taken.get(kind, 0)) + 1 return true # 通道面(副作用形态——显式分支) @@ -487,7 +488,7 @@ func _apply_curio_pick(kind: String, tier: String, value: int, feat_id: String = # 从牌库摘除一枚令牌(TP0——选中踢池单点;迭代 12 T1 relic 按 feat_id 精确摘;找不到 = false) func _consume_deck_token(kind: String, tier: String, feat_id: String = "") -> bool: for i in _curio_deck.size(): - var tok: Dictionary = _curio_deck[i] as Dictionary + var tok: Dictionary[Variant, Variant] = _curio_deck[i] as Dictionary if String(tok.get("kind", "")) != kind or String(tok.get("tier", "")) != tier: continue if kind == "relic" and String(tok.get("feat_id", "")) != feat_id: @@ -559,7 +560,7 @@ func _finish_curio_pick(picked: String, value: int, hp_before: int, tier: String # 候选呈现(节点到达时调用——恰一 reward_pick scope 洗牌 + 道具件掷定 scope;本停点断点不载, # 续玩重建到选路停点后重走重抽,恢复的全局计数器保证同候选逐位复现)。 func node_curio_candidates() -> Dictionary: - var cands: Array = _draw_curio_candidates("reward_node") + var cands: Array[Variant] = _draw_curio_candidates("reward_node") return {"candidates": cands} @@ -576,7 +577,7 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> if _node_curio_pending.is_empty(): push_error("[冒险结构] apply_node_curio 拒绝——无待选停点") return false - var candidates: Array = _node_curio_pending.get("candidates", []) as Array + var candidates: Array[Variant] = _node_curio_pending.get("candidates", []) as Array if _find_token(candidates, kind, tier, feat_id).is_empty(): push_error("[冒险结构] apply_node_curio 拒绝——令牌不在候选: %s|%s|%s" % [kind, tier, feat_id]) return false @@ -590,11 +591,11 @@ func apply_node_curio(kind: String, tier: String = "", feat_id: String = "") -> # 冒险开始充能账本重置(T6——装备 active_charged 件各回 charges_per_adventure;被动件不入账) func _seed_attune_charges() -> void: - attune_charges = {} + run_ledger.attune_charges = {} for iid in _attunement: - var arow: Dictionary = db.treasure_item_row(String(iid)) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid)) if String(arow.get("form", "")) == "active_charged": - attune_charges[String(iid)] = maxi(0, int(arow.get("charges_per_adventure", 0))) + run_ledger.attune_charges[String(iid)] = maxi(0, int(arow.get("charges_per_adventure", 0))) # ---- 三选一刷新(迭代 11 第①批 T5b——P1 甲 X/Y 双源:X=刷新件每碰三选一重给〔优先消耗、不结转〕、 @@ -606,7 +607,7 @@ func _attune_refresh_x_total() -> int: # 的行 magnitude 合计;刷新件类加件零 id 分支) var total := 0 for iid_v in _attunement: - var arow: Dictionary = db.treasure_item_row(String(iid_v)) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has("bonus_refresh"): total += int(arow.get("magnitude", 0)) return total @@ -635,12 +636,12 @@ func choose_battery_target(item_id: String) -> bool: if not _attunement.has(item_id): push_error("[冒险结构] 电池分配拒绝——件不在装配集: %s" % item_id) return false - var arow: Dictionary = db.treasure_item_row(item_id) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id) if String(arow.get("form", "")) != "active_charged": push_error("[冒险结构] 电池分配拒绝——非主动充能件: %s" % item_id) return false _battery_pending -= 1 - attune_charges[item_id] = int(attune_charges.get(item_id, 0)) + 1 + run_ledger.attune_charges[item_id] = int(run_ledger.attune_charges.get(item_id, 0)) + 1 return true @@ -696,16 +697,16 @@ func _refresh_curio_common(paid: bool) -> bool: else: _refresh_stock -= 1 if not _curio_pending.is_empty(): - var cands: Array = _draw_curio_candidates("level_clear") + var cands: Array[Variant] = _draw_curio_candidates("level_clear") _curio_pending["candidates"] = cands.duplicate(true) - var payload: Dictionary = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true) + var payload: Dictionary[Variant, Variant] = (_curio_pending.get("payload", {}) as Dictionary).duplicate(true) payload["candidates"] = cands payload["refreshed"] = true # 重发载荷带标——呈现层直落选择面,不再重开胜利总结面(2026-10-06) curio_choice_presented.emit(payload) # 同信号重 present(零自动推进契约不破——停点仍在途) _snapshot_breakpoint() return true if not _node_curio_pending.is_empty(): - var cands2: Array = _draw_curio_candidates("reward_node") + var cands2: Array[Variant] = _draw_curio_candidates("reward_node") _node_curio_pending["candidates"] = cands2.duplicate(true) return true return false @@ -746,14 +747,14 @@ func _draw_curio_candidates(channel: String = "reward_node") -> Array: var k := String((tok as Dictionary).get("kind", "")) return k != "relic" and k != "feat_ticket") pool = pool.filter(func(tok): - var t: Dictionary = tok as Dictionary + var t: Dictionary[Variant, Variant] = tok as Dictionary return _curio_cap_ok(String(t.get("kind", "")), String(t.get("feat_id", "")))) var rg: RandomNumberGenerator = rng.open_stream("reward_pick") if channel == "level_clear": # 迭代 20 T5——Boss 渠道按档配比加权抽取(无放回加权抽:逐席按 boss_feat_weight 掷点, # 身份去重照旧;rarity 语义约束兑付面——奇物.md v3.1,配比数值归调参批) - var out_w: Array = [] - var seen_w: Array = [] + var out_w: Array[Variant] = [] + var seen_w: Array[Variant] = [] var want_w := int(db.adventure.curio_candidate_count) while out_w.size() < want_w and not pool.is_empty(): var total_w := 0 @@ -768,7 +769,7 @@ func _draw_curio_candidates(channel: String = "reward_node") -> Array: if roll_w <= 0: pick_i = i_w break - var tok_p: Dictionary = pool[pick_i] + var tok_p: Dictionary[Variant, Variant] = pool[pick_i] var ident_p := _token_identity(tok_p) if seen_w.has(ident_p): pool.remove_at(pick_i) # 同身份已出——剔牌重掷(无放回) @@ -791,13 +792,13 @@ func _draw_curio_candidates(channel: String = "reward_node") -> Array: var tmp2 = sub[i] sub[i] = sub[j2] sub[j2] = tmp2 - var out: Array = [] - var seen: Array = [] + var out: Array[Variant] = [] + var seen: Array[Variant] = [] # 收集器:全池序取 n_full 格(普通档打底)→ 子池序取 1 格(曝光槽)→ 子池空回退全池续取 var want := int(db.adventure.curio_candidate_count) var n_full := maxi(1, want - 1) if want >= 2 else want while out.size() < want: - var src: Array = pool + var src: Array[Variant] = pool if out.size() >= n_full and not sub.is_empty(): src = sub # 曝光槽位——优先子池 var placed := false @@ -828,7 +829,7 @@ func _draw_curio_candidates(channel: String = "reward_node") -> Array: func _curio_cap_ok(kind: String, feat_id: String = "") -> bool: var cap := 99 if kind == "relic": - var row: Dictionary = db.feat_row(feat_id) + var row: Dictionary[Variant, Variant] = db.feat_row(feat_id) cap = int(row.get("cap", 1)) if not row.is_empty() else 1 return _curio_taken.get("relic:" + feat_id, 0) < cap if not db.adventure.CURIO_KINDS.has(kind): @@ -860,7 +861,7 @@ func _feat_ticket_consider(rg: RandomNumberGenerator) -> bool: func _redeem_feat_ticket() -> bool: _feat_ticket_sold += 1 _feat_ticket_pity = 0 - var cands: Array = _draw_curio_candidates("level_clear") # 专长池(与通关渠道同白名单) + var cands: Array[Variant] = _draw_curio_candidates("level_clear") # 专长池(与通关渠道同白名单) if cands.is_empty(): push_error("[冒险结构] 门票兑付失败——专长池空集") _feat_ticket_sold -= 1 @@ -868,7 +869,7 @@ func _redeem_feat_ticket() -> bool: if not _node_curio_pending.is_empty(): _node_curio_pending["candidates"] = cands.duplicate(true) return true - var payload: Dictionary = {"context": "feat_ticket", "candidates": cands.duplicate(true)} + var payload: Dictionary[Variant, Variant] = {"context": "feat_ticket", "candidates": cands.duplicate(true)} _curio_pending = {"candidates": cands.duplicate(true), "payload": payload.duplicate(true)} curio_choice_presented.emit(payload) _snapshot_breakpoint() @@ -882,7 +883,7 @@ func _boss_keystone_drop(li: int) -> void: var first := true if _save_manager != null: first = not _save_manager.boss_first_kill_marked(li) - var row: Dictionary = db.meta_boss_keystone_row(li) + var row: Dictionary[Variant, Variant] = db.meta_boss_keystone_row(li) var amount: int = int(row.get("first_kill", 0)) if first else int(row.get("repeat_kill", 0)) _keystone_drops.append({"level": li, "first": first, "amount": amount}) keystone += amount @@ -908,7 +909,7 @@ func abandon() -> bool: # 独立成口=V10 断言面直呼) func _reset_tree_limited_counters() -> void: for cid in ["tree_sense_sharp_used", "tree_sense_guard_used"]: - adventure_counters.erase(cid) + run_ledger.adventure_counters.erase(cid) func _start_current_level() -> bool: @@ -925,7 +926,7 @@ func _start_current_level() -> bool: _runner.set_hero_override(int(_override[0]), int(_override[1])) # 断点续玩分支(迭代 8 卡①):模板随快照强制(已抽模板零重抽零 RNG)、resume_state 带 # 停点节点与已到序——重建到停点后经 present_resumed_choice 呈现(信号顺序契约同入口)。 - var resume_state: Dictionary = {} + var resume_state: Dictionary[Variant, Variant] = {} var template_pick: int = 0 if _first_script else -1 # 首次脚本 = 每关固定模板 0 零抽签(v0.4 拍板点①);否则逐关抽签 if not _resume_bp.is_empty(): resume_state = {"node_id": int(_resume_bp.get("node_id", -1)), @@ -936,20 +937,19 @@ func _start_current_level() -> bool: # 停点前移批 2026-09-23:defer_entry=true 起每关(含首关)——先 emit level_started(呈现层在 # 该回调重取 runner 实例),后入口候选呈现;顺序反了选路口会被呈现层的 runner 空防御静默丢弃。 # 呈现零 RNG 消耗、pick 走同一 _enter_node——测量面逐位零漂移。 - var ok: bool = _runner.start_level_external(level_index, ledger, bonus, purchased, - Callable(self, "apply_rest"), _base_override, true, template_pick, resume_state) + _runner.set_run_ledger(run_ledger) # 迭代 27 T3——共享四账单类下传(原四 setter/参数散传收口;run_ledger=四账类,ledger=关卡账沿用旧名) + _runner.sync_attune_inventory() # 迭代 11 第①批 T6 原语义——能力件骑库存面注入(随账下传同拍) + var ok: bool = _runner.start_level_external(level_index, ledger, purchased, + apply_rest, _base_override, true, template_pick, resume_state) if ok: _runner.set_attunement(_attunement) # 迭代 11 第①批 T5——同调装配下传(enc 携带进战斗结算) - _runner.set_attune_charges(attune_charges) # 迭代 11 第①批 T6——充能账本下传(能力件骑库存面注入) - _runner.set_vision_counter(vision_counter) # 迭代 22 秘宝批 T3——真视使用计数共享 dict 下传(编排器受理成功 +1) _runner.set_feats(_feats) # 迭代 12 T1——特性持有集下传(begin_battle enc.feats 求值面) _runner.set_tree_rules(_tree_rules) # 迭代 12 T10——树 rule 键集下传(A/B 面消费) _runner.set_hero_axes(hero_attack_type, hero_armor_type) # 迭代 23 T4——两根类型轴快照下传(effective_ac 甲型基础 AC 层+enc 携带进 spawn) if _shop_mode == "pool": # 商店共享池注入(迭代 11 第②批 T3——库存账本引用+消费 handler 双下传;legacy 模式零注入零扰动。 # 迭代 20 T2——cap 检查回调四参注入) - _runner.set_shop_pool(_curio_deck, Callable(self, "_consume_pool_purchase"), Callable(self, "_feat_ticket_consider"), Callable(self, "_curio_cap_ok")) - _runner.set_adventure_counters(adventure_counters) # 迭代 20 T6——adventure 域计数器共享引用下传 + _runner.set_shop_pool(_curio_deck, _consume_pool_purchase, _feat_ticket_consider, _curio_cap_ok) # 迭代 20 T4——凭证件三账注入(节点级/回合级一次性:注入即清零,未用即过期=寿命档语义) _runner.set_voucher_grants(_pending_rap, _time_sand_queue, _pending_insurance) _pending_rap = 0 @@ -997,7 +997,7 @@ func _on_level_finished(payload: Dictionary) -> void: _pending_next_level = true _reset_refresh_x() # 迭代 11 第①批 T5b——碰三选一重置 X 余量(不结转) _curio_paid_refreshes = 0 # 付费刷计数随停点清零(对齐商店进店清零——2026-10-06) - var pick_payload: Dictionary = { + var pick_payload: Dictionary[Variant, Variant] = { "level_index": level_index, "candidates": _draw_curio_candidates("level_clear"), # T4 渠道白名单——关底 Boss 击败=通关渠道只出专长 "heal_gained": heal, @@ -1028,7 +1028,7 @@ func _settle(result: String) -> void: "master_seed": master_seed, "aux_cleared_last": _last_treasure_cleared, # additive 扩签(迭代 7 T4——末关清空反馈;非末关走 bonus 载荷) "keystone_drops": _keystone_drops.duplicate(true), # additive 扩签(迭代 10——逐杀掉落明细;settle 入账明细/复盘归因数据源) - "vision_used_delta": int(vision_counter.get("used", 0)), # additive 扩签(迭代 22 秘宝批 T3——真视使用本局计数;settle 聚合入档) + "vision_used_delta": int(run_ledger.vision_counter.get("used", 0)), # additive 扩签(迭代 22 秘宝批 T3——真视使用本局计数;settle 聚合入档) "pool_stock": _pool_stock_snapshot(), # additive 扩签(迭代 11 第②批 T5——终局池读数:清账前快照,商店买走 vs 奖励领走 vs 枯竭的终局面) "relics": _feats.duplicate(), # additive 扩签(迭代 12 T1——特性终局持有集:清账前快照,成型判读取数面) } @@ -1041,14 +1041,15 @@ func _settle(result: String) -> void: "node_type": String(_last_level_payload.get("node_type", "")), } if _runner != null: + summary["gp_final"] = int(_runner.gp) # gp_final 清账前快照(迭代 27 T3——P1-3 GP 影子账收口:终局面板取数源,影子账随批退役) _runner.close_wallet() # GP 冒险收束清零(三结局都清——经转发发 wallet_changed "clear" 供日志/呈现) _reset_curio_state() # 三选一停点态收束清零(结算受理只算一次语义——重复宣布零变化) if _save_manager != null: _save_manager.clear_breakpoint() # 三结局清断点(迭代 8 卡①/T3——退出冒险=放弃流程+清除断点) _save_manager.write_to_disk() - state = "settling" + _state = State.SETTLING adventure_finished.emit(summary) - state = "idle" + _state = State.IDLE # ---- 转发族(签名与 LevelRunner 同款——呈现层对接透明)---- @@ -1070,26 +1071,26 @@ func _forward_choices_presented(candidates: Array) -> void: func _snapshot_breakpoint() -> void: if _save_manager == null or state != "running": return - var treasure_snapshot: Array = [] + var treasure_snapshot: Array[Variant] = [] if _runner != null and _runner.treasure_inventory != null: treasure_snapshot = _runner.treasure_inventory.snapshot_inventory() # 三选一停点续玩分支 _runner 为 null(关卡已收束、下一关未开)——节点域字段按缺省落档, # 该两态的续玩只消费 bonus_* 附随件(选择停点/已兑付停点快照,迭代 10) - var bp: Dictionary = { + var bp: Dictionary[Variant, Variant] = { "level_index": level_index, "node_id": _runner.current_node_id() if _runner != null else -1, "hero_hp": int(ledger.get("hero_hp", 0)), "bonus": { - "atk": int(bonus.get("atk", 0)), - "max_hp": int(bonus.get("max_hp", 0)), - "armor_class": int(bonus.get("armor_class", 0)), - "ap": int(bonus.get("ap", 0)), + "atk": int(run_ledger.bonus.get("atk", 0)), + "max_hp": int(run_ledger.bonus.get("max_hp", 0)), + "armor_class": int(run_ledger.bonus.get("armor_class", 0)), + "ap": int(run_ledger.bonus.get("ap", 0)), }, "gp": int(ledger.get("gp", 0)), "shop_purchased": purchased.duplicate(true), "shop_shelf": _runner.shop_bp_shelf() if _runner != null else [], # 迭代 11 第②批 T2——P2 甲货架入断点(离开店/非店停点 = 空架合法态) "shop_visit_buys": _runner.shop_bp_visit_buys() if _runner != null else 0, # 迭代 11 第②批 T2——本次进店已购次数(A 链定价输入,P2 甲扩列) - "attunement_charges": attune_charges.duplicate(true), # 迭代 11 第①批 T6——同调主动件充能余量随断点(save v4 域) + "attunement_charges": run_ledger.attune_charges.duplicate(true), # 迭代 11 第①批 T6——同调主动件充能余量随断点(save v4 域) "template_pick": _runner.active_template_index if _runner != null else -1, "master_seed": master_seed, "rng_counter": rng.counter(), @@ -1102,8 +1103,8 @@ func _snapshot_breakpoint() -> void: "bonus_pending": _curio_pending.duplicate(true), # 三选一停点态(空 = 无停点——选择停点可中断契约) "feat_ticket_pity": _feat_ticket_pity, # T5 门票保底计数(扩列向后兼容——旧档缺键缺省 0) "feat_ticket_sold": _feat_ticket_sold, # T5 门票限购计数(同上) - "adventure_counters": adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢) - "vision_counter_used": int(vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢) + "adventure_counters": run_ledger.adventure_counters.duplicate(true), # 迭代 20 T6/T7——adventure 域计数器随断点(续玩跨战账不丢) + "vision_counter_used": int(run_ledger.vision_counter.get("used", 0)), # 迭代 22 秘宝批 T3——真视使用计数随断点(续玩跨战账不丢) "refresh_stock": _refresh_stock, # 迭代 20 T4/T7——刷新存量账随断点(F-1 新账) "attune_refresh_y": _attune_refresh_y, # 2026-10-13 缺陷修批——Y 冒险周期池余量随断点(续玩不再自参数面整额重注入=堵「用完退菜单再续玩满血复活」可利用漏洞;扩列向后兼容——旧档缺键回退参数面) "battery_pending": _battery_pending, # 迭代 20 T4/T7——电池待分配充能随断点 diff --git a/game/core/battle_orchestrator.gd b/game/core/battle_orchestrator.gd index e3e6ea5e..258db5b4 100644 --- a/game/core/battle_orchestrator.gd +++ b/game/core/battle_orchestrator.gd @@ -70,22 +70,28 @@ var _facts_path := "" var _settling := false var _defer_board_damage := false # 案C 暂存闸(2026-10-06 用户裁:地图奖励伤害兑付迁战斗段末——怪物还手后) var _deferred_board: Array[Dictionary] = [] # 授予段暂存伤害单据(保授予段事件序;战斗段怪物还手后按序补提交统一结账) +# 结算五段 Segment(迭代 27 T2——P1-1 拆段;段边界=审计五段〔裁点③甲〕,段序骨架冻结于 +# SettlementTable.segment_order 语义:授予→增益/减益→战斗→衰减→判定。本类退为段序编排+ +# 行发射/开单原语单点——段体逻辑归 game/core/settlement/ 五类) +var _grant_seg = null # GrantSegment(①授予段——含旗判与 counter/grant 助手单点) +var _aura_seg = null # AuraSegment(②增益/减益段) +var _combat_seg = null # CombatSegment(③战斗段——原 _combat_segment 体) +var _decay_seg = null # DecaySegment(④衰减段) +var _verdict_seg = null # VerdictSegment(⑤判定段——返回裁定,终局早退语义归本类) var _refresh_used_this_round := 0 # 每轮刷新计数(Active 相位合计,兜底上限 = ActionEconomyTable.refresh_cap_per_round) var _board_seq := 0 # 板序号(本战斗第几张板——快照归因用) var _board_from_midround_refresh := false # 装板 provenance:轮内刷新换来的图 = true(清图奖励守卫——刷新图不发清图奖励) var _flag_consumed := {} # 兑付资格以雷格实例为键(板级;换图重置) # 同调装配运行态(迭代 11 第①批 T5——begin_battle 经 enc.attunement 注入;off=空集全部效果零扰动) -var attunement: Array = [] # 装备中同调件 item_id 集 -var _attune_charges_ref: Dictionary = {} # 充能账本共享引用(迭代 11 第①批 T6——use_treasure 消费直改) +var attunement: Array[Variant] = [] # 装备中同调件 item_id 集 +var _ledger: RunLedger = null # 共享四本账(迭代 27 T3——begin_battle 经 enc["ledger"] 持引用;缺省新账=调试面) var _attune_safe_seen := 0 # 积点制件 battle 域安全开格计数(迭代 22 T3 通用化——装配集 ap_charge_safe 行扫描;反哺批⑤ 2026-10-12 用户裁:满 charge_n 自动兑付当轮 AP+1,无玩家发起入口) # ---- 效果引擎扩签运行态(迭代 20 专长实装批 T1——新效果类型施加面;空订阅零扰动)---- var _fx_hero_ac_base := -1 # 主角 AC 基线快照(enc.hero_armor_class;-1=表值兜底未解析) var _fx_ac_round := 0 # ac_bonus 本轮累积器(封印桩形态——round_start 清零) var nest_preview_count := -1 # 巢穴预告读数口(迭代 22 秘宝批 T3——board_installed 命中落值;-1=未装配/未发射;呈现桥消费) var _fx_ac_battle := 0 # ac_bonus 本战累积器(石肤形态——cap 封顶,begin_battle 清零) -var _fx_adventure_counters_ref: Dictionary = {} # adventure 域计数器共享引用(T6——attune_charges 先例:begin_battle 回灌 rebuild、battle 末 counters_export 回写) -var _fx_bonus_ref: Dictionary = {} # 冒险加算区共享引用(bonus_delta 战末汇入——血税双向账) -var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末并入 _fx_bonus_ref 后清零) +var _fx_bonus_delta := {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} # 本战 bonus_delta 累积(战末经 _ledger.bonus_add_axis 汇入后清零) var _fx_new_resource_types := {} # 本战已见资源类型集(地质学家 fact new_resource_type 判定——battle 域) var _fx_pending_reveals: Array[Dictionary] = [] # 揭示效果待兑队列(见血开窍/斩首学者/地质学家/跳格采掘——授予段末统一兑付,防轮 facts 迭代中追加重入) var _fx_revealing := false # fx 揭示兑付中标记(_on_cell_revealed 缓冲旁路——fx 揭示的奖励由兑付点直接入账,防二次缓冲二次兑付) @@ -102,7 +108,7 @@ var _fx: EffectEngine func _fx_emit(event: String, payload: Dictionary) -> Array: if _fx == null: return [] - return _fx.dispatch(event, payload, Callable(self, "_fx_fact")) + return _fx.dispatch(event, payload, _fx_fact) # round_start 发射+施加(内容数据驱动扩展批 T3——grant_ap mode:queue=当轮入队,行动经济轮切换 @@ -152,7 +158,7 @@ func _flush_pending_reveals(ri: int) -> void: var board: BoardData = minesweeper.current_board() while not _fx_pending_reveals.is_empty(): _fx_pending_reveals.pop_back() - var cands: Array = [] + var cands: Array[Variant] = [] for c in board.all_cells(): if not minesweeper.is_revealed(c) and not board.is_mine(c): cands.append(c) @@ -195,7 +201,7 @@ func _fx_fact(fact: String): hits_n += 1 return hits_n "hp_ratio": - var hu: Dictionary = _enemy_system.hero_unit() + var hu: Dictionary[Variant, Variant] = _enemy_system.hero_unit() return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(0-100——when lte 算子 int 截断比较口径) return null @@ -213,16 +219,12 @@ func attune_has(item_id: String) -> bool: # 装备件表值读取(charge_n 等——TreasureItemTable 行;未装配不读表。magnitude 读数面随 T4 # 秘宝订阅迁移退役——行值经 value_from 在 rebuild 定影,零运行期按件取值) -func _attune_row(item_id: String) -> Dictionary: - return db.treasure_item_row(item_id) if _attune_has(item_id) else {} - - # 装配集参数合计(迭代 22 秘宝批 T3——常时参数面 kind 的装配集 Σmagnitude 读数口; # kind 分发查询先例=_attune_refresh_x_total bonus_refresh。零装配=0) func attune_param_sum(kind: String) -> int: var total := 0 for iid_v in attunement: - var arow: Dictionary = db.treasure_item_row(String(iid_v)) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has(kind): total += int(arow.get("magnitude", 0)) return total @@ -232,7 +234,7 @@ func attune_param_sum(kind: String) -> int: # 首行语义=装配集内至多一件积点制〔新目录恰试炼镐〕) func _attune_charge_safe_row() -> Dictionary: for iid_v in attunement: - var arow: Dictionary = db.treasure_item_row(String(iid_v)) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has("ap_charge_safe"): return arow return {} @@ -244,7 +246,7 @@ func _attune_charge_safe_row() -> Dictionary: func _reseed_attune_charges(reset: String) -> void: for iid_v in attunement: var iid := String(iid_v) - var arow: Dictionary = db.treasure_item_row(iid) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(iid) if String(arow.get("form", "")) != "active_charged": continue if String(arow.get("charge_reset", "adventure")) != reset: @@ -252,8 +254,7 @@ func _reseed_attune_charges(reset: String) -> void: var want := int(arow.get("charges_per_adventure", 0)) if want < 1: continue # charges=0=自动兑付件(试炼镐——不走骑库存面,兑付=_on_cell_revealed 积点段自动 grant_ap) - if _attune_charges_ref != null: - _attune_charges_ref[iid] = want + _ledger.attune_charge_set(iid, want) if treasure_inventory != null: var existing := 0 for r in treasure_inventory.snapshot_inventory(): @@ -303,6 +304,12 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore) -> void: minesweeper.flag_judged.connect(_on_flag_judged) minesweeper.round_end_board.connect(_on_round_end) action_economy.round_end_stopped.connect(_on_round_end) + # 结算五段接线(迭代 27 T2——段持宿主引用;信号连接纪律不涉段〔段零信号连接,ADR-0001 同构〕) + _grant_seg = GrantSegment.new(self) + _aura_seg = AuraSegment.new(self) + _combat_seg = CombatSegment.new(self) + _decay_seg = DecaySegment.new(self) + _verdict_seg = VerdictSegment.new(self) # ---- 战斗开始序列(编排专章①:门 → 增益状态 → 敌人系统 → install+roll → 行动经济)---- @@ -331,12 +338,12 @@ func begin_battle(enc: Dictionary) -> bool: # 开流条件、消费序、消费次数逐位一致——零内容期随机流零变化;区间宽度为零的行不掷 = 「定值不骰」 # 既有口径;atk 区间逐行同构 = 行值语义完整,代拍板随批留痕:属「每行一骰」预批准语义内的 # 行字段区间骰逐行化,非新增随机源形态)。 - var erowset_pre: Array = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) + var erowset_pre: Array[Variant] = db.resolve_enemy_rowset(int(enc.level_index), int(enc.node_depth)) if erowset_pre.is_empty(): _fatal("敌人表 行集解析为空(%d,%d)" % [int(enc.level_index), int(enc.node_depth)]) return false - var count_overrides: Array = [] - var atk_overrides: Array = [] + var count_overrides: Array[Variant] = [] + var atk_overrides: Array[Variant] = [] var needs_count := false var needs_atk := false for r in erowset_pre: @@ -370,7 +377,6 @@ func begin_battle(enc: Dictionary) -> bool: # 同调装配运行态注入(迭代 11 第①批 T5——enc.attunement 携带;off=空集零扰动。淬毒 N 注入 # 伤害控制器、偏运 N 注入地图战斗资源——均须先于首板 install) attunement = (enc.get("attunement", []) as Array).duplicate() - _attune_charges_ref = enc.get("attune_charges", {}) # 共享 dict 引用(level/adventure 同账本——use_treasure 扣减直改) _attune_safe_seen = 0 # 淬毒/偏运注入拆除(内容数据驱动扩展批 T4——两件被动改 triggers 订阅行:淬毒=clamp_check # modify 窗口、偏运=reward_roll bias_rewards;value 行字段 value_from 定影,零 begin_battle 注入) @@ -383,13 +389,11 @@ func begin_battle(enc: Dictionary) -> bool: _fx_new_resource_types = {} _fx_pending_reveals = [] _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} - var bonus_ref = enc.get("bonus_ref", null) - _fx_bonus_ref = bonus_ref if bonus_ref is Dictionary else {} - var counters_ref = enc.get("adventure_counters", null) - _fx_adventure_counters_ref = counters_ref if counters_ref is Dictionary else {} - _damage.fx_reveal_request = Callable(self, "_fx_enqueue_reveal") - _damage.fx_ac_request = Callable(self, "_apply_ac_bonus") - var feat_ids: Array = enc.get("feats", []) as Array + var ledger_ref = enc.get("ledger", null) + _ledger = ledger_ref if ledger_ref is RunLedger else RunLedger.new() # 缺省新账=调试/单场零扰动面 + _damage.fx_reveal_request = _fx_enqueue_reveal + _damage.fx_ac_request = _apply_ac_bonus + var feat_ids: Array[Variant] = enc.get("feats", []) as Array # 迭代 20 T3 换代(V14 旧实现摘除)——流派档位质变随重设计退役(奇物.md v1「不设流派标签、 # 不设档位机制」):FeatsEngine 求值与 rider_tier 注入拆除,散件单体效果=唯一形态 _damage.rider_tier_bonus = 0 @@ -398,12 +402,12 @@ func begin_battle(enc: Dictionary) -> bool: # 树 rule 键消费(迭代 12 T10 → 内容数据驱动扩展批 T3 换代:Fork 激活侧效果=效果引擎合成 # 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1),旧直调拆除。其余 rule 键 # =stat 面投影不入战斗(T8 表内容 provisional 口径披露)) - var tree_rules: Array = enc.get("tree_rules", []) as Array + var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array # 效果引擎订阅重建(内容数据驱动扩展批 T1——每战从持有集/装备集重建,中途获得下一战生效; # 树规则合成订阅同面装载。装载序=feats 表序→treasure 表序→树规则,写死保种子复放。 # 迭代 20 T6——adventure 域计数器从共享引用回灌〔跨战持久〕) _fx.rebuild(feat_ids, attunement, tree_rules) - _fx.counters_seed(_fx_adventure_counters_ref) + _fx.counters_seed(_ledger.counters_seed_snapshot()) # 秘宝常时参数注入(迭代 22 秘宝批 T3——装配集 kind 分发:远程镐挖距放宽/信标网络旗可达豁免; # 缺省 0/false=零装配零扰动。注入先于首板 install 已不成立〔板在早段〕——本批板生成不读两参数, # 参数只影响挖掘/插棋动作判据,时序安全) @@ -458,7 +462,7 @@ func _install_new_board(during_round := false) -> bool: # boss/elite 图命中全局型 → 写 board_variants(_place_variants 见非空即跳 region=替换制); # 全局池空 → 板级字段留空(_place_variants 原路走 region=兜底回落合法态)。 _resolve_global_variant(board) - var res: Dictionary = minesweeper.install_board(board, during_round) + var res: Dictionary[Variant, Variant] = minesweeper.install_board(board, during_round) if not res.accepted: _fatal("排雷玩法 install 拒绝(不变式违例): %s" % str(res)) return false @@ -499,7 +503,7 @@ func _install_new_board(during_round := false) -> bool: func _resolve_global_variant(board: BoardData) -> void: board.board_variants.clear() _reward_economy.board_skew = 0 - var row: Dictionary = db.global_variant_row(int(encounter.level_index), String(board.board_type), + var row: Dictionary[Variant, Variant] = db.global_variant_row(int(encounter.level_index), String(board.board_type), String(encounter.get("node_type", ""))) if row.is_empty(): return @@ -521,7 +525,7 @@ func _resolve_global_variant(board: BoardData) -> void: func _place_variants(board: BoardData) -> void: if not board.board_variants.is_empty(): return - var pool: Array = db.variant_pool(int(encounter.level_index), String(board.board_type)) + var pool: Array[Variant] = db.variant_pool(int(encounter.level_index), String(board.board_type)) if pool.is_empty(): return var r0 := {} @@ -534,7 +538,7 @@ func _place_variants(board: BoardData) -> void: if candidates.is_empty(): return var stream: RandomNumberGenerator = rng.open_stream("variant_place") # 一板恰一 scope(换图/刷新图各取号一次) - var vrow: Dictionary = pool[stream.randi_range(0, pool.size() - 1)] + var vrow: Dictionary[Variant, Variant] = pool[stream.randi_range(0, pool.size() - 1)] var total: int = int(round(float(candidates.size()) * float(db.variation.percent_of_cells) / 100.0)) \ + (int(encounter.level_index) - 1) * int(db.variation.per_level_bonus) total = mini(total, candidates.size()) @@ -552,7 +556,7 @@ func _place_variants(board: BoardData) -> void: break var seed_c: Vector2i = free[stream.randi_range(0, free.size() - 1)] placed[seed_c] = true - var pack: Array = [seed_c] + var pack: Array[Variant] = [seed_c] var pack_set := {seed_c: true} while pack.size() < pack_size: var fringe: Array[Vector2i] = [] @@ -594,7 +598,7 @@ func _on_cell_revealed(evt: Dictionary) -> void: # 先兑再扣,轮次不因本格结束;授予段已无积点段(本段唯一计数点)。fx_reveal 旁路不计数= # 沿现役缓冲路径口径〔天眼开格不计积点〕。grant_ap 相位防御在 ACTIVE 挖格链内恒过) if String(evt.get("kind", "")) == "safe": - var cs_row: Dictionary = _attune_charge_safe_row() + var cs_row: Dictionary[Variant, Variant] = _attune_charge_safe_row() if not cs_row.is_empty(): _attune_safe_seen += 1 var cn_b: int = int(cs_row.get("charge_n", 999)) @@ -631,254 +635,16 @@ func _maybe_settle() -> void: func _settle() -> void: _settling = true - _defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末) var ri := round_index - # ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。 - # 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19; - # 开格事实不可撤)。 - for evt in _facts_reveals: - if not bool(evt.get("resource", false)): - continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) - var snap = evt.get("reward_snapshot") - if not (snap is Dictionary): - continue - if evt.kind == "safe": - if bool(evt.get("dug_under_flag", false)): - # 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付 - # 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。 - _emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") - continue - # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: - # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。 - # 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/ - # new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕) - var board_b: BoardData = minesweeper.current_board() - var snap_pre = evt.get("reward_snapshot") - var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else "" - var is_new_type := ctype != "" and not _fx_new_resource_types.has(ctype) - if is_new_type: - _fx_new_resource_types[ctype] = true - var dr_hits := _fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, - "is_first_dig_of_battle": not _feat_first_dig_used, - "terrain_kind": _harmful_variants(evt.get("variant_ids", []) as Array), - "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 - or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, - "is_blank_zero": board_b.digit_of(evt.cell) == 0, - "new_resource_type": is_new_type}) - var pos_m := 0 - var neg_m := 0 - var combo_cap := -1 - for h in dr_hits: - if String(h.get("kind", "")) != "yield_mult": - continue - pos_m += int(h.get("mult", 0)) - neg_m += int(h.get("cost_mult", 0)) - if int(h.get("cap", 0)) > 0: - combo_cap = int(h.cap) # 矿脉直觉连击上限 - _grant_routed(_feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "") - # 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致) - for h in dr_hits: - if String(h.get("kind", "")) == "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - # 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末 - # _flush_pending_reveals 统一掷格兑付) - if not _hits_of(dr_hits, "reveal_tiles").is_empty(): - _fx_enqueue_reveal() - # 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行) - for h in dr_hits: - if String(h.get("kind", "")) == "grant_material": - var dd_snap = _reward_economy.grant(evt.cell) - if dd_snap is Dictionary: - _grant_routed(dd_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) - # 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效) - var spell_hits: Array = _hits_of(dr_hits, "spawn_spell") - if not spell_hits.is_empty() and not _feat_first_dig_used: - _feat_first_dig_used = true - var spell_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", - "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), - DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") - _submit(spell_t, false) - elif evt.kind == "mine": - # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), - # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 - # mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: - # 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。 - # 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面: - # damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/ - # blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕) - var mine_power := int((snap as Dictionary).get("power", 0)) - var mh_hits := _fx_emit("mine_hit", {"cell": evt.cell, "round": ri, - "damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)), - "hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))}) - var halve_pct := 0 - var mine_reduce := 0 - for h in mh_hits: - match String(h.get("kind", "")): - "damage_mult": - halve_pct += int(h.get("value", 0)) - "damage_reduce": - if h.has("value_counter") and String(h.get("value_counter", "")) != "": - mine_reduce += _fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免 - else: - mine_reduce += int(h.get("value", 0)) # 背水——定值减免 - "heal": - var hu_heal: Dictionary = _enemy_system.hero_unit() - hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1)))) - "bonus_delta": - var axis := String(h.get("axis", "")) - if _fx_bonus_delta.has(axis): - _fx_bonus_delta[axis] += int(h.get("delta", 0)) - "blast_scaled": - var blast_v := maxi(int(db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1)))) - var bs_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board", - blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) - _submit(bs_t, false) - if mine_reduce > 0: - snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1) - for h in mh_hits: - if String(h.get("kind", "")) == "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - if not _hits_of(mh_hits, "reveal_tiles").is_empty(): - _fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付) - _grant_routed(_feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct) - for h in mh_hits: - if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": - var nct := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board", - int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) - _submit(nct, false) - # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到 - # 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽; - # 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径, - # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) - for evt in _facts_reveals: - var vids: Array = evt.get("variant_ids", []) - for vid in vids: - var vrow: Dictionary = db.variation.row_by_id(String(vid)) - if String(vrow.get("effect", "")) != "flat_damage": - continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) - # 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内 - # 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when - # 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加; - # grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化) - var dt_amount := int(vrow.get("effect_value", 0)) - var dt_hits := _fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount, - "source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid), - "hp_ratio": int(round(100.0 * float(int(_enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(_enemy_system.hero_unit().get("max_hp", 1))))))}) - for h in dt_hits: - match String(h.get("kind", "")): - "damage_reduce": - dt_amount = maxi(0, dt_amount - int(h.get("value", 0))) - "ac_bonus": - _apply_ac_bonus(h) - "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - var vt := _damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \ - dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)}) - _submit(vt, false) - for h in dt_hits: - if String(h.get("kind", "")) == "grant_material": - var tw_snap = _reward_economy.grant(evt.cell) - if tw_snap is Dictionary: - _grant_routed(tw_snap, "positive", "safe_cell", evt.cell, ri, "", String(h.get("label", ""))) - for evt in _facts_flags: - _judge_flag(evt, ri) - # 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌 - # 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌—— - # 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。 - # 伤害单据案C 暂存战斗段末兑付。 - # flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket - # 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致) - var fs_hits := 0 - var fs_misses := 0 - for fevt in _facts_flags: - if bool(fevt.get("hit", false)): - fs_hits += 1 - else: - fs_misses += 1 - var fs_fx := _fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0, - "flags_count": fs_hits, "round": ri}) - var banner_hits: Array = [] - for h in fs_fx: - if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack": - banner_hits.append(h) - if not banner_hits.is_empty(): - var strike_value := int((banner_hits[0] as Dictionary).get("value", 0)) - var flag_mult := 1 - for h in fs_fx: - if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult": - flag_mult *= maxi(1, int(h.get("value", 1))) - if fs_misses == 0 and fs_hits > 0 and flag_mult > 1: - strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) - for fevt in _facts_flags: - if not bool(fevt.get("hit", false)): - continue - var st_t := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", - "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, - fevt.cell, "normal") - _submit(st_t, false) - # round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket - # 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 - # request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省) - var re_fx := _fx_emit("round_end", {"round": ri}) - for h in re_fx: - if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": - var mg_layers := _fx.counter(String(h.get("per", ""))) - var mg_total := int(h.get("value", 0)) * mg_layers - if mg_total > 0: - # 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面) - var mg_t := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter", - "积层反噬 ×%d" % mg_layers, "board", mg_total, - DamageController.TARGET_ALL_ALIVE, ri, minesweeper.hero_pos(), "", 0, "", {}) - _submit(mg_t, false) - for h in re_fx: - if String(h.get("kind", "")) == "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - # 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/ - # grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕 - for h in re_fx: - if String(h.get("kind", "")) == "grant_gp": - escrow += int(h.get("value", 0)) - _emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": escrow}, "") - elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": - action_economy.queue_ap_delta(int(h.get("value", 0))) - # 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1; - # 未装配=100 与旧 1:1 逐位一致) - var rap_pct: int = attune_param_sum("rap_transfer_mult") - action_economy.finish_round_stop(_fx != null and _fx.has_effect_on("round_end", "grant_ap", "transfer"), - rap_pct if rap_pct > 0 else 100) - if _facts_path == "cleared": - _grant_clear_reward(ri) - _flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) - _damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) - # ② 增益/减益段(续存实例组合行;本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦) - _emit_composition_effects("hero", ri) - _emit_composition_effects("enemy", ri) - # ③ 战斗段(调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路) - _combat_segment(ri) - # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) - var k: int = _status_effects.decay_round() - _emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") - # ⑤ 判定段(防御性优先级 hero 先判——双死结构不可达的纵深防御) - var verdict := "continue" - if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: - verdict = "defeat" - elif _enemy_system.query_alive("enemy").is_empty(): - verdict = "victory" - _emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) - if verdict == "defeat": - _emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "") - _battle_over("defeat") - return - if verdict == "victory": - _emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "") - _battle_over("victory") - return - _emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") - _exit_distribution() + # 段序编排(迭代 27 T2——五段 Segment;段体归 game/core/settlement/,本类退为段序+ + # 行发射/开单原语单点。旧 259 行巨函数体已逐字迁五段类,git 史留痕) + _grant_seg.run(ri) # ① 授予段(案C 闸随段装载) + _aura_seg.run(ri) # ② 增益/减益段(续存实例组合行) + _combat_seg.run(ri) # ③ 战斗段(主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕) + _decay_seg.run(ri) # ④ 衰减段(恰一次恒执行含末轮——先兑付后衰减) + var verdict: String = _verdict_seg.run(ri) # ⑤ 判定段(防御性优先级 hero 先判) + if verdict != "continue": + return # 终局:旧早退语义逐位一致(_settling 不回置——_battle_over 已接管) _settling = false @@ -894,7 +660,7 @@ func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int return snap if not positive and neg_m <= 0: return snap - var out: Dictionary = snap.duplicate() + var out: Dictionary[Variant, Variant] = snap.duplicate() var power := int(out.get("power", int(_reward_economy.polarity_row(String(out.get("cell_type", "")), "positive" if positive else "negative").power))) var pct := pos_m if positive else neg_m if positive and combo_cap > 0: @@ -906,10 +672,10 @@ func _feat_yield_adjust(snap: Dictionary, positive: bool, pos_m: int, neg_m: int # 伤害型变异过滤(terrain_kind fact 取数面——variation 表 effect=flat_damage 判据; # 🔴 R4 甲永不对冲边界=仅限伤害型,信息型(hide_number 族)永不出现在本集) func _harmful_variants(vids: Array) -> Array: - var out: Array = [] + var out: Array[Variant] = [] for vid_v in vids: var vid := String(vid_v) - var vrow: Dictionary = db.variation.row_by_id(vid) + var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(vid) if String(vrow.get("effect", "")) == "flat_damage": out.append(vid) return out @@ -918,7 +684,7 @@ func _harmful_variants(vids: Array) -> Array: # 邻接伤害型变异收集(terrain_kind fact 取数面——flag_hit 发射点用:标中旗相邻八格的伤害型变异集; # 🔴 R4 甲永不对冲边界=仅限伤害型,信息型〔hide_number 族〕不入集) func _harmful_adjacent(cell: Vector2i) -> Array: - var out: Array = [] + var out: Array[Variant] = [] var board: BoardData = minesweeper.current_board() if board == null: return out @@ -929,7 +695,7 @@ func _harmful_adjacent(cell: Vector2i) -> Array: # 效果命中过滤(按效果类型取命中子集——发射点分桶施加面) func _hits_of(hits: Array, kind: String) -> Array: - var out: Array = [] + var out: Array[Variant] = [] for h in hits: if String((h as Dictionary).get("kind", "")) == kind: out.append(h) @@ -955,7 +721,7 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: var board: MinesweeperRules = minesweeper # 恒等包装退役(2026-10-02 修复批——use_treasure 直引公有成员) # 板维度读数(迭代 22 T3 修——Rules 无 width 代理〔潜伏缺陷:旧目录期零直呼覆盖掩盖〕,读 BoardData 本体) var board_b: BoardData = board.current_board() - var v: Dictionary = treasure_inventory.validate_use(handle, target, + var v: Dictionary[Variant, Variant] = treasure_inventory.validate_use(handle, target, Vector2(board_b.width, board_b.height), func(c: Vector2i): return not board.is_revealed(c)) if not bool(v.ok): @@ -966,10 +732,10 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: for h in tu_hits: if String(h.get("kind", "")) == "counter_spend": if not _fx.counter_spend(String(h.get("counter_id", "")), int(h.get("value", 1))): - var vrow: Dictionary = db.treasure_item_row(String(v.item_id)) + var vrow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.item_id)) var cn_info: int = int(vrow.get("charge_n", 0)) # 迭代 22 T3——读受理行自身(旧 dig_charge 硬编码随行退役) return {"ok": false, "reason": "%s:安全格积点未达(%d/%d)" % [String(vrow.get("display_name", "")), _attune_safe_seen % cn_info if cn_info > 0 else 0, cn_info]} - var kinds: Array = v.kinds + var kinds: Array[Variant] = v.kinds if kinds.has("mine_reveal") or kinds.has("reveal_tiles"): # 真视写入(迭代 22 T3——reveal_tiles 新真视 kind 与 mine_reveal 同叠加层通道: # 危险格显示=信息呈现零回写,棋盘事实不可变契约 3) @@ -995,21 +761,18 @@ func use_treasure(handle: int, target: Vector2i) -> Dictionary: # 爆破工兵包兑付(迭代 13 T8/D6——中立移除雷格:未揭示/无旗/是雷过滤归排雷玩法; # P1 甲邻接数字冻结零重算、清图分母零涉(雷格不入分母);空集拒绝零消耗—— # consume 之前 return(拒绝面注记扩「③内零消耗分支」),拒绝 reason 可呈现) - var blasted: Array = board.remove_mines_in(v.resolved) + var blasted: Array[Variant] = board.remove_mines_in(v.resolved) if blasted.is_empty(): return {"ok": false, "reason": "爆破工兵包:3×3 区域内无可移除雷格(须未揭示且未插旗)"} - var rec: Dictionary = treasure_inventory.consume(handle) + var rec: Dictionary[Variant, Variant] = treasure_inventory.consume(handle) # 同调主动件充能扣减(迭代 11 第①批 T6——共享账本直改:adventure/level/settlement 同一字典引用) if not rec.is_empty() and String(rec.get("source_class", "")) == "attunement": - var used_iid: String = String(rec.get("item_id", "")) - if _attune_charges_ref.has(used_iid): - _attune_charges_ref[used_iid] = maxi(0, int(_attune_charges_ref[used_iid]) - 1) + _ledger.attune_charge_consume(String(rec.get("item_id", ""))) # 真视类工具使用计数(迭代 22 秘宝批 T3——信标网络解锁行为条件:受理成功恰计一次; # 共享 dict 引用下传〔attune_charges 同款〕,冒险收束聚合入档 save.add_vision_used) if kinds.has("reveal_tiles") or kinds.has("safe_dig") or kinds.has("mine_reveal"): - var vc: Dictionary = encounter.get("vision_counter", {}) if encounter is Dictionary else {} - if vc is Dictionary and not vc.is_empty(): - vc["used"] = int(vc.get("used", 0)) + 1 + if not _ledger.vision_counter.is_empty(): + _ledger.vision_count_add(1) # 迭代 22 秘宝批 T3——受理成功恰计一次(缺省空账=调试不计) return {"ok": true, "item_id": v.item_id, "resolved": v.resolved, "record": rec} @@ -1021,7 +784,7 @@ func set_treasure_unlocked(ids: Dictionary) -> void: func _grant_routed(snap: Dictionary, polarity: String, source_class: String, cell: Vector2i, ri: int, highlight: String = "", label_suffix: String = "", halve_pct: int = 0) -> bool: # 迭代 11 第①批 T7——aux 授予分支随掉落/停点渠道退役拆除(reward 词表第十型 treasure_item 保留但零投放: # 无 aux 行入池、CURIO_KINDS 词表已除名);剩余类型走极性字典路由。 - var row: Dictionary = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) + var row: Dictionary[Variant, Variant] = _reward_economy.polarity_row(String(snap.get("cell_type", "")), polarity) if row.is_empty(): _fatal("极性字典缺行: %s %s" % [String(snap.get("cell_type", "")), polarity]) return false @@ -1064,7 +827,7 @@ func _apply_percent_row(row: Dictionary, source_class: String, cell: Vector2i, r var eff := String(row.effect_id) if eff == "gp_gain" or eff == "gp_loss": return _apply_gp_row(row, source_class, cell, ri, highlight, label_suffix) - var srow: Dictionary = db.status_effect_row(eff) + var srow: Dictionary[Variant, Variant] = db.status_effect_row(eff) # 旗溢价贯穿(迭代14② T4——仅标中旗正向行携带 premium_pct;其余授予路径缺省 100 零扰动): # 应用幅度与呈现行值同乘,容器经 magnitude_override 拿同一快照(抗表重载口径保持)。 var prem_pct: int = int(row.get("premium_pct", 100)) @@ -1162,95 +925,6 @@ func _submit_flat_hero(source_class: String, display: String, power: int, cell: # 旗判定批:标中 = 该雷格资源的正向兑付(每雷实例一次;高光 = 强制检查点); # 标空 = "标旗未中"归因行 + 该安全格资源的负向兑付(同键去重——每格实例一次)。 -func _judge_flag(evt: Dictionary, ri: int) -> void: - var cell: Vector2i = evt.cell - if evt.hit: - if _flag_consumed.has(cell): - return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 - _flag_consumed[cell] = true - _perfect_hit_true += 1 - # flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→ - # 净化改写 → 图谱额外资源 → 基线兑付) - var fh_hits := _fx_emit("flag_hit", {"cell": cell, "round": ri, - "terrain_kind": _harmful_adjacent(cell)}) - # 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket - # channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。 - # 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast) - for h in _hits_of(fh_hits, "open_ticket"): - if String(h.get("channel", "")) != "blast": - continue - var fbt := _damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board", - int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {}) - _submit(fbt, false) - # 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/ - # ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/ - # remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。 - # (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕) - for h in fh_hits: - match String(h.get("kind", "")): - "counter_add": - var set_v = h.get("set", null) - _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) - "ac_bonus": - _apply_ac_bonus(h) - "bonus_delta": - var axis_f := String(h.get("axis", "")) - if _fx_bonus_delta.has(axis_f): - _fx_bonus_delta[axis_f] += int(h.get("delta", 0)) - "remove_mines": - var removed: Array = minesweeper.remove_mines_in([cell]) - if not removed.is_empty(): - _emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "") - "preview_area": - var board_p: BoardData = minesweeper.current_board() - var types: Array = [] - for n in board_p.neighbors8(cell): - var n_snap = _reward_economy.query_full_snapshot(n) - if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): - types.append(String((n_snap as Dictionary).get("cell_type", ""))) - if treasure_inventory != null and not types.is_empty(): - treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) - _emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "") - # 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格; - # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) - for h in fh_hits: - if String(h.get("kind", "")) == "grant_material": - var extra_snap = _reward_economy.grant(cell) - if extra_snap is Dictionary: - _grant_routed(extra_snap, "positive", "flag_hit", cell, ri, "", String(h.get("label", ""))) - var snap = _reward_economy.grant(cell) - if not (snap is Dictionary): - return - _grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit") - else: - _perfect_hit_false += 1 - if _flag_miss_seen.has(cell): - return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸) - _flag_miss_seen[cell] = true - # 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错 - # 对完美谓词无痕;只保一次/件,节点级用完即止) - if _fx_insurance > 0: - _fx_insurance -= 1 - _perfect_hit_false -= 1 - _emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": _fx_insurance}, "") - var hl := "" - if _perfect_hit_false == 1: - hl = "perfect_broken" # 本板完美谓词已破的归因锚 - _emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl) - # 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错 - # 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在 - # 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 - # 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行 - # adventure 域计数器闸) - for h in _fx_emit("flag_missed", {"cell": cell, "round": ri}): - if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": - action_economy.queue_ap_delta(int(h.get("value", 0))) - var snap = _reward_economy.grant(cell) - if not (snap is Dictionary): - return - _grant_routed(snap, "negative", "flag_miss", cell, ri, "") - - # 清图批:清图奖励对象恰好发一次;完美排雷追加第二份同身份兑付(grant_clear 本身恰一次)。 # 高光行 value = 实际有效兑付份数(禁记名义值;首份零兑付 → 降级不发)。 func _grant_clear_reward(ri: int) -> void: @@ -1302,7 +976,7 @@ func _roll_round_actions() -> void: # 技能名提示〔轻量 toast 非演出——D8 呈现面〕。门=确定性谓词,机制面零随机,摇号随机 # 消费仍在 behavior_roll 既有流——同种子逐位复现不破坏) for sid in _enemy_system.check_skill_unlocks(u): - var srow_u: Dictionary = db.skill_row(sid) + var srow_u: Dictionary[Variant, Variant] = db.skill_row(sid) _emit_row("highlight", "enemy_action", Vector2i(-1, -1), round_index, "%s 解锁了 %s" % [String(u.display_name), String(srow_u.get("display_name", sid))], 0, {"skill_id": sid}, "skill_unlock") @@ -1320,9 +994,9 @@ func _roll_round_actions() -> void: for h in eht_hits: if String(h.get("kind", "")) == "target_attack_bonus": u["fx_atk_bonus"] = int(u.get("fx_atk_bonus", 0)) + int(h.get("value", 0)) - var entries: Array = _enemy_system.rollable_skills(u) + var entries: Array[Variant] = _enemy_system.rollable_skills(u) if _enemy_system.basic_streak(u) >= pity_m: - var specials: Array = _enemy_system.special_skills(u) + var specials: Array[Variant] = _enemy_system.special_skills(u) if not specials.is_empty(): entries = specials # 保底命中——普攻恒可摇 → 非保底路径池恒非空,此为唯一替代集合 var stream: RandomNumberGenerator = rng.open_stream("behavior_roll") @@ -1336,93 +1010,6 @@ func _roll_round_actions() -> void: # 开单守卫归流程(用户裁决③——流程决定给谁开单):主角存活+目标存在才开先手单;hero hp=0 立即 # 停开(B 模式下若 flush 中主角倒下,由控制器死亡事件终止余单——两模式同一结果)。 -func _combat_segment(ri: int) -> void: - _defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账) - # 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数= - # streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后 - # 同一次 _settle 内,facts 已定。 - var round_safe := 0 - var round_mine := false - for evt in _facts_reveals: - if evt.kind == "safe": - round_safe += 1 - elif evt.kind == "mine": - round_mine = true - _damage.dig_streak = 0 if round_mine else round_safe - # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 - # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) - _damage.charge_layers = action_economy.charge_layers - # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害, - # 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。 - # 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击; - # 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。 - # 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面), - # 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。 - # 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折; - # 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。 - var hero_alive: bool = int(_enemy_system.hero_unit().get("hp", 0)) > 0 - var hero_target = _enemy_system.default_target() - if hero_alive and hero_target != null: - var ht := _damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", _hero_atk, \ - DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(_enemy_system.hero_unit().get("attack_type", "normal"))) - _damage.submit(ht) - # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 - # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 - # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) - for h in _hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): - var segments := mini(int(h.get("cap", 2)), _damage.dig_streak / maxi(1, int(h.get("divisor", 3)))) - for s in segments: - # 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面) - var ct := _damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", - "连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, - minesweeper.hero_pos(), "pierce") - _damage.submit(ct) - # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 - # 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。 - for u in _enemy_system.query_alive("enemy"): - if int(_enemy_system.hero_unit().hp) <= 0: - break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) - var skill_id := _enemy_system.pending_action(u) - var srow: Dictionary = db.skill_row(skill_id) - if srow.is_empty(): - _fatal("敌实例待发动作悬空: %s" % skill_id) - continue - if String(srow.type) == "buff" or String(srow.type) == "debuff": - # 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。 - # (原"本轮喂的伏击逢施放拍不兑现"读怪奖励随伏击退役一并作废——2026-09-20 八型批) - # 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。 - var dur: int = int(srow.get("buff_duration", 1)) - _status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) - _emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) - _enemy_system.commit_action(u) - continue - # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 - # (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/ - # hero_defense×enemy_output 池乘区/下限/rider 后置/commit 全归控制器管线(兑现序 = 开单序; - # 来源敌已死在兑现时作废——死亡即不动手)。 - var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0)))) - var rider := 0 - if srow.has("rider_mult"): - rider = maxi(int(db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult)))) - var et := _damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", "%s %s" % [u.display_name, String(srow.display_name)], "combat", \ - baseline, DamageController.TARGET_HERO, ri, Vector2i(-1, -1), String(u.get("attack_type", "normal")), rider) - et["source_unit"] = u - et["context"] = {"skill_id": skill_id, "baseline": baseline} - _damage.submit(et) - # 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账; - # 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径; - # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) - if int(_enemy_system.hero_unit().get("hp", 0)) <= 0: - for t in _deferred_board: - _emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri, - "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "") - else: - for t in _deferred_board: - _damage.submit(t) - _deferred_board.clear() - _damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结) - - # ---- 算术读数(伤害管线单据化批 2026-09-24 迁移注记)---- # 掷点/折扣/克制点数/池乘区/攻击行文本已全部迁入 伤害控制器(damage_controller.gd 管线单点); # 本类仅保留一处薄委托——_defender_ac(快照面消费)。_clamped_pool 委托壳已随 2026-10-02 修复批删除 @@ -1435,7 +1022,7 @@ func _defender_ac(unit: Dictionary) -> int: func _pool_effective(effect_id: String) -> int: var side := "hero" - var row: Dictionary = db.status_effect_row(effect_id) + var row: Dictionary[Variant, Variant] = db.status_effect_row(effect_id) if not row.is_empty(): side = String(row.target_side) for eff in _status_effects.query_active(side): @@ -1545,19 +1132,16 @@ func _battle_over(result: String) -> void: if result == "victory": for h in _fx_emit("battle_won", {"round": round_index}): if String(h.get("kind", "")) == "heal": - var hu_w: Dictionary = _enemy_system.hero_unit() + var hu_w: Dictionary[Variant, Variant] = _enemy_system.hero_unit() hu_w["hp"] = mini(int(hu_w.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_w.get("max_hp", 1)))) # bonus_delta 战末汇入(迭代 20 T1——血税双向账:本战累积并入冒险加算区共享引用后清零)+ # adventure 域计数器导出回写(T6——共享引用覆写,attune_charges 先例) - if _fx_bonus_ref != null and not _fx_bonus_ref.is_empty(): + if not _ledger.bonus.is_empty(): for axis in _fx_bonus_delta.keys(): - if _fx_bonus_ref.has(axis): - _fx_bonus_ref[axis] = int(_fx_bonus_ref.get(axis, 0)) + int(_fx_bonus_delta[axis]) + _ledger.bonus_add_axis(axis, int(_fx_bonus_delta[axis])) _fx_bonus_delta = {"atk": 0, "max_hp": 0, "armor_class": 0, "ap": 0} - if _fx != null and not _fx_adventure_counters_ref.is_empty(): - var exported: Dictionary = _fx.counters_export() - for cid in exported.keys(): - _fx_adventure_counters_ref[cid] = int(exported[cid]) + if _fx != null: + _ledger.counters_merge_export(_fx.counters_export()) # 映射随战斗结束退役(地图战斗资源 义务④——下一场战斗 = 新板新 scope 批,roll_board 相位守卫的前提) var hero_hp_left := int(_enemy_system.hero_unit().get("hp", 0)) # despawn 前捕获(信号载荷供呈现层"HP 带入下一层") var gp_dropped := _roll_gp_drop() # 战斗掉落(GP+商店房实施批——escrow 随此值上抛即弃) @@ -1577,7 +1161,7 @@ func _battle_over(result: String) -> void: # + level_gp_bonus(level_index) + escrow。掷定行缺位 = 数据面缺行(fail 报错按 0 兑付——不哑掉)。 func _roll_gp_drop() -> int: var tier := "boss" if String(encounter.get("node_type", "")) == "boss" else "normal" - var drops: Dictionary = db.shop_gp_drop_row(tier) + var drops: Dictionary[Variant, Variant] = db.shop_gp_drop_row(tier) var roll := 0 if drops.is_empty(): push_error("[战斗结算] gp_drops 缺行 tier=%s——掉落掷定按 0 兑付" % tier) @@ -1602,18 +1186,18 @@ func _roll_gp_drop() -> int: # 八型批 2026-09-20:ambush_layers 拆除——伏击退役,警觉并入 effects 投影)hero_effects[](增益状态 query_active 投影) func presentation_snapshot() -> Dictionary: var enemies: Array[Dictionary] = [] - var all: Array = _enemy_system.all_enemies() + var all: Array[Variant] = _enemy_system.all_enemies() # 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行,八型批 2026-09-20) var enemy_effects: Array[Dictionary] = [] for eff in _status_effects.query_active("enemy"): - var erow: Dictionary = db.status_effect_row(eff.effect_id) + var erow: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id) if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]: enemy_effects.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)}) for i in all.size(): - var u: Dictionary = all[i] + var u: Dictionary[Variant, Variant] = all[i] var alive: bool = bool(u.get("alive", false)) var pending: String = String(u.get("pending_skill", "")) if alive else "" - var pending_row: Dictionary = db.skill_row(pending) + var pending_row: Dictionary[Variant, Variant] = db.skill_row(pending) enemies.append({ "order": i + 1, "display_name": String(u.display_name), @@ -1630,7 +1214,7 @@ func presentation_snapshot() -> Dictionary: "alive": alive, }) var hero := {} - var h: Dictionary = _enemy_system.hero_unit() + var h: Dictionary[Variant, Variant] = _enemy_system.hero_unit() if not h.is_empty(): hero = {"hp": int(h.hp), "max_hp": int(h.max_hp), "atk": int(h.atk), "alive": bool(h.get("alive", false)), "armor_class": _defender_ac(h)} return { @@ -1639,6 +1223,7 @@ func presentation_snapshot() -> Dictionary: "phase": String(action_economy.phase), "ap": int(action_economy.ap), "rap": int(action_economy.rap), "can_refresh": can_refresh(), + "refresh_used": _refresh_used_this_round, # 迭代 27 T3——P1-2 刷新双账收口:受理计数入呈现成文面(HUD 余量单源读此处) "hero": hero, "enemies": enemies, "hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(code-review 2026-09-29 P2-3——快照消费方直改不渗入状态容器) diff --git a/game/core/board_data.gd b/game/core/board_data.gd index 9402b9ae..7ca6da01 100644 --- a/game/core/board_data.gd +++ b/game/core/board_data.gd @@ -58,10 +58,6 @@ func board_variant_ids() -> Array[String]: # 中和格变异(迭代 12 T6 净化工兵——伤害型地形格变普通格;🔴 R4 甲仅限伤害型的过滤在调用方) -func clear_variants(c: Vector2i) -> void: - variants.erase(c) - - func neighbors8(c: Vector2i) -> Array: var out: Array[Vector2i] = [] for dy in range(-1, 2): @@ -106,7 +102,7 @@ func compute_r0() -> Array: var r0 := {} for c in b3: r0[c] = true - var queue: Array = b3.duplicate() + var queue: Array[Variant] = b3.duplicate() while not queue.is_empty(): var c: Vector2i = queue.pop_front() if is_safe(c) and digit_of(c) == 0: # 变异死分支退役(2026-09-18 用户拍板删——与 排雷玩法 展开实现口径归一;变异定稿批按正式语义重接) diff --git a/game/core/board_generator.gd b/game/core/board_generator.gd index ee8a7c11..4fb150aa 100644 --- a/game/core/board_generator.gd +++ b/game/core/board_generator.gd @@ -15,8 +15,8 @@ const METRIC_VERSION := 1 # rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。 # db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表(2026-10-02 注) -func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: - var row: Dictionary = db.resolve_board_gen(L, D) +func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub) + var row: Dictionary[Variant, Variant] = db.resolve_board_gen(L, D) if row.is_empty(): _fatal("BoardGen 行缺失 (%d,%d)" % [L, D]) return null @@ -54,7 +54,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat if int(params.r0_cap) > 0 and b.compute_r0().size() > int(params.r0_cap): r0_cap_rejected += 1 if legality(b, params, db): - var m: Dictionary = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次) + var m: Dictionary[Variant, Variant] = measure_inferability(b) # 求解器每合法板恰一次(O3 收口——此前重复调用两次) var inf: float = m["inferability"] if best_legal == null or inf > best_legal_score: best_legal = b @@ -80,7 +80,7 @@ func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardDat # ==================== 五阶段:单次尝试 ==================== -func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> BoardData: +func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(同 generate——测量面 stub 注入) var b := BoardData.new() b.width = params.w b.height = params.h @@ -158,7 +158,7 @@ func measure_inferability(b: BoardData) -> Dictionary: while changed: changed = false # 算子① 连锁:0 格的 8 邻安全格全入 R - var frontier: Array = r.keys() + var frontier: Array[Variant] = r.keys() for x in frontier: if safe_set.has(x) and b.digit_of(x) == 0: for n in b.neighbors8(x): @@ -183,8 +183,8 @@ func measure_inferability(b: BoardData) -> Dictionary: var derived := [] for i in constraints.size(): for j in range(i + 1, constraints.size()): - var a: Dictionary = constraints[i] - var bb: Dictionary = constraints[j] + var a: Dictionary[Variant, Variant] = constraints[i] + var bb: Dictionary[Variant, Variant] = constraints[j] var sub := _subset_of(a.unknown, bb.unknown) if sub == 1: derived.append(_diff_constraint(a, bb)) @@ -264,7 +264,7 @@ func is_clearable(b: BoardData) -> bool: for c in b.safe_cells(): safe_set[c] = true var seen := {} - var queue: Array = b.spawn_cells().duplicate() + var queue: Array[Variant] = b.spawn_cells().duplicate() for c in queue: seen[c] = true while not queue.is_empty(): @@ -281,7 +281,7 @@ func is_clearable(b: BoardData) -> bool: return true -func legality(b: BoardData, params: Dictionary, db) -> bool: +func legality(b: BoardData, params: Dictionary, db) -> bool: # type-hint: ignore(同 generate——测量面 stub 注入) var reasons := _legality_faults(b, params) return reasons.is_empty() @@ -331,7 +331,7 @@ func _neighbor_mine_count(b: BoardData, c: Vector2i) -> int: # ==================== 产出上报(生成期指标集) ==================== -func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void: +func _finalize(b: BoardData, params: Dictionary, attempts_used: int, accept_mode: String, _db, r0_cap_rejected: int = 0) -> void: # type-hint: ignore(同 generate——测量面 stub 注入) var m := measure_inferability(b) # 产物断言(棋盘生成 出生区安全与反退化布雷):实际雷数 ≤ 目标雷数 N——先布后删只减不增,失败即致命 var n_target: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS))) @@ -364,7 +364,7 @@ func _badness(b: BoardData, params: Dictionary) -> Array: for c in b.safe_cells(): safe_set[c] = true var seen := {} - var queue: Array = b.spawn_cells().duplicate() + var queue: Array[Variant] = b.spawn_cells().duplicate() for c in queue: seen[c] = true while not queue.is_empty(): diff --git a/game/core/config_db.gd b/game/core/config_db.gd index e3a21806..5153622e 100644 --- a/game/core/config_db.gd +++ b/game/core/config_db.gd @@ -60,7 +60,8 @@ var _reload_gate := false # 数据配置 EC8——战斗结 func _init() -> void: - _resolver = ConfigDbResolver.new(self) # 协作类持门面引用(诊断/致命通道回呼) + _resolver = ConfigDbResolver.new(Callable(self, "push_diag"), Callable(self, "reset_warn_dedup"), + func() -> Variant: return board_gen.rows, func() -> Variant: return enemy.rows, func() -> Variant: return difficulty) # 协作类持门面引用(诊断/致命通道回呼) func push_diag(severity: String, category: String, message: String) -> void: @@ -71,20 +72,25 @@ func push_diag(severity: String, category: String, message: String) -> void: push_error("[ConfigDB/%s] %s" % [category, message]) -# 运行期数据致命(2026-09-18 用户拍板:越域记 Fatal 级日志后立即退出——数据配置通道 fail-fast 纪律补齐到查询期) +# 运行期数据致命(2026-09-18 用户拍板 fail-fast;迭代 27 T6——退出执行归根:核心层不再自携 +# quit/crash,越域记 Fatal 级日志后置 _fatal_flag,进程退出决策归组合根(main._ready 表加载 +# 失败已有同通道 fatal 屏+fail-fast 边界)——查询期消费方按返回值安全面走既有默认值回退) func _fatal(category: String, message: String) -> void: push_diag("fatal", category, message) - push_error("[ConfigDB/%s] FATAL: %s —— 数据配置致命,进程退出" % [category, message]) - var main_loop := Engine.get_main_loop() - if main_loop is SceneTree: - main_loop.quit(1) # 帧末退出,退出码 1——调用方须立即 return,勿再消费返回值 - else: - OS.crash("ConfigDB/%s fatal: %s" % [category, message]) + push_error("[ConfigDB/%s] FATAL: %s —— 数据配置致命(退出决策归组合根)" % [category, message]) + _fatal_flag = true + + +var _fatal_flag := false # 运行期致命旗(迭代 27 T6——退出归根承载;组合根/闸读面) + + +func is_fatal() -> bool: + return _fatal_flag func reset_diagnostics() -> void: _diagnostics.clear() - _resolver.reset_warn_dedup() # R1 告警去重注册表(协作类持钥——清空时点与拆分前一致) + _resolver._reset_warn.call() # R1 告警去重注册表(协作类持钥——清空时点与拆分前一致;通道 Callable 化随 T6) func diagnostics() -> Array: @@ -209,7 +215,7 @@ func variant_pool(level_index: int, board_type: String, node_type: String = "") continue if int(row.get("unlock_level", 999)) > level_index: continue - var tags: Array = row.get("board_types", []) + var tags: Array[Variant] = row.get("board_types", []) if tags.is_empty(): pool.append(row) continue @@ -250,10 +256,6 @@ func level_count() -> int: # ---- GP+商店房实施批(2026-09-21):ShopTable 消费面(行集解析单点——runner/战斗编排禁自扫表)---- # ── 秘宝访问器(迭代 7 T1——消费面一律走访问器,禁直读行集)── # 活行集(非 retired 非 deferred)——投放校验/奖励节点池/呈现可用性的唯一取数口径 -func treasure_active_rows() -> Array: - return treasure_items.active_rows() if treasure_items != null else [] - - func treasure_item_row(item_id: String) -> Dictionary: return treasure_items.row_by_id(item_id) if treasure_items != null else {} @@ -270,13 +272,6 @@ func treasure_attunement_rows() -> Array: # 活行词表(掉落投放 effect_id 白名单——过渡渠道口径) -func treasure_live_ids() -> Dictionary: - var out := {} - for row in treasure_transitional_rows(): - out[String(row.get("item_id", ""))] = true - return out - - # ── 同调轴访问器已删除(迭代 21 T5——attunement_growth 谓词表随槽位收编退役:槽位数量=树同调位 # 节点点亮数,判定源=Attunement.slots;boss_kill_count/clear_count 计数域保留——非槽位硬消费在册)── diff --git a/game/core/config_db_cross_validator.gd b/game/core/config_db_cross_validator.gd index 63de9684..252c3c58 100644 --- a/game/core/config_db_cross_validator.gd +++ b/game/core/config_db_cross_validator.gd @@ -77,7 +77,7 @@ func cross_validate(loaded: Dictionary, push_diag: Callable) -> void: var li: int = int(lrow.level_index) # 模板池感知(关卡节点图生成 v0.4——池内全模板跨表校验,不只 row.nodes) var node_lists: Array[Array] = [] - var tpls: Array = lrow.get("templates", []) + var tpls: Array[Variant] = lrow.get("templates", []) if tpls.is_empty(): node_lists.append(lrow.get("nodes", [])) else: @@ -137,7 +137,7 @@ func validate_skill_pool_links(skill_table: SkillTable, enemy_table: EnemyTable, continue var want_side: String = "enemy" if st == "buff" else "hero" var eff: String = String(row.get("buff_ref", "")) - var srow: Dictionary = status_table.row_by_id(eff) if status_table != null else {} + var srow: Dictionary[Variant, Variant] = status_table.row_by_id(eff) if status_table != null else {} if not srow.is_empty() and String(srow.get("target_side", "")) != want_side: errs.append({"category": "cast_side", "message": "SkillTable %s(%s)引用 %s 的 target_side=%s ≠ %s(施放类 side 守卫)" % [String(row.get("skill_id", "")), st, eff, String(srow.get("target_side", "")), want_side]}) for row in enemy_table.rows: diff --git a/game/core/config_db_resolver.gd b/game/core/config_db_resolver.gd index 73becc2a..15e81c87 100644 --- a/game/core/config_db_resolver.gd +++ b/game/core/config_db_resolver.gd @@ -11,12 +11,21 @@ class_name ConfigDbResolver extends RefCounted -var _db: ConfigDb # 门面引用(表读取 + 诊断/致命通道——_init 注入) +var _diag: Callable # 诊断/致命通道(迭代 27 T6——门面类型反持解耦,validator 先例:签名 (level, category, message)) +var _reset_warn: Callable # 去重注册表清空通道(reset_diagnostics 时点由门面驱动) +var _board_gen_rows_get: Callable # () -> Array——board_gen 行集活读(数据面经 Callable 注入,ConfigDb 类型环断根) +var _enemy_rows_get: Callable # () -> Array——enemy 行集活读 +var _difficulty_get: Callable # () -> DifficultyTable(可空——表缺省合法态) var _warn_dedup: Dictionary[String, bool] = {} # R1 四元组去重注册表(自 ConfigDb 迁入) -func _init(db_ref: ConfigDb) -> void: - _db = db_ref +func _init(diag_cb: Callable, reset_warn_cb: Callable, board_gen_rows_cb: Callable, + enemy_rows_cb: Callable, difficulty_cb: Callable) -> void: + _diag = diag_cb + _reset_warn = reset_warn_cb + _board_gen_rows_get = board_gen_rows_cb + _enemy_rows_get = enemy_rows_cb + _difficulty_get = difficulty_cb # 去重注册表清空(门面 load_tables / reset_diagnostics 调用——与拆分前清空时点逐位一致) @@ -27,21 +36,22 @@ func reset_warn_dedup() -> void: # ---- R1 难度行解析(2026-09-18 用户拍板修订:ceil 向上取整保证偏难;域越界与内容缺口 Fatal 退出)---- func resolve_board_gen(L: int, D: int) -> Dictionary: - return _resolve_progress(_db.board_gen.rows, L, D, "board_gen") + return _resolve_progress(_board_gen_rows_get.call(), L, D, "board_gen") func resolve_enemy_rowset(L: int, D: int) -> Array[Dictionary]: # 行集数组形态(2026-10-07 敌人表行集多行批——原单行 Dictionary 退役):ceil 命中 (l*,d*) 后 # 返回该坐标全部行、按表文件行序(行序即契约——同坐标多行文件内相邻由 EnemyTable.validate 兜底)。 # 空集表达 = 空数组(生产表下 ceil 命中坐标恒有行,空集仅表空时可达——validate 已拦)。 - return _resolve_progress(_db.enemy.rows, L, D, "enemy", true) + return _resolve_progress(_enemy_rows_get.call(), L, D, "enemy", true) # 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故) func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: - if _db.difficulty == null: + var dt = _difficulty_get.call() + if dt == null: return {} - for row in _db.difficulty.rows: + for row in dt.rows: if int(row.get("level_index", 0)) == L and int(row.get("node_depth", -1)) == D: return { "hp_mult": clampf(float(row.get("hp_mult", 1.0)), 0.1, 10.0), @@ -52,7 +62,7 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary: func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all := false) -> Variant: if L < 1 or D < 0: - _db._fatal("request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) + _diag.call("fatal", "request_oob", "%s 请求越域 (%d,%d)——进度坐标低于合法域(关卡序号≥1、节点深度≥0)" % [table_id, L, D]) return {} var level_keys: Array[int] = [] for row in rows: @@ -60,7 +70,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all if not level_keys.has(lv): level_keys.append(lv) if level_keys.is_empty(): - _db._fatal("structural", "%s 行集为空——锚行校验应已拦截" % table_id) + _diag.call("fatal", "structural", "%s 行集为空——锚行校验应已拦截" % table_id) return {} level_keys.sort() var l_star := _ceil_key(level_keys, L) @@ -96,7 +106,7 @@ func _resolve_progress(rows: Array, L: int, D: int, table_id: String, return_all if not _warn_dedup.has(dedup_key): _warn_dedup[dedup_key] = true var verb := "clamp 解析(超出最大行取最大值)" if missing.contains("clamp") else "ceil 解析(向上取整保证偏难)" - _db.push_diag("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) + _diag.call("warning", "resolve_" + missing, "%s 请求 (%d,%d) %s → (%d,%d) [%s]——2026-09-18 拍板" % [table_id, L, D, verb, l_star, d_star, missing]) if return_all: var all_out: Array[Dictionary] = [] for row in rows: diff --git a/game/core/damage_controller.gd b/game/core/damage_controller.gd index c175a501..eff8c656 100644 --- a/game/core/damage_controller.gd +++ b/game/core/damage_controller.gd @@ -95,14 +95,10 @@ func set_fx(fx: EffectEngine) -> void: func _fx_hits(event: String, payload: Dictionary) -> Array: if _fx == null: return [] - return _fx.dispatch(event, payload, Callable(self, "_fx_fact")) + return _fx.dispatch(event, payload, _fx_fact) # 事件发射薄面(免施加观测位——enemy_hp_threshold 等现无订阅者的事件保留零扰动面) -func _fx_dispatch(event: String, payload: Dictionary) -> void: - _fx_hits(event, payload) - - # 补充事实面(fact 取值第二优先源——payload 未携带的运行态读数:charge_layers 编排器每轮注入。 # 迭代 20 T1——hp_ratio 补入〔背水 when 面,敌技 damage_taken 载荷不带血比例时回落〕; # fx_reveal_request=揭示效果回调〔斩首学者 kill 订阅面——编排器注入,入队授予段末兑付〕) @@ -113,7 +109,7 @@ func _fx_fact(fact: String): "dig_streak": return dig_streak "hp_ratio": - var hu: Dictionary = units.hero_unit() + var hu: Dictionary[Variant, Variant] = units.hero_unit() return int(round(100.0 * float(int(hu.get("hp", 0))) / float(maxi(1, int(hu.get("max_hp", 1)))))) # 百分比 int(when lte int 截断口径) return null @@ -268,18 +264,33 @@ func _resolve(t: Dictionary) -> void: # 主角先手攻击:掷点 → 折扣/暴击 → 克制点数 → hero_offense×enemy_defense 池乘区 → 下限 → 交付 # (内容数据驱动扩展批 T3——attack_commit/clamp_check/armor_roll 订阅施加面:命中=开单入口随单 -# context 携带,本函数按迁移前算序逐族施加——命中序=数据表声明序,确定性) +# context 携带,按迁移前算序逐族施加——命中序=数据表声明序,确定性)。 +# 迭代 27 T2 姊妹病灶拆段(P1-1):原 148 行单体拆四步——命中面(_hero_attack_ctx,目标/护甲/ +# 掷点/按序改写族四拍/克制点数) → 基值装配(_hero_attack_base_value,加法族/计数器施加/倍乘) → +# 掷点兑现(_hero_attack_redeem,rolled/池乘/下限/rider/落地/击杀发射) → 交付(_hero_attack_deliver, +# 状态施加/标签/明细行)。步骤间经 ctx/redemption 字典穿线——算序/取值逐字保留(🔴 逐位一致契约)。 func _resolve_hero_attack(t: Dictionary) -> void: + var ctx := _hero_attack_ctx(t) + if ctx.is_empty(): + return # 不可达(流程开单前已有存活守卫)——防御位不发行(outcome 已置 zero) + var base_value: float = _hero_attack_base_value(t, ctx) + var redemption := _hero_attack_redeem(t, ctx, base_value) + _hero_attack_deliver(t, ctx, redemption) + + +# 命中面(原 :273-328 逐字):目标/护甲克制 matchup/armor_roll 发射/按序改写族四拍(矿工重锤命中 +# 交换→专注免掷→厚积薄发必中→猎弱折扣移档)/克制点数读值+订阅改写。返回穿线 ctx(空=目标不可达)。 +func _hero_attack_ctx(t: Dictionary) -> Dictionary: var target = _target_for(String(t.target_rule)) if target == null: _outcomes[int(t.ticket_id)] = "zero" # 不可达(流程开单前已有存活守卫)——防御位不发行 - return + return {} var armor: String = String(target.get("armor_type", "light")) var atk_type: String = String(t.attack_type) var matchup := (atk_type + "_vs_" + armor) if atk_type != "" else "" - var ac_hits: Array = (t.context.get("fx_attack_hits", []) as Array) - var ac_payload: Dictionary = (t.context.get("fx_attack_payload", {}) as Dictionary) - var roll_read: Dictionary = _armor_roll() + var ac_hits: Array[Variant] = (t.context.get("fx_attack_hits", []) as Array) + var ac_payload: Dictionary[Variant, Variant] = (t.context.get("fx_attack_payload", {}) as Dictionary) + var roll_read: Dictionary[Variant, Variant] = _armor_roll() var threshold: int = defender_ac(target) + int(db.settlement.attack_bonus) # armor_roll 发射点(内容数据驱动扩展批 T1——敌护甲掷骰判定;免掷恒折扣订阅面) var ar_hits := _fx_hits("armor_roll", {"target_ac": threshold, "roll": int(roll_read.roll)}) @@ -326,10 +337,20 @@ func _resolve_hero_attack(t: Dictionary) -> void: # 淬毒刃纹(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:特判拆除——行 triggers clamp_check # modify(mode:clamp_bonus) 订阅命中已并入上方 delta 合计;mode 路由=主角攻击面专享, # 棋盘克制直伤窗只消费无模式 delta——棋盘不吃毒面与迁移前一致) + return {"target": target, "armor": armor, "atk_type": atk_type, "ac_hits": ac_hits, + "ac_payload": ac_payload, "roll_read": roll_read, "threshold": threshold, + "crit": crit, "discount": discount, "spike_ready": spike_ready, "cdelta": cdelta} + + +# 基值装配(原 :329-355 逐字):基值+单位标记加伤+矿工重锤增伤面+加法族(attack_bonus/ +# value_counter 层值)+计数器施加(attack_commit 用后清零行——命中序=声明序)+spike 倍乘。 +func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float: + var ac_hits: Array[Variant] = ctx["ac_hits"] + var ac_payload: Dictionary[Variant, Variant] = ctx["ac_payload"] var base_value := float(t.base_value) # 单位标记加伤(迭代 20 T1——处刑台 target_attack_bonus 挂单位字典 fx_atk_bonus 键; # 阈值沿发射点已写入,本处通用消费——对该敌伤害加算) - base_value += float(int(target.get("fx_atk_bonus", 0))) + base_value += float(int((ctx["target"] as Dictionary).get("fx_atk_bonus", 0))) # 按序改写族第五拍:矿工重锤增伤面(交换的正向侧) for h in ac_hits: if String(h.get("kind", "")) == "modify_ticket" and int(h.get("trade_atk", 0)) > 0: @@ -349,17 +370,27 @@ func _resolve_hero_attack(t: Dictionary) -> void: var set_v = h.get("set", null) _fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) # 任意真族:厚积薄发满层 spike 倍乘(≥3 层——强制必中已在折扣拍承载) - if spike_ready: + if ctx["spike_ready"]: for h in ac_hits: if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "spike": base_value *= float(int(h.get("value", 1))) - var rolled: int = DamageMath.rolled_component(base_value, discount, crit, int(db.settlement.crit_multiplier), cdelta) - var payload: Dictionary = {"target": String(target.display_name)} + return base_value + + +# 掷点兑现(原 :356-402 逐字):rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/ +# 击杀标记/kill 发射+贯穿弹射。返回 {landed, payload, hero_rider}(交付步消费)。 +func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> Dictionary: + var target = ctx["target"] + var ac_hits: Array[Variant] = ctx["ac_hits"] + var roll_read: Dictionary[Variant, Variant] = ctx["roll_read"] + var atk_type: String = ctx["atk_type"] + var rolled: int = DamageMath.rolled_component(base_value, int(ctx["discount"]), bool(ctx["crit"]), int(db.settlement.crit_multiplier), int(ctx["cdelta"])) + var payload: Dictionary[Variant, Variant] = {"target": String(target.display_name)} payload[atk_type] = rolled # 分量键 = 实际攻击类型(归因字段,单分量模型) payload["roll"] = int(roll_read.roll) - payload["ac"] = threshold - payload["tier"] = discount - if crit: + payload["ac"] = int(ctx["threshold"]) + payload["tier"] = int(ctx["discount"]) + if bool(ctx["crit"]): payload["crit"] = true var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense"), clamped_pool("enemy_defense")) var landed: int = maxi(int(db.settlement.damage_floor), pooled) @@ -370,12 +401,12 @@ func _resolve_hero_attack(t: Dictionary) -> void: var hero_rider := rider_tier_bonus for h in ac_hits: if String(h.get("kind", "")) == "add_ticket_rider": - var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ac_payload) + var contrib := int(h.get("value", 0)) * _per_value(String(h.get("per", "")), ctx["ac_payload"]) if h.has("cap"): contrib = mini(int(h.cap), contrib) hero_rider += contrib if hero_rider > 0: - if crit and rider_tier_crit: + if bool(ctx["crit"]) and rider_tier_crit: hero_rider = int(round(float(hero_rider) * float(db.settlement.crit_multiplier))) landed += hero_rider payload["hero_rider"] = hero_rider @@ -400,17 +431,23 @@ func _resolve_hero_attack(t: Dictionary) -> void: _queue.append(of_t) else: _resolve(of_t) + return {"landed": landed, "payload": payload, "hero_rider": hero_rider} + + +# 交付(原 :403-414 逐字):即时执行族(碎甲震击护甲裂解)/标签装配/明细行发射/兑现登记。 +func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary) -> void: + var payload: Dictionary[Variant, Variant] = redemption["payload"] # 即时执行族:碎甲震击打重甲附加「护甲裂解」(复用 off_guard——零新状态行;apply_effect 域闭/ # 重复授予由状态容器自行守卫) - for h in ac_hits: + for h in (ctx["ac_hits"] as Array): if String(h.get("kind", "")) == "apply_status": # 迭代 20 T9——source_class 中性名(旧件名 feat_armor_crack 随碎甲震击退役;apply_status 通道保留泛用面) effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1) payload["cracked"] = true - var label := _attack_label(String(t.display), crit, discount) - if hero_rider > 0: - label += "+奥术%d(必中)" % hero_rider - emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, landed, payload, String(t.highlight)) + var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"])) + if int(redemption["hero_rider"]) > 0: + label += "+奥术%d(必中)" % int(redemption["hero_rider"]) + emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), label, int(redemption["landed"]), payload, String(t.highlight)) _outcomes[int(t.ticket_id)] = "redeemed" @@ -418,8 +455,8 @@ func _resolve_hero_attack(t: Dictionary) -> void: # 前态恒中性 delta 0,写法同 hero_attack 的 _counter_delta 消费位)→ hero_defense×enemy_output # 池乘区 → 下限 → +rider(后置必中附加)→ 交付 → commit_action(作废单不 commit——死亡即不动手) func _resolve_enemy_skill(t: Dictionary) -> void: - var hero: Dictionary = units.hero_unit() - var roll_read: Dictionary = _armor_roll() + var hero: Dictionary[Variant, Variant] = units.hero_unit() + var roll_read: Dictionary[Variant, Variant] = _armor_roll() var threshold: int = defender_ac(hero) + int(db.settlement.attack_bonus) var crit: bool = bool(roll_read.crit) var discount: int = _roll_discount(int(roll_read.roll), threshold) @@ -448,7 +485,7 @@ func _resolve_enemy_skill(t: Dictionary) -> void: if shield > 0: total = maxi(int(db.settlement.damage_floor), total - shield) units.apply_damage(hero, total) - var payload: Dictionary = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)), + var payload: Dictionary[Variant, Variant] = {"target": "hero", "skill_id": String(t.context.get("skill_id", "")), "baseline": int(t.context.get("baseline", 0)), "roll": int(roll_read.roll), "ac": threshold, "tier": discount, "attack_type": String(t.attack_type)} if crit: payload["crit"] = true @@ -488,7 +525,7 @@ func _resolve_board_typed(t: Dictionary) -> void: # 棋盘群伤:无类型恒定表值,结账时刻全体存活敌逐个交付(单行汇总——行内 count = 兑现时存活数) func _resolve_board_blast(t: Dictionary) -> void: - var alive: Array = units.query_alive("enemy") + var alive: Array[Variant] = units.query_alive("enemy") if alive.is_empty(): _ineffective_row(t, "无存活敌人") _outcomes[int(t.ticket_id)] = "zero" @@ -505,7 +542,7 @@ func _resolve_board_blast(t: Dictionary) -> void: func _resolve_board_flat_hero(t: Dictionary) -> void: var dealt: int = maxi(int(db.settlement.damage_floor), int(t.base_value)) # 伤害下限全来源(2026-09-20) units.apply_damage(units.hero_unit(), dealt) - var extra: Dictionary = {"target": "hero"} + var extra: Dictionary[Variant, Variant] = {"target": "hero"} for k in t.context.keys(): extra[k] = t.context[k] emit_row_cb.call("damage", String(t.source_class), t.origin_cell, int(t.round_index), "%s → 主角" % String(t.display), dealt, extra, String(t.highlight)) @@ -537,13 +574,13 @@ func defender_ac(unit: Dictionary) -> int: if total < 0: total = int(db.hero.armor_class) for eff in effects.query_active("hero"): - var row: Dictionary = db.status_effect_row(eff.effect_id) + var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id) if not row.is_empty() and String(row.unit) == "ac": total += int(eff.effective_value) return total var total_e: int = int(unit.get("armor_class", 0)) for eff_e in effects.query_active("enemy"): - var erow_ac: Dictionary = db.status_effect_row(eff_e.effect_id) + var erow_ac: Dictionary[Variant, Variant] = db.status_effect_row(eff_e.effect_id) if not erow_ac.is_empty() and String(erow_ac.unit) == "ac": total_e += int(eff_e.effective_value) return total_e @@ -554,14 +591,14 @@ func defender_ac(unit: Dictionary) -> int: func _counter_delta(attack_type: String, armor: String) -> int: if attack_type == "": return 0 - var tbl: Dictionary = db.settlement.armor_effectiveness - var row: Dictionary = tbl.get(attack_type, {}) + var tbl: Dictionary[Variant, Variant] = db.settlement.armor_effectiveness + var row: Dictionary[Variant, Variant] = tbl.get(attack_type, {}) return int(row.get(armor, 0)) # 折扣档改善(迭代 12 T4 猎弱本能——折扣表向全额方向移 value 档;查不到当前档 = 不动防御口径) func _discount_shift_up(discount: int, shift: int) -> int: - var tbl: Array = db.settlement.roll_tier_discounts + var tbl: Array[Variant] = db.settlement.roll_tier_discounts var idx := tbl.find(discount) if idx < 0: return discount @@ -572,7 +609,7 @@ func _discount_shift_up(discount: int, shift: int) -> int: func clamped_pool(pool: String) -> int: var total := 0 for eff in effects.query_active("hero") + effects.query_active("enemy"): - var row: Dictionary = db.status_effect_row(eff.effect_id) + var row: Dictionary[Variant, Variant] = db.status_effect_row(eff.effect_id) if row.is_empty(): continue if String(db.settlement.effect_pool_map.get(eff.effect_id, "")) == pool and String(row.unit) == "percent": diff --git a/game/core/effect_engine.gd b/game/core/effect_engine.gd index 5c718fd9..6c660a29 100644 --- a/game/core/effect_engine.gd +++ b/game/core/effect_engine.gd @@ -91,8 +91,8 @@ const DO_KEYS := ["kind", "value", "value_from", "delta", "delta_from", "mult", const ACTIVE_KINDS := ["reveal_tiles", "preview", "grant_ap", "remove_mines", "refresh_pick", "safe_dig"] # ---- 订阅集与计数器账本 ---- -var _triggers: Array = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死) -var _counters: Dictionary = {} # counter_id → int(battle 域账本;重建清零;重置点由订阅行声明) +var _triggers: Array[Variant] = [] # [{source, id, on, when, do}](装载序=声明序,重建时定死) +var _counters: Dictionary[Variant, Variant] = {} # counter_id → int(battle 域账本;重建清零;重置点由订阅行声明) var _reset_on_round_start: Array[String] = [] # round_start 重置点计数器名单(装载时收集) var _adventure_counter_ids: Array[String] = [] # domain=adventure 计数器名单(T6——重建时收集、不清零; # battle 末 export → 冒险账本、begin_battle seed 回灌) @@ -171,7 +171,7 @@ func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void: func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) -> void: var on := String(tr.get("on", "")) assert(EVENTS.has(on), "[效果引擎] 订阅事件出域: %s(%s %s)" % [on, source, id]) - var do_d: Dictionary = (tr.get("do", {}) as Dictionary).duplicate(true) + var do_d: Dictionary[Variant, Variant] = (tr.get("do", {}) as Dictionary).duplicate(true) # *_from 解析(rebuild 时把行字段引用定影为值——value_from:"magnitude" → value=行 magnitude) for pair in [["value_from", "value"], ["delta_from", "delta"], ["mult_from", "mult"], ["cap_from", "cap"]]: if do_d.has(pair[0]): @@ -179,9 +179,9 @@ func _push_trigger(source: String, id: String, tr: Dictionary, row: Dictionary) do_d.erase(pair[0]) # when value_from 同面解析(行字段引用条件——dawn_order effective_rounds 先例:字面量出行=数值 # 单一事实源破面,行引用随 rebuild 定影) - var when_d: Array = (tr.get("when", []) as Array).duplicate(true) + var when_d: Array[Variant] = (tr.get("when", []) as Array).duplicate(true) for c_v in when_d: - var c: Dictionary = c_v as Dictionary + var c: Dictionary[Variant, Variant] = c_v as Dictionary if c.has("value_from"): c["value"] = int(row.get(String(c["value_from"]), 0)) c.erase("value_from") @@ -206,7 +206,7 @@ func has_effect_on(event: String, kind: String, mode: String = "") -> bool: for tr in _triggers: if String(tr.on) != event: continue - var d: Dictionary = tr.do + var d: Dictionary[Variant, Variant] = tr.do if String(d.get("kind", "")) == kind and (mode == "" or String(d.get("mode", "")) == mode): # 有 when 时不能预判命中(发射点缺省行为保守按「可能命中」计——命中由 dispatch 定夺) return true @@ -221,7 +221,7 @@ func dispatch(event: String, payload: Dictionary, facts: Callable = Callable()) if event == "round_start": for cid in _reset_on_round_start: _counters[cid] = 0 - var hits: Array = [] + var hits: Array[Variant] = [] for tr in _triggers: if String(tr.on) != event: continue @@ -350,8 +350,8 @@ func _counter_delta_of(matchup: String) -> int: var parts := matchup.split("_vs_") if parts.size() != 2: return 0 - var tbl: Dictionary = db.settlement.armor_effectiveness - var row: Dictionary = tbl.get(String(parts[0]), {}) + var tbl: Dictionary[Variant, Variant] = db.settlement.armor_effectiveness + var row: Dictionary[Variant, Variant] = tbl.get(String(parts[0]), {}) return int(row.get(String(parts[1]), 0)) @@ -362,7 +362,7 @@ func _counter_delta_of(matchup: String) -> int: static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, errs: Array) -> bool: var mine_covered := false for tr_v in triggers: - var tr: Dictionary = tr_v as Dictionary + var tr: Dictionary[Variant, Variant] = tr_v as Dictionary for k in tr.keys(): if not ["on", "when", "do"].has(String(k)): errs.append({"category": "zero_flat", "message": "%s trigger 键出白名单: %s" % [rid, String(k)]}) @@ -372,7 +372,7 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, elif MINE_EVENTS.has(on): mine_covered = true for c_v in (tr.get("when", []) as Array): - var c: Dictionary = c_v as Dictionary + var c: Dictionary[Variant, Variant] = c_v as Dictionary for k in c.keys(): if not WHEN_KEYS.has(String(k)): errs.append({"category": "zero_flat", "message": "%s when 键出白名单: %s" % [rid, String(k)]}) @@ -381,7 +381,7 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, errs.append({"category": "enum", "message": "%s fact 出域(事实词汇封闭 9 项): %s" % [rid, fact]}) if not OPS.has(String(c.get("op", "eq"))): errs.append({"category": "enum", "message": "%s when 算子出域: %s" % [rid, String(c.get("op", ""))]}) - var do_d: Dictionary = tr.get("do", {}) as Dictionary + var do_d: Dictionary[Variant, Variant] = tr.get("do", {}) as Dictionary for k in do_d.keys(): if not DO_KEYS.has(String(k)): errs.append({"category": "zero_flat", "message": "%s do 键出白名单: %s(机制轴禁无条件属性加项)" % [rid, String(k)]}) @@ -396,7 +396,7 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, if fld != "" and not row.has(fld): errs.append({"category": "dangling", "message": "%s do.%s 悬空(行无字段 %s)" % [rid, vf_k, fld]}) for c_v in (tr.get("when", []) as Array): - var c_w: Dictionary = c_v as Dictionary + var c_w: Dictionary[Variant, Variant] = c_v as Dictionary if c_w.has("value_from"): var wfld := String(c_w.get("value_from", "")) if wfld != "" and not row.has(wfld): @@ -413,21 +413,4 @@ static func validate_trigger_row(rid: String, row: Dictionary, triggers: Array, ## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。 ## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。 ## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、 -## rewrite_board、apply_status——发射点逐条执行。 -static func combo_of(kind: String) -> String: - match kind: - "add_ticket_rider", "attack_bonus", "grant_material", "counter_add", "shield", "heal", "ac_bonus", "damage_reduce", "bonus_delta": - return "additive" - "grant_ap": - return "additive" # mode:transfer 由发射点即时执行族承载(转移非加法) - "yield_mult", "damage_mult": - return "multiplicative" - "spawn_spell", "overflow", "spike": - return "any_true" - "modify_ticket": - return "ordered_write" - "counter_spend": - return "ordered_spend" - "open_ticket", "apply_status", "blast_scaled": - return "immediate" - return "immediate" # 默认即时执行(每条命中各自执行一次) +## rewrite_board、apply_status——发射点逐条执行。 \ No newline at end of file diff --git a/game/core/feats_engine.gd b/game/core/feats_engine.gd deleted file mode 100644 index 2e324a72..00000000 --- a/game/core/feats_engine.gd +++ /dev/null @@ -1,39 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# Date: 2026-10-04(双层 BD 实施批 T1——标签档位激活引擎) -# 流派成型判据机制化(整合定稿 设计工具 1——以撒套装/小骨铭文先例): -# 同流派标签 relic 计数 ≥2 → 档一(流派核心规则);≥4 → 档二(完全体);<2 = 散件(单体保底—— -# 每件效果独立生效,档位只做质变不做启动,反例 B-2)。 -# 纯函数零 await 零 RNG 零副作用(Attunement 求值服务同款纪律);核心层零特判延伸——引擎只读 -# feat_table 行集与持有集,接线点消费 tiers/hooks 投影。 -class_name FeatsEngine -extends RefCounted - - -## 档位求值:feat_ids 持有集 × 行集 → {archetype: tier}(tier ∈ 0/1/2;无行流派不出现) -static func tiers_for(feat_ids: Array, table: FeatTable) -> Dictionary: - var counts := {} - for rid in feat_ids: - var row: Dictionary = table.row_by_id(String(rid)) if table != null else {} - if row.is_empty(): - continue - var arch := String(row.get("archetype", "")) - counts[arch] = int(counts.get(arch, 0)) + 1 - var out := {} - for arch in counts.keys(): - var n := int(counts[arch]) - if n >= 4: - out[arch] = 2 - elif n >= 2: - out[arch] = 1 - else: - out[arch] = 0 - return out - - -## 挂点投影随 T3 退役(内容数据驱动扩展批——hook 词族消亡:订阅投影=效果引擎 rebuild 装载序 -## (feats 表序),投影稳定性由 EffectEngine._triggers 声明序承载) - - -## 成型判读(读数面口径):流派 tier ≥1 = 成型(档一即判——需求卡片完成定义「标签+档位激活引擎可判成型」) -static func is_formed(tiers: Dictionary, archetype: String) -> bool: - return int(tiers.get(archetype, 0)) >= 1 diff --git a/game/core/feats_engine.gd.uid b/game/core/feats_engine.gd.uid deleted file mode 100644 index 1d722a33..00000000 --- a/game/core/feats_engine.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://6d12lkx5w2x8 diff --git a/game/core/level_runner.gd b/game/core/level_runner.gd index 264868ac..dfe1445d 100644 --- a/game/core/level_runner.gd +++ b/game/core/level_runner.gd @@ -47,7 +47,7 @@ var node_index := -1 # ---- 账本载体(冒险结构实现批——漂移②清偿)---- # 冒险模式 = AdventureRunner 共享字典(Dictionary 引用共享——hero_hp/gp 属性读写直接落冒险账本); # 单关模式 = 本实例私有账本(start_level 重建,既有单关语义逐位保留)。 -var _ledger: Dictionary = {"hero_hp": 0, "gp": 0} +var _ledger: Dictionary[Variant, Variant] = {"hero_hp": 0, "gp": 0} var _rest_handler: Callable # 治疗封顶回报执行体(默认内置 rest_heal_amount;冒险模式 = AdventureRunner.apply_rest——接口③) var _external := false # 冒险模式标记(钱包收束时机与播种语义分叉点) # hero_hp / gp 属性化(既有读写点零改动——值类型经属性落 _ledger) @@ -57,28 +57,25 @@ var hero_hp: int: var gp: int: # GP 冒险钱包(局内货币——冒险开始 = 0、收束清零;不入存档不入摘要) get: return int(_ledger["gp"]) set(v): _ledger["gp"] = v -var bonus := {"atk": 0, "max_hp": 0, "armor_class": 0} # 统一冒险加算区(冒险开始全 0)——商店购买与关卡大奖励写同一本账(冒险模式 = AdventureRunner 共享字典;大奖励接线归 AdventureRunner._on_level_finished) var base_override := {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat 注入(元进度实现批——冒险模式 = AdventureRunner 共享字典经 start_level_external 注入;单关测试模式 = 私有全零) var _shop_purchased := {} # product_id → 已购档数(现价重算输入;冒险开始清零) # 协作对象私有(E1/E3 封装收口 2026-09-13):settlement 读取经 current_battle() 成文出口 var _settlement: BattleOrchestrator = null # 当前战斗编排器(关卡结束置 null) var treasure_inventory: TreasureItems = null # 秘宝 关卡域库存(迭代 7 T3——TreasureItems;跨战斗延续, var _treasure_unlocked := {} # aux 解锁门控集(迭代 7 T5——拍板③甲;空集=零投放) -var _attunement: Array = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动) -var _attune_charges: Dictionary = {} # 同调主动件充能账本(迭代 11 第①批 T6——adventure 共享 dict 引用;settlement use_treasure 扣减直改同引用) +var _attunement: Array[Variant] = [] # 同调装配集(迭代 11 第①批 T5——adventure params.attunement 下传,begin_battle enc 携带进战斗结算;空集=off 零扰动) # 关卡末清空+快照计数入 level_finished 载荷) -var _vision_counter: Dictionary = {"used": 0} # 真视类工具使用计数(迭代 22 秘宝批 T3——adventure 共享 dict 引用;编排器受理成功 +1,收束聚合入档) # 两根类型轴(迭代 23 T4——adventure 参数面快照经 set_hero_axes 下传;空串=未注入→锻造配置默认值回退。 # 消费面:effective_ac 甲型基础 AC 层 + begin_battle enc 携带进 unit_container.spawn 英雄快照) var _hero_attack_type := "" var _hero_armor_type := "" # ---- 商店共享池模式态(迭代 11 第②批 T3——缺省 legacy=固定三件加定价 off 对照承载,零 RNG 零池消费;TP0 令牌池换代)---- var _shop_pool_mode := false # pool 模式旗(set_shop_pool 注入时置位——AdventureRunner 按 params.shop_mode 下传) -var _pool_deck: Array = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点) +var _pool_deck: Array[Variant] = [] # 共享池令牌库共享引用(AdventureRunner._curio_deck——库存=剩余令牌数单点) var _pool_consume := Callable() # 池消费 handler(AdventureRunner._consume_pool_purchase——_apply_curio_pick 同口写加算区+账本+摘牌) var _pool_ticket := Callable() # 门票展示判定 handler(T5——AdventureRunner._feat_ticket_consider;节点与商店共享计数) var _pool_cap_check := Callable() # cap 检查回调(迭代 20 T2——AdventureRunner._curio_cap_ok;案②提前退役) -var _shop_shelf: Array = [] # 当前货架(进店抽 3/买后全重抽——令牌字典 {"kind","tier"} 序,同 kind 去重每 kind 至多一卡) +var _shop_shelf: Array[Variant] = [] # 当前货架(进店抽 3/买后全重抽——令牌字典 {"kind","tier"} 序,同 kind 去重每 kind 至多一卡) var _shop_visit_buys := 0 # 本次进店已购次数(A 链定价输入——离开清零) var _shop_paid_refreshes := 0 # 本次进店已付费刷新次数(MC-1——GP 刷新价曲线输入,离开清零) @@ -115,12 +112,12 @@ func set_hero_override(atk_pct: int, hp_pct: int) -> void: # 叠加次序(元进度实现批钉死):表值 → 永久强化 flat(base_override)→ 加算区 flat(bonus)先加 # → 调试浮窗百分比后乘(调试数值工具恒乘最后——代拍板 6,与 GP 批既有次序兼容) func effective_atk() -> int: - var base: int = int(db.hero.atk) + int(base_override.get("atk", 0)) + int(bonus.get("atk", 0)) + var base: int = int(db.hero.atk) + int(base_override.get("atk", 0)) + int(run_ledger.bonus.get("atk", 0)) return maxi(1, int(round(float(base) * (100.0 + float(hero_atk_pct)) / 100.0))) func effective_max_hp() -> int: - var base: int = int(db.hero.max_hp) + int(base_override.get("max_hp", 0)) + int(bonus.get("max_hp", 0)) + var base: int = int(db.hero.max_hp) + int(base_override.get("max_hp", 0)) + int(run_ledger.bonus.get("max_hp", 0)) return maxi(1, int(round(float(base) * (100.0 + float(hero_hp_pct)) / 100.0))) @@ -128,14 +125,14 @@ func effective_max_hp() -> int: # 甲路线等级加值〔base_override.armor_class——hero_override 锻造分量〕+ 加算区 armor_class 轴; # 无调试浮窗百分比通道,纯 flat) func effective_ac() -> int: - return maxi(0, _armor_base_ac() + int(base_override.get("armor_class", 0)) + int(bonus.get("armor_class", 0))) + return maxi(0, _armor_base_ac() + int(base_override.get("armor_class", 0)) + int(run_ledger.bonus.get("armor_class", 0))) # 甲型基础 AC(三层之第一层——选中甲路线行 base_ac;未注入/缺行=锻造配置默认值路线=轻甲 10, # 与 hero.tres 两轴删除前 armor_class=10 行为连续) func _armor_base_ac() -> int: var at: String = _hero_armor_type if _hero_armor_type != "" else db.forge_default_armor_type() - var row: Dictionary = db.forge_armor_route_by_type(at) + var row: Dictionary[Variant, Variant] = db.forge_armor_route_by_type(at) if row.is_empty(): return int(db.hero.armor_class) # 表缺失防御兜底(现役基准) return int(row.get("base_ac", 0)) @@ -144,7 +141,7 @@ func _armor_base_ac() -> int: # 行动点加成生效值(迭代 10 奖励重构批——统一冒险加算区第四轴 ap;经战斗开始入参 hero_ap_bonus # 传入战斗域,兑现 = 每场战斗开始首轮行动点基线 +加成值〔行动经济 GDD 跨冒险行动点加成轴节〕) func effective_ap_bonus() -> int: - return maxi(0, int(bonus.get("ap", 0))) + return maxi(0, int(run_ledger.bonus.get("ap", 0))) # 覆盖复位(关卡结束归零——表值不可变,仅清百分比;调用方兼容保留) @@ -163,7 +160,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = # 三键 {atk, max_hp, armor_class}、缺键/-1 = 0、负值 clamp 0。游戏直入口仍恒白板—— # 注入面只服务数值验证工具,非玩家可达路径) _external = false - _rest_handler = Callable(self, "rest_heal_amount") + _rest_handler = rest_heal_amount _ledger = {"hero_hp": 0, "gp": 0} var row := _find_level_row(level_index) if row.is_empty(): @@ -176,7 +173,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = visited = [] _graph_mode = false gp = 0 # 冒险钱包清零开局(GP+商店房实施批——局内货币生命周期起点) - bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0 + run_ledger.bonus = {"atk": 0, "max_hp": 0, "armor_class": 0} # 加算区冒险开始全 0(单关入口零元进度——注入面见上) base_override = {"atk": 0, "max_hp": 0, "armor_class": 0} # 永久强化 flat:缺省全零白板(单关入口零元进度——注入面见上) _shop_purchased = {} # 已购档数清零(跨冒险不延续——递增定价按冒险内累计) for k in base_override.keys(): @@ -209,7 +206,7 @@ func start_level(level_index: int, seed_val: int, base_override_in: Dictionary = # 信号顺序契约:呈现层须先经 level_started 重取 runner 实例,choices_presented 才不会被 # _on_choices_presented 的 runner 空防御静默丢弃。入口呈现零 RNG 消耗、pick 走同一个 # _enter_node(entry)——与旧自动进入逐位等价(测量面零漂移红线)。 -func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dictionary, +func start_level_external(level_index: int, ledger: Dictionary, purchased_ref: Dictionary, rest_handler: Callable, base_override_ref: Dictionary = {}, defer_entry: bool = false, template_pick: int = -1, resume_state: Dictionary = {}) -> bool: var row := _find_level_row(level_index) @@ -218,7 +215,6 @@ func start_level_external(level_index: int, ledger: Dictionary, bonus_ref: Dicti return false _external = true _ledger = ledger - bonus = bonus_ref base_override = base_override_ref _shop_purchased = purchased_ref _rest_handler = rest_handler @@ -278,7 +274,7 @@ func _restore_to_stop(node_id: int, visited_ids: Array) -> bool: _phase = Phase.AWAITING_CHOICE _candidates = [entry] return true - var stop: Dictionary = {} + var stop: Dictionary[Variant, Variant] = {} for node in nodes(): if _node_id(node) == node_id: stop = node @@ -334,7 +330,7 @@ var active_template_name := "" var active_template_index := -1 # -1 = 非池形态(旧单图)/未解析 func _resolve_template_row(row: Dictionary, template_pick: int) -> Dictionary: - var templates: Array = row.get("templates", []) + var templates: Array[Variant] = row.get("templates", []) active_template_name = "" active_template_index = -1 if templates.is_empty(): @@ -345,7 +341,7 @@ func _resolve_template_row(row: Dictionary, template_pick: int) -> Dictionary: else: var stream: RandomNumberGenerator = rng.open_stream("node_map") idx = stream.randi_range(0, templates.size() - 1) - var tpl: Dictionary = templates[idx] + var tpl: Dictionary[Variant, Variant] = templates[idx] active_template_name = String(tpl.get("display_name", "")) active_template_index = idx var out := row.duplicate(true) @@ -389,7 +385,7 @@ func _entry_node() -> Dictionary: func _out_ids(node: Dictionary) -> Array: if node.has("next"): return node.get("next", []) - var nodes: Array = nodes() + var nodes: Array[Variant] = nodes() var i := nodes.find(node) if i >= 0 and i < nodes.size() - 1: return [i + 1] @@ -498,13 +494,10 @@ func _start_battle(node: Dictionary) -> bool: "hero_armor_class": effective_ac(), "hero_ap_bonus": effective_ap_bonus(), "attack_type": _hero_attack_type, "armor_type": _hero_armor_type, # 迭代 23 T4——两根类型轴(spawn 英雄快照;空串=锻造配置默认值回退) "attunement": _attunement, - "attune_charges": _attune_charges, - "vision_counter": _vision_counter, # 迭代 22 秘宝批 T3——真视使用计数共享 dict(编排器受理成功 +1,收束入档) + "ledger": run_ledger, # 迭代 27 T3——共享四账单类下传(原四散 dict 引用键收口;裁点②甲) "feats": _feats, "tree_rules": _tree_rules, "battle_index": _battle_index(), - "bonus_ref": bonus, - "adventure_counters": _adventure_counters_ref, "voucher_rap": _voucher_rap, "voucher_time_sand": _voucher_time_sand, "voucher_insurance": _voucher_insurance, @@ -634,7 +627,7 @@ func set_attunement(ids: Array) -> void: # 特性持有集注入(迭代 12 T1——AdventureRunner._start_current_level 唯一调用方;begin_battle # enc.feats 携带进战斗求值;**共享引用不复制**——关内奖励节点兑付的 relic 须当关后续战斗生效 # (attune_charges 共享账本同款);空集=零扰动) -var _feats: Array = [] +var _feats: Array[Variant] = [] func set_feats(ids: Array) -> void: @@ -647,7 +640,7 @@ func feats_held() -> Array: # 树 rule 键集注入(迭代 12 T10——begin_battle enc.tree_rules 携带;Fork 激活侧 effects) -var _tree_rules: Array = [] +var _tree_rules: Array[Variant] = [] func set_tree_rules(rules: Array) -> void: @@ -661,14 +654,14 @@ func set_hero_axes(attack_type: String, armor_type: String) -> void: _hero_armor_type = armor_type -# adventure 域计数器共享引用注入(迭代 20 T6——AdventureRunner 冒险级账本引用下传; -# begin_battle enc.adventure_counters 携带进效果引擎 rebuild 回灌与战末 counters_export 回写—— -# **共享引用不复制**,attune_charges 同款;缺省空 dict=零扰动) -var _adventure_counters_ref: Dictionary = {} +# 共享四本账注入(迭代 27 T3——P1-4 收口:RunLedger 单类下传,替代原 bonus/attune_charges/ +# vision_counter/adventure_counters 四散引用;战斗侧只走账类原语,战末 export/开局 seed +# 时点语义不变〔裁点②甲〕) +var run_ledger: RunLedger = RunLedger.new() -func set_adventure_counters(ref: Dictionary) -> void: - _adventure_counters_ref = ref +func set_run_ledger(l: RunLedger) -> void: + run_ledger = l # 凭证件三账注入(迭代 20 T4——储备金袋 RAP/时沙 首轮 AP/完美保险 标错豁免;节点级一次性, @@ -684,10 +677,12 @@ func set_voucher_grants(rap: int, time_sand: int, insurance: int) -> void: _voucher_insurance = maxi(0, insurance) -# 同调主动件充能账本注入(迭代 11 第①批 T6——共享 dict 引用直改;能力件骑库存面注入: -# 差额注入防续玩双份——resume 路径 restore_inventory 已含断点内 attune 记录) -func set_attune_charges(charges: Dictionary) -> void: - _attune_charges = charges +# 同调主动件充能账骑库存面注入(迭代 11 第①批 T6 原语义 + 迭代 27 T3 换代——账本住 +# RunLedger;能力件骑库存面注入保留:差额注入防续玩双份——resume 路径 restore_inventory +# 已含断点内 attune 记录。原 set_attune_charges/set_vision_counter/set_adventure_counters +# 三散 setter 随单类下传承接) +func sync_attune_inventory() -> void: + var charges: Dictionary[Variant, Variant] = run_ledger.attune_charges if treasure_inventory == null: treasure_inventory = TreasureItems.new(db) for iid in charges: @@ -698,11 +693,6 @@ func set_attune_charges(charges: Dictionary) -> void: for i in range(maxi(0, int(charges[iid]) - existing)): treasure_inventory.add_item(String(iid), "attunement", Vector2i(-1, -1), 0) - -# 真视使用计数共享引用注入(迭代 22 秘宝批 T3——adventure 持有、编排器受理成功 +1) -func set_vision_counter(counter: Dictionary) -> void: - _vision_counter = counter - # 三选一 GP 袋件入账(迭代 10——钱包收口扩「奖励入账」入口;复用战斗完成同一入账原语, # wallet_changed 载荷带 source 供呈现归因) func grant_curio_gp(amount: int) -> void: @@ -781,12 +771,12 @@ func purchased_tier(product_id: String) -> int: # 购买(未知商品/属性键出域/余额不足拒绝——返回 false 零副作用): # 出账 → 加算区 += flat_value → 购血同步当前生命(封顶新上限)→ 已购档数 +1 func buy_product(product_id: String) -> bool: - var prow: Dictionary = db.shop_product_row(product_id) + var prow: Dictionary[Variant, Variant] = db.shop_product_row(product_id) if prow.is_empty(): push_error("[关卡节点地图] 商店房未知商品: %s" % product_id) return false var attr := String(prow.get("attr", "")) - if not bonus.has(attr): + if not run_ledger.bonus.has(attr): push_error("[关卡节点地图] 商品属性键出域: %s(attr=%s)" % [product_id, attr]) return false var price := current_price(prow) @@ -795,7 +785,7 @@ func buy_product(product_id: String) -> bool: return false # 余额不足(覆盖层按钮禁用为常态防线——此处兜底拒绝;2026-10-02 补静默留痕) gp -= price var flat := int(prow.get("flat_value", 0)) - bonus[attr] = int(bonus[attr]) + flat + run_ledger.bonus[attr] = int(run_ledger.bonus[attr]) + flat if attr == "max_hp": hero_hp = mini(hero_hp + flat, effective_max_hp()) # 购血:上限与当前生命同步 +Δ(封顶新上限) _shop_purchased[product_id] = purchased_tier(product_id) + 1 @@ -836,13 +826,13 @@ func _shop_open_pool() -> void: # 合法态(池空硬闸——买光即枯竭既定后果)。 func _shop_draw_shelf() -> void: _shop_shelf = [] - var sellable: Array = [] + var sellable: Array[Variant] = [] for row in db.shop_pool_rows(): var shelf_kind := String(row.get("kind_id", "")) if _pool_cap_check.is_valid() and not bool(_pool_cap_check.call(shelf_kind, "")): continue # 迭代 20 T2 案②——cap 满提前退役(货架候选视图剔除;牌库本体不动) for t in _pool_deck: - var tok: Dictionary = t as Dictionary + var tok: Dictionary[Variant, Variant] = t as Dictionary if String(tok.get("kind", "")) == shelf_kind: sellable.append({"kind": shelf_kind, "tier": String(tok.get("tier", ""))}) if sellable.is_empty(): @@ -853,7 +843,7 @@ func _shop_draw_shelf() -> void: var tmp = sellable[i] sellable[i] = sellable[j] sellable[j] = tmp - var seen: Array = [] + var seen: Array[Variant] = [] for tok in sellable: var k := String((tok as Dictionary).get("kind", "")) if seen.has(k): @@ -884,7 +874,7 @@ func pool_stock(kind: String) -> int: # A/b 表 knob provisional;核心现算,呈现层禁复算——契约 5)。relic 卡(保底占位——shop 词表外) # 基价 provisional 15 固定锚(台账披露待追认——shop.tres 词表扩列归数值批)。 func pool_price(kind: String) -> int: - var row: Dictionary = db.shop_pool_row(kind) + var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind) if row.is_empty(): if kind == "relic": return _spend_discounted(int(ceil(15.0 * pow(db.shop_price_b(), float(maxi(0, int(level_row.level_index) - 1))) * pow(db.shop_price_a(), float(_shop_visit_buys))))) @@ -902,11 +892,11 @@ func shop_shelf() -> Array: for tok in _shop_shelf: var kind := String((tok as Dictionary).get("kind", "")) var tier := String((tok as Dictionary).get("tier", "")) - var row: Dictionary = db.shop_pool_row(kind) + var row: Dictionary[Variant, Variant] = db.shop_pool_row(kind) var display := String(row.get("display_name", kind)) var value := int(db.adventure.curio_value(kind, tier)) if row.is_empty() and kind == "relic": - var rrow: Dictionary = db.feat_row(String(tok.get("feat_id", ""))) + var rrow: Dictionary[Variant, Variant] = db.feat_row(String(tok.get("feat_id", ""))) display = String(rrow.get("display_name", "特性")) value = 0 out.append({ @@ -934,7 +924,7 @@ func shop_refresh_free_left() -> int: func _attune_shop_refresh_bonus() -> int: var total := 0 for iid_v in _attunement: - var arow: Dictionary = db.treasure_item_row(String(iid_v)) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(iid_v)) if (arow.get("effect_kinds", []) as Array).has("shop_refresh_bonus"): total += int(arow.get("magnitude", 0)) return total diff --git a/game/core/meta_progression.gd b/game/core/meta_progression.gd index 449e156b..729139cc 100644 --- a/game/core/meta_progression.gd +++ b/game/core/meta_progression.gd @@ -14,18 +14,29 @@ # 参数面显式传参)。 # 纪律:数值零字面量——价格/档位/阈值/两档金额只读 db.meta_* 访问器;存档只经 SaveManager 访问器, # 禁读内部结构;行集解析禁自扫表(一律走 ConfigDB 访问器)。 -class_name MetaProgression -extends RefCounted +class_name MetaProgressionCore +extends Node var db: ConfigDb -var save: SaveManager +var save: SaveManagerCore -func _init(db_ref: ConfigDb, save_ref: SaveManager) -> void: +# 迭代 27 T1 裁点①——MetaProgression 升格 autoload(RngService 先例:类名加 Core 后缀、 +# autoload 占净名)。_init 参数可缺省兼容双形态:autoload 实例化走 .new() 零参 + _ready 从 +# autoload 链取依赖(工程序 ConfigDB→RngService→SaveManager→MetaProgression,_ready 时序 +# 已就绪);单测/工具直呼 .new(db, save) 形态不变(不入树零 _ready)。 +func _init(db_ref: ConfigDb = null, save_ref: SaveManagerCore = null) -> void: db = db_ref save = save_ref +func _ready() -> void: + if db == null: + db = get_node("/root/ConfigDB") + if save == null: + save = get_node("/root/SaveManager") + + func keystone_balance() -> int: return save.keystone_balance() @@ -58,7 +69,7 @@ func _treasure_unlock_progress(key: String) -> int: # 钥石购买解锁(营地秘宝匣唯一购买写口):仅购买件(行带 unlock_keystone_cost)可购; # 已解锁/余额不足/条件件误购 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币)。 func buy_treasure_unlock(item_id: String) -> bool: - var arow: Dictionary = db.treasure_item_row(item_id) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id) var price := int(arow.get("unlock_keystone_cost", 0)) if arow.is_empty() or price < 1 or (arow.get("unlock_requires", []) as Array).size() > 0: return false # 非购买件(条件件/无价行)拒绝 @@ -78,12 +89,12 @@ func refresh_treasure_unlocks() -> Array: var iid := String(row.get("item_id", "")) if save.item_unlocked(iid): continue - var ureqs: Array = row.get("unlock_requires", []) as Array + var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array if ureqs.is_empty(): continue # 购买件不入条件刷新面 var met := true for req_v in ureqs: - var req: Dictionary = req_v as Dictionary + var req: Dictionary[Variant, Variant] = req_v as Dictionary if _treasure_unlock_progress(String(req.get("key", ""))) < int(req.get("threshold", 999)): met = false break @@ -102,10 +113,10 @@ func treasure_shop_status() -> Array: for row in db.treasure_attunement_rows(): var iid := String(row.get("item_id", "")) var unlocked := save.item_unlocked(iid) - var ureqs: Array = row.get("unlock_requires", []) as Array - var req_view: Array = [] + var ureqs: Array[Variant] = row.get("unlock_requires", []) as Array + var req_view: Array[Variant] = [] for req_v in ureqs: - var req: Dictionary = req_v as Dictionary + var req: Dictionary[Variant, Variant] = req_v as Dictionary req_view.append({ "key": String(req.get("key", "")), "threshold": int(req.get("threshold", 0)), @@ -132,7 +143,7 @@ func treasure_shop_status() -> Array: # 路线解锁态派生(不落档——初始持有∨直购∨练满前一档;派生单点只此一处) func forge_route_unlocked(route_id: String) -> bool: - var row: Dictionary = db.forge_route(route_id) + var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty(): return false if bool(row.get("initial_owned", false)): @@ -141,7 +152,7 @@ func forge_route_unlocked(route_id: String) -> bool: return true var prereq := String(row.get("prereq_route", "")) if prereq != "": - var prow: Dictionary = db.forge_route(prereq) + var prow: Dictionary[Variant, Variant] = db.forge_route(prereq) if not prow.is_empty() and save.forge_route_level(prereq) >= int(prow.get("cap", 0)): return true return false @@ -150,7 +161,7 @@ func forge_route_unlocked(route_id: String) -> bool: # 路线购买(几何价=基础价×150%^已购级数——锚表 20/30/45/67/101;等级+1 即时写盘)。 # 未解锁/已满级/余额不足/未知路线 = 拒绝且零副作用(spend_keystone 内嵌余额闸,失败不扣币不写盘)。 func forge_buy_level(route_id: String) -> bool: - var row: Dictionary = db.forge_route(route_id) + var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty() or not forge_route_unlocked(route_id): return false var lvl := save.forge_route_level(route_id) @@ -166,7 +177,7 @@ func forge_buy_level(route_id: String) -> bool: # 一次性解锁价直购(并联双通道②)。已解锁(含练满前一档派生)= 拒绝零收费——两通道互不影响: # 未练满也可直购;直购后练满不重复收(练满通道本就是派生、零收费)。初始持有行无解锁面。 func forge_buy_unlock(route_id: String) -> bool: - var row: Dictionary = db.forge_route(route_id) + var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty() or bool(row.get("initial_owned", false)): return false if forge_route_unlocked(route_id): @@ -181,7 +192,7 @@ func forge_buy_unlock(route_id: String) -> bool: # 当前武器/甲选中切换(免费——只受理已解锁路线;写存档域+即时写盘。冒险开始锁死归参数面快照, # 契约 8;冒险中无切换入口=UI 面无路径可达本口) func forge_select_weapon(route_id: String) -> bool: - var row: Dictionary = db.forge_route(route_id) + var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(route_id): return false save.set_forge_weapon(route_id) @@ -189,7 +200,7 @@ func forge_select_weapon(route_id: String) -> bool: func forge_select_armor(route_id: String) -> bool: - var row: Dictionary = db.forge_route(route_id) + var row: Dictionary[Variant, Variant] = db.forge_route(route_id) if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(route_id): return false save.set_forge_armor(route_id) @@ -200,7 +211,7 @@ func forge_select_armor(route_id: String) -> bool: # 选中态异常(未解锁/表缺行)防御回退 0——注入面只认合法态,禁把脏选中带进冒险。 func forge_weapon_atk_bonus() -> int: var rid := save.forge_weapon() - var row: Dictionary = db.forge_route(rid) + var row: Dictionary[Variant, Variant] = db.forge_route(rid) if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid): return 0 return save.forge_route_level(rid) * int(row.get("per_level_gain", 0)) @@ -208,7 +219,7 @@ func forge_weapon_atk_bonus() -> int: func forge_armor_ac_bonus() -> int: var rid := save.forge_armor() - var row: Dictionary = db.forge_route(rid) + var row: Dictionary[Variant, Variant] = db.forge_route(rid) if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid): return 0 return save.forge_route_level(rid) * int(row.get("per_level_gain", 0)) @@ -218,7 +229,7 @@ func forge_armor_ac_bonus() -> int: # 默认值与柜台解锁状态解耦:树钥匙未点亮时本函数照常返回默认值(配置常驻可读)。 func hero_attack_type() -> String: var rid := save.forge_weapon() - var row: Dictionary = db.forge_route(rid) + var row: Dictionary[Variant, Variant] = db.forge_route(rid) if row.is_empty() or String(row.get("kind", "")) != "weapon" or not forge_route_unlocked(rid): return db.forge_default_attack_type() return String(row.get("type_axis", "")) @@ -226,7 +237,7 @@ func hero_attack_type() -> String: func hero_armor_type() -> String: var rid := save.forge_armor() - var row: Dictionary = db.forge_route(rid) + var row: Dictionary[Variant, Variant] = db.forge_route(rid) if row.is_empty() or String(row.get("kind", "")) != "armor" or not forge_route_unlocked(rid): return db.forge_default_armor_type() return String(row.get("type_axis", "")) @@ -278,7 +289,7 @@ func forge_shop_status() -> Array: func settle_adventure(summary: Dictionary) -> Dictionary: # 首次冒险布尔位置位(关卡节点图生成 v0.4——完成/失败/放弃三结局同经本收束挂点,写入只在收束) save.mark_first_adventure_done() - var drops: Array = (summary.get("keystone_drops", []) as Array).duplicate(true) + var drops: Array[Variant] = (summary.get("keystone_drops", []) as Array).duplicate(true) save.apply_adventure_summary(summary, drops) save.add_vision_used(int(summary.get("vision_used_delta", 0))) # 迭代 22 T3——真视使用计数收束聚合入档(信标网络行为条件) var new_unlocks := refresh_treasure_unlocks() # 迭代 22 T2——条件解锁刷新换代(旧工坊面退役) @@ -322,7 +333,7 @@ func tree_spent_total() -> int: # 节点可购校验(行门槛 + 显式前置 + 币 + cap;free 节点=钥匙不经购买,彩蛋=觉醒不经购买) func tree_can_buy(node_id: String) -> bool: - var row: Dictionary = db.tree_node(node_id) + var row: Dictionary[Variant, Variant] = db.tree_node(node_id) if row.is_empty(): return false # 彩蛋不经购买(觉醒唯一入口=_check_easter_egg 特化判定——V8 反例:手动购亮=红) @@ -343,7 +354,7 @@ func tree_can_buy(node_id: String) -> bool: return false # 等级阈值前置(R1 机制位——requires_level {node_id: n}:前置节点等级 ≥ n 才可购; # 2026-10-13 用户推翻裁撤案随批重立,现役表零配置=空转零行为影响) - var req_lv: Dictionary = row.get("requires_level", {}) as Dictionary + var req_lv: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary for req_id in req_lv.keys(): if save.tree_level(String(req_id)) < int(req_lv[req_id]): return false @@ -357,7 +368,7 @@ func tree_can_buy(node_id: String) -> bool: func tree_buy(node_id: String) -> bool: if node_id == "ks_refresh": return buy_attune_refresh() - var row: Dictionary = db.tree_node(node_id) + var row: Dictionary[Variant, Variant] = db.tree_node(node_id) if row.is_empty() or not tree_can_buy(node_id): return false var price := int(row.get("price", 0)) @@ -382,7 +393,7 @@ func tree_toggle_fork(group: String, side: String) -> bool: # Fork 节点购买入口(一节点双态——购=解锁开关并置默认 A 态;重复购买由 cap 经 can_buy 拦截) func tree_buy_fork(node_id: String) -> bool: - var row: Dictionary = db.tree_node(node_id) + var row: Dictionary[Variant, Variant] = db.tree_node(node_id) if row.is_empty() or String(row.get("kind", "")) != "fork": return tree_buy(node_id) if not tree_can_buy(node_id): @@ -407,15 +418,15 @@ func tree_stat_flat(key: String) -> int: func tree_rule_keys() -> Array: - var out: Array = [] - var sides: Dictionary = save.fork_sides() + var out: Array[Variant] = [] + var sides: Dictionary[Variant, Variant] = save.fork_sides() for row in db.tree_rows(): if String(row.get("kind", "")) != "fork": continue var active := String(sides.get(String(row.get("fork_group", "")), "")) if active == "": continue # 未解锁组零规则输出 - var fx: Dictionary = row.get("effects_" + active, {}) as Dictionary + var fx: Dictionary[Variant, Variant] = row.get("effects_" + active, {}) as Dictionary for k in fx.keys(): if not out.has(String(k)): out.append(String(k)) @@ -424,7 +435,7 @@ func tree_rule_keys() -> Array: # 营地树面投影(CampOverlay _rebuild_tree 数据源——行板只读;账面归本类、拼句归视图同款分工) func tree_camp_rows() -> Array: - var out: Array = [] + var out: Array[Variant] = [] for row in db.tree_rows(): if String(row.get("kind", "")) == "fork": continue # Fork 行由 fork_groups 段承载 @@ -446,19 +457,19 @@ func tree_camp_rows() -> Array: # 行板门槛文案(R1 机制位呈现——requires_level 未达标项拼「需 {前置名} Lv{n}」;达标/未配置=空串。 # 只呈现未达标项:达标后锁标消失,不做全量前置罗列) func tree_gate_text(node_id: String) -> String: - var row: Dictionary = db.tree_node(node_id) - var rl: Dictionary = row.get("requires_level", {}) as Dictionary + var row: Dictionary[Variant, Variant] = db.tree_node(node_id) + var rl: Dictionary[Variant, Variant] = row.get("requires_level", {}) as Dictionary var parts: Array[String] = [] for req_id in rl.keys(): if save.tree_level(String(req_id)) < int(rl[req_id]): - var req_row: Dictionary = db.tree_node(String(req_id)) + var req_row: Dictionary[Variant, Variant] = db.tree_node(String(req_id)) parts.append("需 %s Lv%d" % [String(req_row.get("display_name", req_id)), int(rl[req_id])]) return "、".join(parts) func tree_fork_groups() -> Array: - var out: Array = [] - var sides: Dictionary = save.fork_sides() + var out: Array[Variant] = [] + var sides: Dictionary[Variant, Variant] = save.fork_sides() for row in db.tree_rows(): if String(row.get("kind", "")) != "fork": continue @@ -485,7 +496,7 @@ func _check_easter_egg() -> void: if save.tree_level("easter_ley") > 0: return for nid in ["sp_hp", "sp_hit"]: - var row: Dictionary = db.tree_node(nid) + var row: Dictionary[Variant, Variant] = db.tree_node(nid) if row.is_empty() or save.tree_level(nid) < int(row.get("cap", 1)): return for i in range(1, 4): @@ -506,9 +517,9 @@ func _check_easter_egg() -> void: # 格 1=普通格/格 2=优秀格/格 3=稀有格递增向下兼容;槽位来源=树同调位节点点亮数,树接口接缝现役)。 # 装配关系写存档持久。 func toggle_attunement(item_id: String) -> bool: - var equipped: Array = save.attunement_equipped() + var equipped: Array[Variant] = save.attunement_equipped() if not equipped.has(item_id): - var arow: Dictionary = db.treasure_item_row(item_id) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(item_id) if arow.is_empty() or String(arow.get("form", "")) == "": return false # 非同调行(过渡/退役行)禁装配 if not save.item_unlocked(item_id): @@ -527,7 +538,7 @@ func toggle_attunement(item_id: String) -> bool: # 营地装配面投影(账面归本类、拼句归 CampOverlay——_camp_data 同款分工) func attune_camp_status() -> Dictionary: - var equipped: Array = save.attunement_equipped() + var equipped: Array[Variant] = save.attunement_equipped() var items: Array[Dictionary] = [] for row in db.treasure_attunement_rows(): var iid := String(row.get("item_id", "")) @@ -541,7 +552,7 @@ func attune_camp_status() -> Dictionary: "unlocked": save.item_unlocked(iid), }) var lvl := save.attune_refresh_level() - var next_row: Dictionary = db.meta_attune_refresh_row(lvl + 1) + var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(lvl + 1) return { "slots": Attunement.slots(db, save), "equipped": equipped, @@ -562,7 +573,7 @@ func attune_refresh_charges() -> int: # Y 升级购买(直购价 = 下一级行表值 keystone_cost;已满级/余额不足 false 零副作用——buy_forge 同款) func buy_attune_refresh() -> bool: - var next_row: Dictionary = db.meta_attune_refresh_row(save.attune_refresh_level() + 1) + var next_row: Dictionary[Variant, Variant] = db.meta_attune_refresh_row(save.attune_refresh_level() + 1) if next_row.is_empty(): return false if not save.spend_keystone(int(next_row.get("keystone_cost", 0))): diff --git a/game/core/minesweeper_rules.gd b/game/core/minesweeper_rules.gd index 475ae154..0c71da3d 100644 --- a/game/core/minesweeper_rules.gd +++ b/game/core/minesweeper_rules.gd @@ -38,10 +38,6 @@ func _init(db_ref: ConfigDb, action_economy_ref: ActionEconomy) -> void: # ---- 板态 ---- -func has_board() -> bool: - return _board != null - - # 板产物只读出口(E2 收口——战斗结算 快照 / 组合根 rebuild / 探针经此取板,禁再直读字段) func current_board() -> BoardData: return _board @@ -84,7 +80,7 @@ func clear_map_cleared() -> bool: # 移除集=未揭示、无旗、是雷——旗判定基线零回溯;雷格不入清图分母/连锁/C_dig 模拟,判定逻辑零改动) ---- func remove_mines_in(cells: Array) -> Array: - var out: Array = [] + var out: Array[Variant] = [] 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): @@ -188,11 +184,11 @@ func _install_invariant_faults(b: BoardData) -> Array: # 初始展开规范序:触发格 BFS、8 邻行主序(≡ 棋盘生成 F1 R₀ 算子;禁外扩环、禁只开 3×3) func _initial_expansion_order(b: BoardData) -> Array: - var order: Array = b.spawn_cells().duplicate() + var order: Array[Variant] = b.spawn_cells().duplicate() var seen := {} for c in order: seen[c] = true - var queue: Array = order.duplicate() + var queue: Array[Variant] = order.duplicate() while not queue.is_empty(): var c: Vector2i = queue.pop_front() if b.is_safe(c) and b.digit_of(c) == 0: @@ -240,7 +236,7 @@ func _classify(intent: Dictionary) -> int: var c: Vector2i = intent.get("cell", Vector2i(-9, -9)) if _board_face_dig(c) == "" and not _board.is_mine(c): # 模拟 Chain(O, c) 后清图判定——挖旗格同口径:本格旗视为已隐式退役(其余旗照挡) - var sim_flags: Dictionary = _flags.duplicate() + var sim_flags: Dictionary[Variant, Variant] = _flags.duplicate() sim_flags.erase(c) var chain := _chain_closure(c, sim_flags) # 与真实执行同口径——空旗集捷径会在旗挡连锁时误判 C_dig(排雷玩法:真模拟) var remaining := 0 @@ -279,7 +275,7 @@ func _do_dig(c: Vector2i) -> Dictionary: # 计算(旗格为 0 时连锁照常外扩);request_dig 被拒则旗原地保留(事务性),受理后该旗轮末不判 # (发射面基线 = 在场旗——标中/标空兑付让位于挖掘本体结局,四情况②④)。 var was_flagged := has_flag(c) - var eff_flags: Dictionary = _flags + var eff_flags: Dictionary[Variant, Variant] = _flags if was_flagged: eff_flags = _flags.duplicate() eff_flags.erase(c) @@ -297,7 +293,7 @@ func _do_dig(c: Vector2i) -> Dictionary: # 事务内执行:首格 + 连锁新格揭示(BFS 规范序,零特判);事件 = O′ ∖ O if was_flagged: _flags.erase(c) - var reveal_order: Array = _chain_reveal_order(c, eff_flags) + var reveal_order: Array[Variant] = _chain_reveal_order(c, eff_flags) for rc in reveal_order: if _revealed.has(rc): continue # 闭包可穿过已开区——已开格零事件(cell_revealed × |O′ ∖ O|) @@ -361,7 +357,7 @@ func fx_reveal(c: Vector2i) -> bool: # 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越旗 func _chain_closure(start: Vector2i, flag_set: Dictionary) -> Dictionary: var chain := {start: true} - var queue: Array = [start] + var queue: Array[Variant] = [start] while not queue.is_empty(): var c: Vector2i = queue.pop_front() if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0: @@ -374,9 +370,9 @@ func _chain_closure(start: Vector2i, flag_set: Dictionary) -> Dictionary: # 揭示规范序:首格 + 触发格 BFS、8 邻行主序 func _chain_reveal_order(start: Vector2i, flag_set: Dictionary) -> Array: - var order: Array = [start] + var order: Array[Variant] = [start] var seen := {start: true} - var queue: Array = [start] + var queue: Array[Variant] = [start] while not queue.is_empty(): var c: Vector2i = queue.pop_front() if _board.is_safe(c) and not flag_set.has(c) and _board.digit_of(c) == 0: @@ -444,7 +440,7 @@ func compute_move_path(target: Vector2i) -> Array: return [] # BFS(8 邻行主序)——路径 = 合法性载体 + HUD框架预览数据源 var prev := {_hero: _hero} - var queue: Array = [_hero] + var queue: Array[Variant] = [_hero] while not queue.is_empty(): var c: Vector2i = queue.pop_front() if c == target: @@ -478,7 +474,7 @@ func _do_stop() -> Dictionary: func _emit_flags_batch() -> void: # 判定基线 = 轮次结束事务开始时旗位;行主序枚举(确定性——排序键单点 BoardData.row_major_key, # 2026-10-02 修复批);被拔的不判;诚实重复上报(去重归 战斗结算) - var cells: Array = _flags.keys() + var cells: Array[Variant] = _flags.keys() cells.sort_custom(func(a, b): return BoardData.row_major_key(a) < BoardData.row_major_key(b)) for c in cells: flag_judged.emit({"cell": c, "hit": _board.is_mine(c)}) @@ -486,7 +482,7 @@ func _emit_flags_batch() -> void: func _emit_round_end_flags_and_payload() -> void: _emit_flags_batch() - var payload: Dictionary = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约) + var payload: Dictionary[Variant, Variant] = action_economy.last_round_end # 逐字段取自 行动经济(读取权 = 接口契约) round_end_board.emit({"path_tag": payload.get("path_tag", ""), "continuation_hint": payload.get("continuation_hint", "")}) diff --git a/game/core/reward_economy.gd b/game/core/reward_economy.gd index 60dc29db..29def278 100644 --- a/game/core/reward_economy.gd +++ b/game/core/reward_economy.gd @@ -72,8 +72,8 @@ func roll_board(eligible: Array) -> bool: for h in _fx.dispatch("reward_roll", {"polarity": "positive", "cell_count": list.size()}): if String(h.get("kind", "")) == "bias_rewards": luck_bias += int(h.get("value", 0)) - var cell_rows: Array = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰) - var clear_rows: Array = _pool_rows("clear_default") + var cell_rows: Array[Variant] = _biased_cell_rows(luck_bias) # cell 池正向行 weight>0(偏运硬币装配时稀有正向 kinds 权重 +N——池结构零触碰) + var clear_rows: Array[Variant] = _pool_rows("clear_default") # ① 身份批:每非封锁格恰一类型身份(雷安同池——同一行集同一权重分布) var draft := {} for e in list: @@ -82,10 +82,10 @@ func roll_board(eligible: Array) -> bool: var clear_rid := weighted_pick(clear_rows, stream.randi_range(0, _pool_weight(clear_rows) - 1)) # ③ 区间行 power roll(身份定稿后逐格恰一追加取号,定值行零取号) for cell in draft.keys(): - var row: Dictionary = _row_by_id(draft[cell]) + var row: Dictionary[Variant, Variant] = _row_by_id(draft[cell]) if row.has("power_min"): _power_roll[cell] = stream.randi_range(int(row.power_min), int(row.power_max)) - var clear_row: Dictionary = _row_by_id(clear_rid) + var clear_row: Dictionary[Variant, Variant] = _row_by_id(clear_rid) if clear_row.has("power_min"): _clear_power = stream.randi_range(int(clear_row.power_min), int(clear_row.power_max)) _mapping = draft @@ -114,7 +114,7 @@ func query_identity(cell: Vector2i): if rid == "": _diag("query_identity 清单外坐标(E19——未分配非错误态;封锁格不入清单)") return null - var row: Dictionary = _row_by_id(rid) + var row: Dictionary[Variant, Variant] = _row_by_id(rid) return {"reward_id": rid, "cell_type": String(row.cell_type), "tier": int(row.tier)} @@ -155,7 +155,7 @@ func grant_clear(): if _clear_granted: _diag("grant_clear 同批次重调(E18 幂等兜底)") return _clear_snapshot - var row: Dictionary = _row_by_id(_clear_identity) + var row: Dictionary[Variant, Variant] = _row_by_id(_clear_identity) _clear_snapshot = _snapshot(row, _clear_power) _clear_granted = true return _clear_snapshot @@ -206,13 +206,13 @@ const SKEW_UP_KINDS := ["attack", "blast", "pierce", "magic"] # 基座换源(首发批 2026-10-04):先经图级偏斜投影再叠件级加法——合成序锚=图级先、件级后 #(环境事实先定型、玩家构筑叠其上;两投影均为池内权重面,雷安同池与取号次数零变化) func _biased_cell_rows(bias: int) -> Array: - var base: Array = _skewed_cell_rows() + var base: Array[Variant] = _skewed_cell_rows() if bias <= 0: return base - var out: Array = [] + var out: Array[Variant] = [] for row in base: if LUCK_BIAS_KINDS.has(String(row.get("cell_type", ""))): - var biased: Dictionary = row.duplicate() + var biased: Dictionary[Variant, Variant] = row.duplicate() biased["weight"] = int(row.weight) + bias out.append(biased) else: @@ -226,9 +226,9 @@ func _biased_cell_rows(bias: int) -> Array: func _skewed_cell_rows() -> Array: if board_skew <= 0: return _rollable_rows() - var base: Array = _rollable_rows() - var down: Array = [] - var up: Array = [] + var base: Array[Variant] = _rollable_rows() + var down: Array[Variant] = [] + var up: Array[Variant] = [] for row in base: var k := String(row.get("cell_type", "")) if SKEW_DOWN_KINDS.has(k): @@ -244,7 +244,7 @@ func _skewed_cell_rows() -> Array: var gains := _largest_remainder_split(up, drops_total(drops), _pool_weight(up), drops_total(drops)) var di := 0 var ui := 0 - var out: Array = [] + var out: Array[Variant] = [] for row in base: var k := String(row.get("cell_type", "")) var w := int(row.weight) @@ -255,7 +255,7 @@ func _skewed_cell_rows() -> Array: w += gains[ui] ui += 1 assert(w > 0, "图级偏斜投影产生非正权重(池结构变更须走另批重审)") - var skewed: Dictionary = row.duplicate() + var skewed: Dictionary[Variant, Variant] = row.duplicate() skewed["weight"] = w out.append(skewed) assert(_pool_weight(out) == _pool_weight(base), "图级偏斜守恒破(偏斜后合计须等于原池)") diff --git a/game/core/run_ledger.gd b/game/core/run_ledger.gd new file mode 100644 index 00000000..8a0a3789 --- /dev/null +++ b/game/core/run_ledger.gd @@ -0,0 +1,51 @@ +class_name RunLedger +extends RefCounted +# 共享四本账显式承载(迭代 27 T3——审计 P1-4「共享可变字典跨三层:所有权名义单点、实际三处 +# 可写」收口;裁点②甲 = 单类持四账)。 +# 所有权:AdventureRunner 持有(开局构造、冒险全程唯一宿主);LevelRunner 经 set_run_ledger +# 下传并装配 encounter;战斗侧(BattleOrchestrator)经 encounter["ledger"] 拿本类引用, +# 只走下方 read/append 原语——战末 export/开局 seed 时点语义不变(确定性设计原样保留, +# 只把「函数签名上不可见的共享可变引用」换成签名可见的显式类)。 +var attune_charges: Dictionary[Variant, Variant] = {} # 同调主动件充能账(迭代 11 第①批 T6——use_treasure 扣减;断点随局回档) +var vision_counter: Dictionary[Variant, Variant] = {} # 真视类使用计数(迭代 22 秘宝批 T3——冒险开始置 {"used":0}; + # 缺省空 = 调试/单场战斗不计——原 enc 缺键守卫语义) +var adventure_counters: Dictionary[Variant, Variant] = {} # adventure 域效果计数器(迭代 20 T6——rebuild 回灌/战末导出回写目标) +var bonus: Dictionary[Variant, Variant] = {} # 统一冒险加算区(冒险开始置四轴零账——商店购买/三选一属性件/战末 bonus_delta 汇入) + + +# ---- 战斗侧原语(裁点②甲:战斗侧只拿 read/append 两族;宿主侧冒险/关卡域直读字段域) ---- + +func attune_charge_has(item_id: String) -> bool: + return attune_charges.has(item_id) + + +func attune_charge_set(item_id: String, value: int) -> void: + attune_charges[item_id] = value + + +func attune_charge_consume(item_id: String) -> void: + # 原编排器 use_treasure 扣减语义逐字(has 闸 + maxi(0,·) 下限) + if attune_charges.has(item_id): + attune_charges[item_id] = maxi(0, int(attune_charges[item_id]) - 1) + + +func vision_count_add(n: int) -> void: + vision_counter["used"] = int(vision_counter.get("used", 0)) + n + + +func counters_seed_snapshot() -> Dictionary: + return adventure_counters # 回灌读面(时点 = begin_battle rebuild 后——语义不变) + + +func counters_merge_export(exported: Dictionary) -> void: + # 战末回写(原编排器逐字语义:仅非空账参与、逐键覆写) + if adventure_counters.is_empty(): + return + for cid in exported.keys(): + adventure_counters[cid] = int(exported[cid]) + + +func bonus_add_axis(axis: String, delta: int) -> void: + # 原编排器战末汇入逐字语义(has 闸——账外轴丢弃) + if bonus.has(axis): + bonus[axis] = int(bonus.get(axis, 0)) + delta diff --git a/game/core/run_ledger.gd.uid b/game/core/run_ledger.gd.uid new file mode 100644 index 00000000..e1e24a62 --- /dev/null +++ b/game/core/run_ledger.gd.uid @@ -0,0 +1 @@ +uid://7afr7e1gtrj5 diff --git a/game/core/save_manager.gd b/game/core/save_manager.gd index 968b4a39..4a56eb8f 100644 --- a/game/core/save_manager.gd +++ b/game/core/save_manager.gd @@ -47,8 +47,8 @@ # # JSON 键纪律:字典键一律 String——JSON 键非字符串的坑,int 键(关卡序号)读写两侧都要 str() 换算, # 混用必出静默 miss。 -class_name SaveManager -extends RefCounted +class_name SaveManagerCore +extends Node const SAVE_FORMAT_VERSION := 7 # registry schema_version:v7=锻造工坊域(迭代 23 T5——meta 新域 forge_routes〔六路线等级 levels+直购解锁清单 unlocked+当前武器/甲选中标记 weapon/armor;无特效域——裁 6 满级特效本期不做〕,旧档缺键由 _normalize 按默认补齐=初始持有态〔sword/light 在档余四锁、选中=初始路线〕,v6→v7 空迁移=缺键补齐模式——v3→v4 先例);v6=生涯终身计数域(迭代 18 卡④——meta 两新域 adventure_total〔冒险总局数,开局即计含放弃与失败〕/adventure_cleared〔通关局数,三关全通才计 1——判定字段 summary.cleared〕,旧档缺键由 _normalize 按默认补零,收束自增唯一入账=apply_adventure_summary);v5=天赋树域(迭代 12 T9——meta 三新域 tree_nodes〔node_id→已购等级〕/fork_sides〔fork_group→a|b〕/migration_note〔三体系迁移补偿注记〕——M-4 迭代 12 批次二;需求卡片「v3→v4」系同调批未落时行文,v4 已被同调批占用顺延 v5 披露);v4=同调批扩域(meta 三新域 boss_kill_count/attunement_equipped/attune_refresh_level——M-4 迭代 11 第①批 T2;断点充能余量随 T6 接线,旧档缺键 = 合法空态);v3=断点附随件扩列+摘要 keystone_drops(M-3 迭代 10 奖励重构);v2=断点域出生(M-3 迭代 8 卡①);v1=首版;高版本拒读 owner=存档序列化 const SUMMARY_CAP := 10 # 最近冒险摘要保留条数(最新在前) @@ -74,6 +74,22 @@ func _init(path: String = "user://save.json") -> void: register_migration(6, _migrate_v6_v7) +# autoload 生命周期(迭代 27 T1 裁点①——SaveManager 升格跨场景持有者,RngService 先例: +# 类名加 Core 后缀、autoload 占净名;读盘挂 _ready=autoload 先于主场景就绪,时序与旧 +# main._ready 读盘等价)。独立 .new() 实例(单测/工具)不入树零 _ready,读盘仍由调用方显式触发。 +func _ready() -> void: + load_from_disk() + + +# 原地重置(迭代 27 T1——探针隔离面,沿 RngService.set_master_seed 单例原地重置先例: +# 换路径+回默认数据+重读盘;对象引用恒定,MetaProgression 持有的 save 引用零失效) +func attach_path(path: String) -> void: + _path = path + _read_only = false + _data = _default_data() + load_from_disk() + + # v1→v2:断点域出生(旧档无断点 = 合法空态——「继续上局」隐藏,非损坏非债务) func _migrate_v1_v2(data: Dictionary) -> Dictionary: if not data.has("breakpoint"): @@ -102,7 +118,7 @@ func _migrate_v3_v4(data: Dictionary) -> Dictionary: # 补齐;迭代 21 T5 换代:原「三体系迁移」映射体随 migrate_tree 一并删除——开发阶段破坏性废弃 # 〔Q3 用户裁〕,换代前结构由 _normalize 的 tree_struct 标记检测废弃重置,本函数只剩缺键补齐)。 func _migrate_v4_v5(data: Dictionary) -> Dictionary: - var meta: Dictionary = data.get("meta", {}) as Dictionary + var meta: Dictionary[Variant, Variant] = data.get("meta", {}) as Dictionary if not meta.has("tree_nodes"): meta["tree_nodes"] = {} if not meta.has("fork_sides"): @@ -314,7 +330,7 @@ func tree_level(node_id: String) -> int: func set_tree_level(node_id: String, level: int) -> void: - var tn: Dictionary = _data["meta"].get("tree_nodes", {}) as Dictionary + var tn: Dictionary[Variant, Variant] = _data["meta"].get("tree_nodes", {}) as Dictionary tn[node_id] = maxi(0, level) _data["meta"]["tree_nodes"] = tn @@ -328,7 +344,7 @@ func fork_side(group: String) -> String: func set_fork_side(group: String, side: String) -> void: - var fs: Dictionary = _data["meta"].get("fork_sides", {}) as Dictionary + var fs: Dictionary[Variant, Variant] = _data["meta"].get("fork_sides", {}) as Dictionary fs[group] = side _data["meta"]["fork_sides"] = fs @@ -347,13 +363,13 @@ func forge_routes_snapshot() -> Dictionary: func forge_route_level(route_id: String) -> int: - var lv: Dictionary = _forge_domain().get("levels", {}) as Dictionary + var lv: Dictionary[Variant, Variant] = _forge_domain().get("levels", {}) as Dictionary return int(lv.get(route_id, 0)) func set_forge_route_level(route_id: String, level: int) -> void: - var dom: Dictionary = _forge_domain() - var lv: Dictionary = dom.get("levels", {}) as Dictionary + var dom: Dictionary[Variant, Variant] = _forge_domain() + var lv: Dictionary[Variant, Variant] = dom.get("levels", {}) as Dictionary lv[route_id] = maxi(0, level) dom["levels"] = lv _data["meta"]["forge_routes"] = dom @@ -364,8 +380,8 @@ func forge_unlocked_purchases() -> Array: func add_forge_unlock(route_id: String) -> void: - var dom: Dictionary = _forge_domain() - var un: Array = dom.get("unlocked", []) as Array + var dom: Dictionary[Variant, Variant] = _forge_domain() + var un: Array[Variant] = dom.get("unlocked", []) as Array if not un.has(route_id): un.append(route_id) dom["unlocked"] = un @@ -377,7 +393,7 @@ func forge_weapon() -> String: func set_forge_weapon(route_id: String) -> void: - var dom: Dictionary = _forge_domain() + var dom: Dictionary[Variant, Variant] = _forge_domain() dom["weapon"] = route_id _data["meta"]["forge_routes"] = dom @@ -387,7 +403,7 @@ func forge_armor() -> String: func set_forge_armor(route_id: String) -> void: - var dom: Dictionary = _forge_domain() + var dom: Dictionary[Variant, Variant] = _forge_domain() dom["armor"] = route_id _data["meta"]["forge_routes"] = dom @@ -399,7 +415,7 @@ func attunement_equipped() -> Array: func set_attunement_equipped(item_ids: Array) -> void: - var out: Array = [] + var out: Array[Variant] = [] for v in item_ids: out.append(String(v)) _data["meta"]["attunement_equipped"] = out @@ -416,14 +432,6 @@ func set_attune_refresh_level(n: int) -> void: # 同调位计数源组装(Attunement 服务取数口——clear_count = 逐关通关次数合计〔含重复关,底稿 §2.1 口径〕; # clear_count(level_index) 逐关访问器零改动复用) -func attunement_counters() -> Dictionary: - var total := 0 - var counts: Dictionary = _data["meta"]["level_clear_counts"] - for k in counts.keys(): - total += int(counts[k]) - return {"clear_count": total, "boss_kill_count": boss_kill_count()} - - func recent_summaries() -> Array: return _data["meta"]["recent_summaries"].duplicate(true) # 值拷贝出户(本类契约「禁碰内部结构」——消费方直改不得渗入存档内存态) @@ -446,9 +454,9 @@ func apply_adventure_summary(summary: Dictionary, boss_drops: Array = []) -> voi _data["meta"]["level_clear_counts"][key] = int(_data["meta"]["level_clear_counts"].get(key, 0)) + 1 _data["meta"]["boss_first_kill"][key] = true _data["meta"]["boss_kill_count"] = boss_kill_count() + cleared # 迭代 11 T2——Boss 恒唯一终点:levels_cleared 即本次冒险 Boss 击杀数(含重复) - var entry: Dictionary = summary.duplicate(true) + var entry: Dictionary[Variant, Variant] = summary.duplicate(true) entry["boss_drops"] = boss_drops.duplicate(true) - var arr: Array = _data["meta"]["recent_summaries"] + var arr: Array[Variant] = _data["meta"]["recent_summaries"] arr.push_front(entry) while arr.size() > SUMMARY_CAP: arr.pop_back() @@ -493,10 +501,10 @@ func _corrupt_fallback(reason: String) -> bool: # tree_nodes/fork_sides 清空、钥石余额等他域零触碰(V16 断言面)。 func _normalize(data: Dictionary) -> Dictionary[String, Variant]: var merged := _default_data() - var def_meta: Dictionary = merged["meta"] # 补齐源引用(先于 data 覆盖取出——data 自带 meta 时 merged["meta"] 被换走,此引用仍指默认值) + var def_meta: Dictionary[Variant, Variant] = merged["meta"] # 补齐源引用(先于 data 覆盖取出——data 自带 meta 时 merged["meta"] 被换走,此引用仍指默认值) for key in data: merged[key] = data[key] - var meta: Dictionary = merged.get("meta", {}) + var meta: Dictionary[Variant, Variant] = merged.get("meta", {}) if int(meta.get("tree_struct", 0)) < TREE_STRUCT_VERSION: meta["tree_nodes"] = {} meta["fork_sides"] = {} diff --git a/game/core/settlement/aura_segment.gd b/game/core/settlement/aura_segment.gd new file mode 100644 index 00000000..add4aca7 --- /dev/null +++ b/game/core/settlement/aura_segment.gd @@ -0,0 +1,16 @@ +class_name AuraSegment +extends RefCounted +# ② 增益/减益段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:续存实例组合行(本结算新授予的已在授予段逐 grant 出行——增益状态 义务⑦)。 +# 行发射归宿主编排器(oc)——零算法改动(🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + oc._emit_composition_effects("hero", ri) + oc._emit_composition_effects("enemy", ri) diff --git a/game/core/settlement/aura_segment.gd.uid b/game/core/settlement/aura_segment.gd.uid new file mode 100644 index 00000000..05197d02 --- /dev/null +++ b/game/core/settlement/aura_segment.gd.uid @@ -0,0 +1 @@ +uid://buwfoafhp2wvk diff --git a/game/core/settlement/combat_segment.gd b/game/core/settlement/combat_segment.gd new file mode 100644 index 00000000..ff80ef2d --- /dev/null +++ b/game/core/settlement/combat_segment.gd @@ -0,0 +1,101 @@ +class_name CombatSegment +extends RefCounted +# ③ 战斗段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak/连珠矢段数)+ 蓄力层注入 + 主角先手 +# (含连珠矢分段开单)+ 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。 +# 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C 2026-10-06 用户裁〕;hero hp=0 短路。 +# 行发射/开单/兑付原语仍归宿主编排器(oc)——本函数体为原编排器 _combat_segment 逐字搬运 +# (🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + oc._defer_board_damage = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账) + # 本轮扫雷事实推导(迭代 12 T4——执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数= + # streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后 + # 同一次 _settle 内,facts 已定。 + var round_safe := 0 + var round_mine := false + for evt in oc._facts_reveals: + if evt.kind == "safe": + round_safe += 1 + elif evt.kind == "mine": + round_mine = true + oc._damage.dig_streak = 0 if round_mine else round_safe + # 蓄力层注入(内容数据驱动扩展批 T3——整备军备「每层下轮首段 +1」/厚积薄发满层 spike/铁壁整备 + # 减伤——三件 fact charge_layers 同读 action_economy;battle 域每轮刷新) + oc._damage.charge_layers = oc.action_economy.charge_layers + # ⓪ 主角先手攻击(基础攻击恒存在——2026-09-11 用户裁决;先于敌方行动。案C 后授予段不再产出伤害, + # 主角进入战斗段时恒存活——先手必兑,除非场上无敌)。 + # 单分量伤害构成(2026-09-19 充能退役批·用户拍板):主角攻击 = 唯一基础分量(atk,吃掷点含暴击; + # 类型 = 主角 attack_type 表值——2026-09-18 营地扩展前置批:量与类型抽象分离;查甲效矩阵对应行取克制修正)。 + # 原"基础 + 穿刺/魔法充能三分量"模型随充能系统整体退役——克制策略归冒险前配置(营地换型 / 未来冒险前参数面), + # 局内不再有挖掘攒弹药的第二伤害供给。attack_up 增益经 hero_offense 池乘攻击合计(见池乘区注记)。 + # 护甲等级掷点批(2026-09-16):基础分量吃掷点判定(含暴击);棋盘来源伤害不掷不折; + # 任何伤害落地 ≥ damage_floor(全局 max(1,·)——2026-09-20 用户拍板扩全来源,下限在控制器管线单点收口)。 + var hero_alive: bool = int(oc._enemy_system.hero_unit().get("hp", 0)) > 0 + var hero_target = oc._enemy_system.default_target() + if hero_alive and hero_target != null: + var ht := oc._damage.open_ticket(DamageController.KIND_HERO_ATTACK, "hero_attack", "主角攻击", "combat", oc._hero_atk, \ + DamageController.TARGET_FRONT_ALIVE, ri, Vector2i(-1, -1), String(oc._enemy_system.hero_unit().get("attack_type", "normal"))) + oc._damage.submit(ht) + # 连珠矢(内容数据驱动扩展批 T3——attack_commit 订阅施加面:开格节奏变攻击段数,本轮每开 + # ~3 安全格 +1 段穿刺附加(上限 cap;段值 provisional 固定值不乘 atk——台账披露)。施加位 + # 紧随主角攻击单开立=迁移前开单位(先于敌方行动单入队——flush 兑现序逐位一致) + for h in oc._hits_of(ht.context.get("fx_attack_hits", []) as Array, "attack_segments"): + var segments := mini(int(h.get("cap", 2)), oc._damage.dig_streak / maxi(1, int(h.get("divisor", 3)))) + for s in segments: + # 迭代 20 T9——source_class 中性名(旧件名 feat_chain_arrows 随连珠矢退役;attack_segments 兑付通道保留泛用面) + var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", + "连击分段", "board", int(h.get("value", 0)), DamageController.TARGET_FRONT_ALIVE, ri, + oc.minesweeper.hero_pos(), "pierce") + oc._damage.submit(ct) + # 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。 + # 死亡即不动手:摇定后、兑现前死亡的敌不在存活序中——本拍技能作废,不 commit、不进冷却。 + for u in oc._enemy_system.query_alive("enemy"): + if int(oc._enemy_system.hero_unit().hp) <= 0: + break # hero hp=0 立即短路(死尸挨打行是归因噪音——用户拍板) + var skill_id := oc._enemy_system.pending_action(u) + var srow: Dictionary[Variant, Variant] = oc.db.skill_row(skill_id) + if srow.is_empty(): + oc._fatal("敌实例待发动作悬空: %s" % skill_id) + continue + if String(srow.type) == "buff" or String(srow.type) == "debuff": + # 增益/妨害技(新效应词汇批 2026-09-19)= 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。 + # (原"本轮喂的伏击逢施放拍不兑现"读怪奖励随伏击退役一并作废——2026-09-20 八型批) + # 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。 + var dur: int = int(srow.get("buff_duration", 1)) + oc._status_effects.apply_effect(String(srow.buff_ref), {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur) + oc._emit_row("cast", "enemy_action", Vector2i(-1, -1), ri, "%s 施放 %s" % [u.display_name, String(srow.display_name)], 0, {"skill_id": skill_id, "effect_id": String(srow.buff_ref), "duration": dur}) + oc._enemy_system.commit_action(u) + continue + # 伤害技(含普攻——倍率恒 1)= 敌方伤害技单据:基线 = round(atk×倍率)、rider 预计算归开单侧 + # (表值算术零随机),掷点/克制查表(第 9 参 = 敌方 attack_type——迭代 24 N3 轴开列前恒空串不查表)/ + # hero_defense×enemy_output 池乘区/下限/rider 后置/commit 全归控制器管线(兑现序 = 开单序; + # 来源敌已死在兑现时作废——死亡即不动手)。 + var baseline: int = int(round(float(u.atk) * float(srow.get("damage_mult", 1.0)))) + var rider := 0 + if srow.has("rider_mult"): + rider = maxi(int(oc.db.settlement.damage_floor), int(floor(float(u.atk) * float(srow.rider_mult)))) + var et := oc._damage.open_ticket(DamageController.KIND_ENEMY_SKILL, "enemy_action", "%s %s" % [u.display_name, String(srow.display_name)], "combat", \ + baseline, DamageController.TARGET_HERO, ri, Vector2i(-1, -1), String(u.get("attack_type", "normal")), rider) + et["source_unit"] = u + et["context"] = {"skill_id": skill_id, "baseline": baseline} + oc._damage.submit(et) + # 地图伤害兑付(案C 2026-10-06 用户裁——战斗段末:怪物还手后按授予段事件序补提交统一结账; + # 主角死于怪物 → 地图账不兑,逐单留「未生效」行不作废静默——没生效也要留痕口径; + # 补提交后若地图直伤反杀主角,控制器 _halted 作废余单——战斗已败,胜负归判定段) + if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: + for t in oc._deferred_board: + oc._emit_row("reward_ineffective", String(t.get("source_class", "")), t.get("origin_cell", Vector2i(-1, -1)), ri, + "%s 未生效(主角已倒下)" % String(t.get("display", "")), 0, {"forfeit": "hero_down"}, "") + else: + for t in oc._deferred_board: + oc._damage.submit(t) + oc._deferred_board.clear() + oc._damage.flush() # 战斗段界结账(B 模式——主角单+敌方单+地图单按开单序统一兑现;A 模式地图单补提交时即结) diff --git a/game/core/settlement/combat_segment.gd.uid b/game/core/settlement/combat_segment.gd.uid new file mode 100644 index 00000000..b37f260a --- /dev/null +++ b/game/core/settlement/combat_segment.gd.uid @@ -0,0 +1 @@ +uid://cmr602bpitrxd diff --git a/game/core/settlement/decay_segment.gd b/game/core/settlement/decay_segment.gd new file mode 100644 index 00000000..c54e13d2 --- /dev/null +++ b/game/core/settlement/decay_segment.gd @@ -0,0 +1,16 @@ +class_name DecaySegment +extends RefCounted +# ④ 衰减段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:状态衰减恰一次恒执行含末轮——先兑付后衰减。行发射归宿主编排器(oc)—— +# 零算法改动(🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + var k: int = oc._status_effects.decay_round() + oc._emit_row("decay", "", Vector2i(-1, -1), ri, "状态衰减", k, {}, "") diff --git a/game/core/settlement/decay_segment.gd.uid b/game/core/settlement/decay_segment.gd.uid new file mode 100644 index 00000000..8cc85091 --- /dev/null +++ b/game/core/settlement/decay_segment.gd.uid @@ -0,0 +1 @@ +uid://c4oht2fyigigk diff --git a/game/core/settlement/grant_segment.gd b/game/core/settlement/grant_segment.gd new file mode 100644 index 00000000..c5cfda5a --- /dev/null +++ b/game/core/settlement/grant_segment.gd @@ -0,0 +1,333 @@ +class_name GrantSegment +extends RefCounted +# ① 授予段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付 + 旗判/军规开单 + round_end 施加 + +# 转存收口 + 揭示兑付 + 段界结账。行发射/开单/兑付原语仍归宿主编排器(oc)—— +# 本段只搬运时序,零算法改动(🔴 零行为变更契约:同种子完整局输出逐位一致)。 +# kind 分派单点化(P1-1 同批):counter_add 五位三行组 / grant_material 三位兑付块 +# 收 _apply_counter_hit / _apply_material_hit 两助手(原 :683/:742/:777/:839/:1193 与 +# :690/:782/:1217 八处——分支本体逐字保留在助手内)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主(状态/行发射/开单单点仍归其身——段只搬运时序) + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> void: + # ① 授予段:结算队列(开格/旗判定/清图)→ 极性路由逐批兑付(伤害形态单据暂存——见 _submit 案C 闸)。 + # 判定基线 = 轮末在场旗(发射面已过滤——被挖开的旗已随挖掘擦除零兑付,插旗不可逆批 2026-09-19; + # 开格事实不可撤)。 + oc._defer_board_damage = true # 案C:授予段伤害单据只暂存不结账(2026-10-06 用户裁——兑付时点=战斗段末) + for evt in oc._facts_reveals: + if not bool(evt.get("resource", false)): + continue # 封锁格(initial 通道)恒无资源——不入账(战斗结算 义务②) + var snap = evt.get("reward_snapshot") + if not (snap is Dictionary): + continue + if evt.kind == "safe": + if bool(evt.get("dug_under_flag", false)): + # 插旗不可逆四情况④(2026-09-19 用户拍板):插安全格再挖开 = 零惩罚零奖励——正向兑付 + # 作废(归因行在案);标空判定已随旗擦除自然缺席(被挖的旗不在轮末在场基线)。 + oc._emit_row("reward_ineffective", "safe_cell", evt.cell, ri, "奖励作废(插旗格直接挖开——零惩罚零奖励)", 0, {}, "") + continue + # dig_reveal 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: + # 乘区收集 → 正向兑付 → 计数器 → 追加产出(深挖)→ 奥术矢。 + # 迭代 20 T1——payload 扩三事实:is_edge_cell〔边陲矿工〕/is_blank_zero〔跳格采掘〕/ + # new_resource_type〔地质学家——快照 cell_type 首见判定,见后集登记〕) + var board_b: BoardData = oc.minesweeper.current_board() + var snap_pre = evt.get("reward_snapshot") + var ctype := String((snap_pre as Dictionary).get("cell_type", "")) if snap_pre is Dictionary else "" + var is_new_type := ctype != "" and not oc._fx_new_resource_types.has(ctype) + if is_new_type: + oc._fx_new_resource_types[ctype] = true + var dr_hits := oc._fx_emit("dig_reveal", {"cell": evt.cell, "round": ri, + "is_first_dig_of_battle": not oc._feat_first_dig_used, + "terrain_kind": oc._harmful_variants(evt.get("variant_ids", []) as Array), + "is_edge_cell": evt.cell.x == 0 or evt.cell.y == 0 + or evt.cell.x == board_b.width - 1 or evt.cell.y == board_b.height - 1, + "is_blank_zero": board_b.digit_of(evt.cell) == 0, + "new_resource_type": is_new_type}) + var pos_m := 0 + var neg_m := 0 + var combo_cap := -1 + for h in dr_hits: + if String(h.get("kind", "")) != "yield_mult": + continue + pos_m += int(h.get("mult", 0)) + neg_m += int(h.get("cost_mult", 0)) + if int(h.get("cap", 0)) > 0: + combo_cap = int(h.cap) # 矿脉直觉连击上限 + oc._grant_routed(oc._feat_yield_adjust(snap, true, pos_m, neg_m, combo_cap), "positive", "safe_cell", evt.cell, ri, "") + # 计数器执行(连击计数——加法族,乘区收集后执行=本格兑付用旧值,与迁移前逐位一致) + for h in dr_hits: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + # 揭示效果入队(迭代 20 T1——地质学家/跳格采掘 reveal_tiles 订阅面;授予段末 + # _flush_pending_reveals 统一掷格兑付) + if not oc._hits_of(dr_hits, "reveal_tiles").is_empty(): + oc._fx_enqueue_reveal() + # 追加产出(深挖钻头 v2——本格双份制:grant_material 命中=配额内追加第二份;标签随行) + for h in dr_hits: + if String(h.get("kind", "")) == "grant_material": + _apply_material_hit(h, evt.cell, ri) + # 奥术矢(魔能共鸣——每战首次开安全格追加魔法矢;任意真族单次生效) + var spell_hits: Array[Variant] = oc._hits_of(dr_hits, "spawn_spell") + if not spell_hits.is_empty() and not oc._feat_first_dig_used: + oc._feat_first_dig_used = true + var spell_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_first_dig_spell", + "奥术矢", "board", int((spell_hits[0] as Dictionary).get("value", 0)), + DamageController.TARGET_FRONT_ALIVE, ri, evt.cell, "magic") + oc._submit(spell_t, false) + elif evt.kind == "mine": + # 插旗不可逆四情况②:插雷格再挖开 = 只受挖雷一层惩罚(mine_hit 照常兑付——惊动巢穴), + # 标中不发(旗已随挖掘擦除、不入轮末判定基线)。 + # mine_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价: + # 减半收集 → 负向兑付〔带减半系数〕→ 反打开单 → 连击清零计数器。 + # 迭代 20 T1——发射前移+payload 扩 dug_under_flag〔破封一击〕;新增施加面: + # damage_reduce〔安全帽耗层减伤——value_counter 层值,一次清零〕/ heal〔血蜜〕/ + # blast_scaled〔陷阱爆发——雷伤×倍率全体敌〕/ bonus_delta〔血税〕/ reveal_tiles〔见血开窍〕) + var mine_power := int((snap as Dictionary).get("power", 0)) + var mh_hits := oc._fx_emit("mine_hit", {"cell": evt.cell, "round": ri, + "damage": mine_power, "dug_under_flag": bool(evt.get("dug_under_flag", false)), + "hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))}) + var halve_pct := 0 + var mine_reduce := 0 + for h in mh_hits: + match String(h.get("kind", "")): + "damage_mult": + halve_pct += int(h.get("value", 0)) + "damage_reduce": + if h.has("value_counter") and String(h.get("value_counter", "")) != "": + mine_reduce += oc._fx.counter(String(h.get("value_counter", ""))) # 安全帽——层值减免 + else: + mine_reduce += int(h.get("value", 0)) # 背水——定值减免 + "heal": + var hu_heal: Dictionary[Variant, Variant] = oc._enemy_system.hero_unit() + hu_heal["hp"] = mini(int(hu_heal.get("hp", 0)) + int(h.get("value", 0)), maxi(1, int(hu_heal.get("max_hp", 1)))) + "bonus_delta": + var axis := String(h.get("axis", "")) + if oc._fx_bonus_delta.has(axis): + oc._fx_bonus_delta[axis] += int(h.get("delta", 0)) + "blast_scaled": + var blast_v := maxi(int(oc.db.settlement.damage_floor), int(float(mine_power) * float(h.get("mult", 1)))) + var bs_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_blast_scaled", "陷阱爆发", "board", + blast_v, DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) + oc._submit(bs_t, false) + if mine_reduce > 0: + snap["power"] = maxi(0, mine_power - mine_reduce) # 减伤落快照(负向兑付按减后值——安全帽每层减 1) + for h in mh_hits: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + if not oc._hits_of(mh_hits, "reveal_tiles").is_empty(): + oc._fx_enqueue_reveal() # 见血开窍——每颗雷一次揭示(授予段末统一兑付) + oc._grant_routed(oc._feat_yield_adjust(snap, false, 0, 0, -1), "negative", "mine_hit", evt.cell, ri, "", "", halve_pct) + for h in mh_hits: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "reflect": + var nct := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "attune_nest_counter", "惊巢反制·反弹", "board", + int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, evt.cell, "", 0, "", {}) + oc._submit(nct, false) + # 地形变异 兑付段(授予段尾——触发枚举 on_open:挖开即伤、chain 通道同口径;伤害单据案C 暂存到 + # 战斗段末兑付〔2026-10-06 用户裁〕;信息型 hide_number 不进结算——呈现层读取,见 board_view 数字遮蔽; + # 雷格恒无变异——布置段排除,双层叠罚不存在;插旗格直挖照吃变异伤害——地形后果同 mine_hit 口径, + # 非奖励不作废;吞噬流资源兑付〔荆棘编织/地脉补偿〕中伤害形态行同样经案C闸暂存,非伤害形态照常当场) + for evt in oc._facts_reveals: + var vids: Array[Variant] = evt.get("variant_ids", []) + for vid in vids: + var vrow: Dictionary[Variant, Variant] = oc.db.variation.row_by_id(String(vid)) + if String(vrow.get("effect", "")) != "flat_damage": + continue # 触发词表按 effect 路由:flat_damage=on_open 兑现;hide_number/on_open 外组合 = 数据行新词表未接线(显式忽略面) + # 地形变异伤害单据(伤害管线单据化批 2026-09-24)——无类型必中,下限保底在控制器管线内 + # 迭代 20 T1——damage_taken 发射前移(单据开立前):damage_reduce〔背水——hp_ratio when + # 面〕先收集再按减后值开单;counter_add〔保险丝/石肤〕+ ac_bonus〔石肤〕照常施加; + # grant_material〔荆棘/地脉〕移到单据提交后(与迁移前兑付值序一致——收益不因减伤变化) + var dt_amount := int(vrow.get("effect_value", 0)) + var dt_hits := oc._fx_emit("damage_taken", {"kind": "terrain", "amount": dt_amount, + "source": String(vid), "cell": evt.cell, "round": ri, "terrain_kind": String(vid), + "hp_ratio": int(round(100.0 * float(int(oc._enemy_system.hero_unit().get("hp", 0))) / float(maxi(1, int(oc._enemy_system.hero_unit().get("max_hp", 1))))))}) + for h in dt_hits: + match String(h.get("kind", "")): + "damage_reduce": + dt_amount = maxi(0, dt_amount - int(h.get("value", 0))) + "ac_bonus": + oc._apply_ac_bonus(h) + "counter_add": + _apply_counter_hit(h) + var vt := oc._damage.open_ticket(DamageController.KIND_BOARD_FLAT_HERO, "地形变异", String(vrow.get("display_name", vid)), "board", \ + dt_amount, DamageController.TARGET_HERO, ri, evt.cell, "", 0, "", {"variant_id": String(vid)}) + oc._submit(vt, false) + for h in dt_hits: + if String(h.get("kind", "")) == "grant_material": + _apply_material_hit(h, evt.cell, ri) + for evt in oc._facts_flags: + _judge_flag(evt, ri) + # 战旗军规开单段(迭代 12 T3 标旗流——flag_judged 批后统一开单:轮末每面标中旗对最前存活敌 + # 结算 board_typed 必中伤害〔复用零新配方;「旗所在巢穴敌」简化为 default_target 最前存活敌—— + # 台账披露待追认〕;完美布防=本轮旗批全中(hit>0 且 miss==0)时旗伤 ×mult——全对推理奖励)。 + # 伤害单据案C 暂存战斗段末兑付。 + # flag_settle 发射+施加(内容数据驱动扩展批 T3——战旗 open_ticket + 完美布防 modify_ticket + # 改写窗口:旗伤基值×完美倍率后逐旗开单,与迁移前算序一致) + var fs_hits := 0 + var fs_misses := 0 + for fevt in oc._facts_flags: + if bool(fevt.get("hit", false)): + fs_hits += 1 + else: + fs_misses += 1 + var fs_fx := oc._fx_emit("flag_settle", {"flags_hit_all": fs_hits > 0 and fs_misses == 0, + "flags_count": fs_hits, "round": ri}) + var banner_hits: Array[Variant] = [] + for h in fs_fx: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "attack": + banner_hits.append(h) + if not banner_hits.is_empty(): + var strike_value := int((banner_hits[0] as Dictionary).get("value", 0)) + var flag_mult := 1 + for h in fs_fx: + if String(h.get("kind", "")) == "modify_ticket" and String(h.get("mode", "")) == "flag_mult": + flag_mult *= maxi(1, int(h.get("value", 1))) + if fs_misses == 0 and fs_hits > 0 and flag_mult > 1: + strike_value *= flag_mult # 完美布防:同轮全中旗伤倍增(全对推理奖励) + for fevt in oc._facts_flags: + if not bool(fevt.get("hit", false)): + continue + var st_t := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "feat_flag_strike", + "战旗军规", "board", strike_value, DamageController.TARGET_FRONT_ALIVE, ri, + fevt.cell, "normal") + oc._submit(st_t, false) + # round_end 发射+施加(内容数据驱动扩展批 T3——每轮结算收口:熔火之躯反打单开立〔open_ticket + # 即时执行〕+积层清零〔counter_add set〕;整备转蓄力由 finish_round_stop 承载——停手侧 AP 已在 + # request_stop 记账,转存决策=grant_ap(mode:transfer) 订阅定夺,无订阅走 RAP 转存缺省) + var re_fx := oc._fx_emit("round_end", {"round": ri}) + for h in re_fx: + if String(h.get("kind", "")) == "open_ticket" and String(h.get("channel", "")) == "blast": + var mg_layers := oc._fx.counter(String(h.get("per", ""))) + var mg_total := int(h.get("value", 0)) * mg_layers + if mg_total > 0: + # 迭代 20 T9——source_class 中性名(旧件名 feat_magma_body 随熔火之躯退役;per-counter 反打通道保留泛用面) + var mg_t := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "fx_blast_counter", + "积层反噬 ×%d" % mg_layers, "board", mg_total, + DamageController.TARGET_ALL_ALIVE, ri, oc.minesweeper.hero_pos(), "", 0, "", {}) + oc._submit(mg_t, false) + for h in re_fx: + if String(h.get("kind", "")) == "counter_add": + _apply_counter_hit(h) + # 迭代 20 T1——round_end 新施加面:grant_gp〔爆破会计/收盘——escrow 战斗域缓冲,随 gp_dropped 上抛〕/ + # grant_ap mode:queue〔封印回响——返 1 AP 下轮,queue_ap_delta 通道与轮始加成同面〕 + for h in re_fx: + if String(h.get("kind", "")) == "grant_gp": + oc.escrow += int(h.get("value", 0)) + oc._emit_row("effect", "fx_grant_gp", Vector2i(-1, -1), ri, "专长·GP 入账 +%d" % int(h.get("value", 0)), int(h.get("value", 0)), {"effect_id": "fx_grant_gp", "escrow": oc.escrow}, "") + elif String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + oc.action_economy.queue_ap_delta(int(h.get("value", 0))) + # 转存率入参(迭代 22 秘宝批 T3——储备罐 rap_transfer_mult magnitude=目标转存率百分比 125=1.25:1; + # 未装配=100 与旧 1:1 逐位一致) + var rap_pct: int = oc.attune_param_sum("rap_transfer_mult") + oc.action_economy.finish_round_stop(oc._fx != null and oc._fx.has_effect_on("round_end", "grant_ap", "transfer"), + rap_pct if rap_pct > 0 else 100) + if oc._facts_path == "cleared": + oc._grant_clear_reward(ri) + oc._flush_pending_reveals(ri) # 迭代 20 T1——揭示效果兑付(授予段末统一掷格,防轮 facts 迭代重入) + oc._damage.flush() # 授予段界结账(案C 后本段伤害单据全数暂存,此处对空队 no-op——保留作防御性段界) + + +# 旗判定(迭代 12 T3 标旗流本体随段迁入——原编排器 _judge_flag 逐字搬运,仅宿主引用加前缀; +# 唯一调用面=本段 run 的 facts_flags 循环)。 +func _judge_flag(evt: Dictionary, ri: int) -> void: + var cell: Vector2i = evt.cell + if evt.hit: + if oc._flag_consumed.has(cell): + return # 去重跳过(资格以雷格实例为键——排雷玩法 诚实重复上报的对侧);零明细行 + oc._flag_consumed[cell] = true + oc._perfect_hit_true += 1 + # flag_hit 发射+施加(内容数据驱动扩展批 T3——桶序固定承载等价:殉爆〔T4 秘宝面〕→ + # 净化改写 → 图谱额外资源 → 基线兑付) + var fh_hits := oc._fx_emit("flag_hit", {"cell": cell, "round": ri, + "terrain_kind": oc._harmful_adjacent(cell)}) + # 标中殉爆(迭代 11 同调批 T5 → 内容数据驱动扩展批 T4:订阅施加——行 triggers open_ticket + # channel blast value_from magnitude;每标中一面旗全体存活敌吃 N 群伤,无类型走 blast 通道。 + # 迭代 20 T1——封印爆裂〔迁入专长〕同面消费:行 triggers open_ticket channel blast) + for h in oc._hits_of(fh_hits, "open_ticket"): + if String(h.get("channel", "")) != "blast": + continue + var fbt := oc._damage.open_ticket(DamageController.KIND_BOARD_BLAST, "feat_flag_burst", "封印爆裂", "board", + int(h.get("value", 0)), DamageController.TARGET_ALL_ALIVE, ri, cell, "", 0, "highlight", {}) + oc._submit(fbt, false) + # 迭代 20 T1——标中新施加面:counter_add〔血税 标中+1 攻——bonus_delta atk 轴〕/ + # ac_bonus〔封印桩 本轮 AC+N〕/ preview_area〔示警烟火 3×3 资源类型预告〕/ + # remove_mines〔封印匠 每第 3 次该危险格中立移除——mod_eq 闸在行 when〕。 + # (净化工兵 rewrite_board 分支随老 27 件退役摘除——效果类型同批除名,T1 落法 commit 留痕) + for h in fh_hits: + match String(h.get("kind", "")): + "counter_add": + _apply_counter_hit(h) + "ac_bonus": + oc._apply_ac_bonus(h) + "bonus_delta": + var axis_f := String(h.get("axis", "")) + if oc._fx_bonus_delta.has(axis_f): + oc._fx_bonus_delta[axis_f] += int(h.get("delta", 0)) + "remove_mines": + var removed: Array[Variant] = oc.minesweeper.remove_mines_in([cell]) + if not removed.is_empty(): + oc._emit_row("mine_neutralized", "fx_seal_smith", cell, ri, "封印匠·危险格中立移除", 0, {}, "") + "preview_area": + var board_p: BoardData = oc.minesweeper.current_board() + var types: Array[Variant] = [] + for n in board_p.neighbors8(cell): + var n_snap = oc._reward_economy.query_full_snapshot(n) + if n_snap is Dictionary and not types.has(String((n_snap as Dictionary).get("cell_type", ""))): + types.append(String((n_snap as Dictionary).get("cell_type", ""))) + if oc.treasure_inventory != null and not types.is_empty(): + oc.treasure_inventory.apply_overlay("reward_preview", board_p.neighbors8(cell)) + oc._emit_row("fx_preview_area", "", cell, ri, "示警烟火·周围资源类型", 0, {"types": types}, "") + # 雷区图谱(迭代 12 T3 标旗流——标中雷额外获得一份格面资源:插旗开始产出对标开格; + # 复用 grant 单点,额外兑付走同一 _grant_routed 正向路由) + for h in fh_hits: + if String(h.get("kind", "")) == "grant_material": + _apply_material_hit(h, cell, ri, "flag_hit") + var snap = oc._reward_economy.grant(cell) + if not (snap is Dictionary): + return + oc._grant_routed(snap, "positive", "flag_hit", cell, ri, "flag_hit") + else: + oc._perfect_hit_false += 1 + if oc._flag_miss_seen.has(cell): + return # 每格实例一次去重(归因行与负向兑付同键——防逐轮惩罚轰炸) + oc._flag_miss_seen[cell] = true + # 完美保险(迭代 20 T4——本战下一次标错不破坏完美资格:_perfect_hit_false 回退=本次标错 + # 对完美谓词无痕;只保一次/件,节点级用完即止) + if oc._fx_insurance > 0: + oc._fx_insurance -= 1 + oc._perfect_hit_false -= 1 + oc._emit_row("insurance_used", "", cell, ri, "完美保险·标错豁免", 0, {"left": oc._fx_insurance}, "") + var hl := "" + if oc._perfect_hit_false == 1: + hl = "perfect_broken" # 本板完美谓词已破的归因锚 + oc._emit_row("flag_miss", "flag_miss", cell, ri, "标旗未中", 0, {}, hl) + # 危险直觉·守挂点(迭代 21 T3——flag_missed 发射:契约 12 触发主语=「当回合发生的标错 + # 动作」——去重闸=新旧标错判别器〔后续轮重判旧旗被闸挡回零触发〕;双条件②「结算时旗仍在 + # 场」由发射面既有过滤承载〔被挖开的旗已随挖掘擦除零兑付〕;grant_ap mode:queue 下一轮 + # 开轮兑现——末轮判出无下一轮=合法作废自动成立〔编排器每战新建,队列不跨战〕;限次=合成行 + # adventure 域计数器闸) + for h in oc._fx_emit("flag_missed", {"cell": cell, "round": ri}): + if String(h.get("kind", "")) == "grant_ap" and String(h.get("mode", "")) == "queue": + oc.action_economy.queue_ap_delta(int(h.get("value", 0))) + var snap = oc._reward_economy.grant(cell) + if not (snap is Dictionary): + return + oc._grant_routed(snap, "negative", "flag_miss", cell, ri, "") + + +# counter_add 三行组单点(原五位分支本体逐字收拢——kind 分派随段归位 P1-1) +func _apply_counter_hit(h: Dictionary) -> void: + var set_v = h.get("set", null) + oc._fx.counter_add(String(h.get("counter_id", "")), int(h.get("value", 0)) if set_v == null else int(set_v), set_v != null) + + +# grant_material 兑付块单点(原三位分支本体逐字收拢——source_class 两态 safe_cell/flag_hit 由参承载) +func _apply_material_hit(h: Dictionary, cell: Vector2i, ri: int, source_class: String = "safe_cell") -> void: + var g_snap = oc._reward_economy.grant(cell) + if g_snap is Dictionary: + oc._grant_routed(g_snap, "positive", source_class, cell, ri, "", String(h.get("label", ""))) diff --git a/game/core/settlement/grant_segment.gd.uid b/game/core/settlement/grant_segment.gd.uid new file mode 100644 index 00000000..2e82a6bd --- /dev/null +++ b/game/core/settlement/grant_segment.gd.uid @@ -0,0 +1 @@ +uid://b0f3h2g13d04g diff --git a/game/core/settlement/verdict_segment.gd b/game/core/settlement/verdict_segment.gd new file mode 100644 index 00000000..5c5bd287 --- /dev/null +++ b/game/core/settlement/verdict_segment.gd @@ -0,0 +1,33 @@ +class_name VerdictSegment +extends RefCounted +# ⑤ 判定段 Segment(迭代 27 T2——P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。 +# 职责:防御性优先级 hero 先判(双死结构不可达的纵深防御)→ 全量状态快照(despawn 前 +# 组装——终局含 result)→ 终局行+收束分派 / 下一轮出口分发。 +# 返回值 = 裁定("victory"/"defeat"/"continue")——宿主 _settle 据此保持旧早退语义 +#(终局不回置 _settling,_battle_over 接管);本段零算法改动(🔴 零行为变更契约)。 + +var oc: BattleOrchestrator # 宿主(类型标注=段内 := 推断前提;循环类名引用 Godot 4 全局类安全) 宿主 + + +func _init(host) -> void: + oc = host + + +func run(ri: int) -> String: + var verdict := "continue" + if int(oc._enemy_system.hero_unit().get("hp", 0)) <= 0: + verdict = "defeat" + elif oc._enemy_system.query_alive("enemy").is_empty(): + verdict = "victory" + oc._emit_settlement_snapshot(verdict) # 全量状态快照(despawn 前组装——终局含 result) + if verdict == "defeat": + oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗失败", 0, {}, "") + oc._battle_over("defeat") + return verdict + if verdict == "victory": + oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "战斗胜利", 1, {}, "") + oc._battle_over("victory") + return verdict + oc._emit_row("verdict", "", Vector2i(-1, -1), ri, "下一轮次", 0, {}, "") + oc._exit_distribution() + return verdict diff --git a/game/core/settlement/verdict_segment.gd.uid b/game/core/settlement/verdict_segment.gd.uid new file mode 100644 index 00000000..e44c4f30 --- /dev/null +++ b/game/core/settlement/verdict_segment.gd.uid @@ -0,0 +1 @@ +uid://i0dhvekibaam diff --git a/game/core/status_container.gd b/game/core/status_container.gd index 5e9159e9..685a50c4 100644 --- a/game/core/status_container.gd +++ b/game/core/status_container.gd @@ -33,7 +33,7 @@ func apply_effect(effect_id: String, source: Dictionary, duration_override: int if not _open: push_warning("[增益状态] 域闭授予(EC1)") return false - var row: Dictionary = db.status_effect_row(effect_id) + var row: Dictionary[Variant, Variant] = db.status_effect_row(effect_id) if row.is_empty(): push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id) return false @@ -88,7 +88,7 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, func query_active(side: String) -> Array: var out: Array[Dictionary] = [] for inst in _instances: - var row: Dictionary = db.status_effect_row(inst.effect_id) + var row: Dictionary[Variant, Variant] = db.status_effect_row(inst.effect_id) if row.is_empty(): continue if String(row.target_side) != side: diff --git a/game/core/treasure_items.gd b/game/core/treasure_items.gd index 9542f69a..dc619f90 100644 --- a/game/core/treasure_items.gd +++ b/game/core/treasure_items.gd @@ -17,7 +17,7 @@ class_name TreasureItems extends RefCounted -var _items: Array = [] # [{handle, item_id, used}]——受理序;Used 保留只读 +var _items: Array[Variant] = [] # [{handle, item_id, used}]——受理序;Used 保留只读 var _handle_seq := 0 # 受理序句柄(单调递增——零随机) var _reveal_cells := {} # 真视集:Vector2i → true(惩罚格标记——插旗仍走既有 flag 路径) var _preview_cells := {} # 预读集:Vector2i → true(奖励身份徽章呈现挂点) @@ -52,9 +52,9 @@ func add_item(item_id: String, source_class: String, cell: Vector2i, round_index # 枚举序=受理序;Used 保留只读;逐件含 usable 板级零信息预判(调用方喂板面回调) func query_inventory(board_has_preview_info: Callable) -> Array: - var out: Array = [] + var out: Array[Variant] = [] for rec in _items: - var row: Dictionary = rec.duplicate() + var row: Dictionary[Variant, Variant] = rec.duplicate() row["usable"] = _usable_now(String(rec.item_id), board_has_preview_info) out.append(row) return out @@ -65,14 +65,6 @@ func inventory_count() -> int: # 持有数(迭代 10 奖励重构批——同调位上限判定读数:只计未消耗件;Used 不可逆即不再持有) -func holding_count() -> int: - var n := 0 - for rec in _items: - if not bool(rec.get("used", false)): - n += 1 - return n - - # 同调位守卫谓词——迭代 11 第①批 T7 退役(读取源 transitional_sync_slots 过渡键已随表退役; # 容量闸正主 = 营地装配面 Attunement.slots 满位禁选。函数体保留空壳防四散引用误删遗漏—— # T8 冒烟「过渡键读取点归零」断言覆盖) @@ -80,22 +72,14 @@ func holding_count() -> int: # 持有件记录(迭代 10 追加批——奖励节点替换子交互数据源:只列未消耗件,handle 定位丢弃目标) # 丢弃(同调位替换交互节点渠道——腾位收新;与 Used 消费分轨的一次性移除,快照不再含) -func discard_item(handle: int) -> bool: - for i in _items.size(): - if int(_items[i].get("handle", -1)) == handle and not bool(_items[i].get("used", false)): - _items.remove_at(i) - return true - return false - - # 板级零信息可用性预判(图标置灰/入口禁用判据——GDD「整张图都没东西可看」级): # 真视恒可用(零雷窗口=信息非空);AP 类恒可用(组合判据取 OR——任一效果有信息即可用); # 纯预读行 = 板上还有未开格才可用。(积点件不入使用面——反哺批⑤自动兑付无玩家入口) func _usable_now(item_id: String, board_has_preview_info: Callable) -> bool: - var row: Dictionary = _item_row(item_id) + var row: Dictionary[Variant, Variant] = _item_row(item_id) if row.is_empty(): return false - var kinds: Array = row.get("effect_kinds", []) + var kinds: Array[Variant] = row.get("effect_kinds", []) if kinds.has("mine_reveal") or kinds.has("ap_grant"): return true return bool(board_has_preview_info.call()) if board_has_preview_info.is_valid() else true @@ -127,11 +111,11 @@ func validate_use(handle: int, target: Vector2i, board_size: Vector2i, cell_unre return _reject("句柄不存在") if bool(rec.used): return _reject("道具已使用(Used 不可逆)") - var row: Dictionary = _item_row(String(rec.item_id)) + var row: Dictionary[Variant, Variant] = _item_row(String(rec.item_id)) if row.is_empty() or bool(row.get("retired", false)) or bool(row.get("deferred", false)): return _reject("道具未激活") - var kinds: Array = row.get("effect_kinds", []) - var resolved: Array = [] + var kinds: Array[Variant] = row.get("effect_kinds", []) + var resolved: Array[Variant] = [] if kinds.has("mine_reveal") or kinds.has("reward_preview") or kinds.has("reveal_tiles") or kinds.has("safe_dig"): # 空间效果(迭代 22 秘宝批 T3 扩面——真视 reveal_tiles/天眼复合 safe_dig 入空间解析; # 真视窄域护栏 REVEAL_KINDS 同守): @@ -206,7 +190,7 @@ func consume(handle: int) -> Dictionary: # 随图消亡——停点处无战斗在飞,天然空集)。 func snapshot_inventory() -> Array: - var out: Array = [] + var out: Array[Variant] = [] for rec in _items: var cell: Vector2i = rec.get("origin_cell", Vector2i(-1, -1)) out.append({ @@ -224,7 +208,7 @@ func restore_inventory(items: Array) -> void: _handle_seq = 0 for row in items: _handle_seq += 1 - var cell_arr: Array = row.get("origin_cell", [-1, -1]) + var cell_arr: Array[Variant] = row.get("origin_cell", [-1, -1]) _items.append({ "handle": _handle_seq, "item_id": String(row.get("item_id", "")), diff --git a/game/core/unit_container.gd b/game/core/unit_container.gd index b4258fa9..97a1daec 100644 --- a/game/core/unit_container.gd +++ b/game/core/unit_container.gd @@ -51,7 +51,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides push_warning("[敌人系统] Alive 相位重复 spawn(相位守卫)") return false # 行集数组口径(2026-10-07 行集多行批——resolver 返回该坐标全部行、文件序) - var rowset: Array = db.resolve_enemy_rowset(level_index, node_depth) + var rowset: Array[Variant] = db.resolve_enemy_rowset(level_index, node_depth) if rowset.is_empty(): push_error("[敌人系统] 遭遇行解析失败/越域 (%d,%d)" % [level_index, node_depth]) assert(false, "遭遇行悬空(R1 前置守卫)") @@ -65,7 +65,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides # (恢复点/治疗包的满血守卫与治疗上限随之失效——切片登记接口语义澄清) _hero = {"hp": hero_hp, "max_hp": eff_max_hp, "atk": eff_atk, "is_hero": true, "alive": true, "display_name": "探险者", "enemy_id": "hero", "armor_type": hero_armor_type if hero_armor_type != "" else db.forge_default_armor_type(), "attack_type": hero_attack_type if hero_attack_type != "" else db.forge_default_attack_type()} # 护甲/攻击类型轴快照(迭代 23 T2——hero.tres 两轴删除后改读注入值,空串回退锻造工坊配置默认值 normal/light=行为连续;甲效矩阵消费面) _enemies.clear() # 难度曲线与Boss DifficultyTable 修饰(2026-09-12 结构位兑现):施加于行值、行值快照之前——MVP 空表 = 恒 1.0 无行为(AC-10);混编行集内全部敌种同乘((L,D) 精确匹配一次取值——F9 同乘不改变异,敌人系统 v0.15) - var mod: Dictionary = db.resolve_difficulty_modifiers(level_index, node_depth) + var mod: Dictionary[Variant, Variant] = db.resolve_difficulty_modifiers(level_index, node_depth) var hp_mult: float = float(mod.get("hp_mult", 1.0)) var atk_mult: float = float(mod.get("atk_mult", 1.0)) # 逐行数量定值(敌人系统 零随机纪律——骰定在 战斗结算 遭遇骰层按行完成,本层收确定值; @@ -73,7 +73,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides var per_row_counts: Array[int] = [] var total_instances := 0 for row_i in range(rowset.size()): - var row_i_dict: Dictionary = rowset[row_i] + var row_i_dict: Dictionary[Variant, Variant] = rowset[row_i] var ovr: int = int(unit_count_overrides[row_i]) if row_i < unit_count_overrides.size() else -1 var count: int = ovr if ovr >= 1 else int(row_i_dict.unit_count) if count < EnemyTable.UNIT_COUNT_MIN or count > EnemyTable.UNIT_COUNT_MAX: @@ -87,7 +87,7 @@ func spawn(level_index: int, node_depth: int, hero_hp: int, unit_count_overrides return false var handle := 0 for row_i in range(rowset.size()): - var row: Dictionary = rowset[row_i] + var row: Dictionary[Variant, Variant] = rowset[row_i] var eovr: int = int(enemy_atk_overrides[row_i]) if row_i < enemy_atk_overrides.size() else -1 for i in range(per_row_counts[row_i]): var eff_row_atk: int = eovr if eovr >= 0 else int(row.atk) @@ -174,7 +174,7 @@ func special_skills(u: Dictionary) -> Array: for e in u.pool: if _entry_locked(e): continue - var srow: Dictionary = db.skill_row(String(e.skill_id)) + var srow: Dictionary[Variant, Variant] = db.skill_row(String(e.skill_id)) if not srow.is_empty() and String(srow.type) != "basic": out.append({"skill_id": String(e.skill_id), "weight": int(e.weight)}) return out @@ -184,7 +184,7 @@ func special_skills(u: Dictionary) -> Array: # 一次过门、单向闩〔实例标记,不解锁回退〕。门=确定性谓词零随机;返回本拍新解锁的 skill_id 列表 # 供编排层出归因行〔每技恰一次——闩已置位不再返回〕) func check_skill_unlocks(u: Dictionary) -> Array: - var out: Array = [] + var out: Array[Variant] = [] if u.is_empty() or _despawned or u.get("is_hero", false) or not bool(u.get("alive", false)): return out for e in u.pool: @@ -233,7 +233,7 @@ func commit_action(u: Dictionary) -> void: if sid == "": _diag("commit_action 无待发动作(兑现序违例)") return - var srow: Dictionary = db.skill_row(sid) + var srow: Dictionary[Variant, Variant] = db.skill_row(sid) var cd: int = int(srow.get("cooldown_beats", 0)) if not srow.is_empty() else 0 for e in u.pool: if String(e.skill_id) == sid: diff --git a/game/data/schemas/enemy_table.gd b/game/data/schemas/enemy_table.gd index 899cf046..8229043b 100644 --- a/game/data/schemas/enemy_table.gd +++ b/game/data/schemas/enemy_table.gd @@ -88,7 +88,7 @@ func validate() -> Array: # 池模板(敌人技能池批——单表结构校验;跨表构成纪律在 ConfigDB): # 非空池 + 条目 {skill_id 非空, weight > 0} + 池内 skill_id 不重复。 # 普攻冷却恒 0(SkillTable schema 锁死)→ 可摇集合恒非空,"空池不可能"由此结构保证。 - var pool: Array = row.get("pool", []) + var pool: Array[Variant] = row.get("pool", []) if pool.is_empty(): errs.append({"category": "domain", "message": "%s pool 空池(禁——技能池行必带池模板)" % eid}) var pool_ids := {} @@ -96,7 +96,7 @@ func validate() -> Array: if not (entry_v is Dictionary): errs.append({"category": "domain", "message": "%s pool 条目非字典: %s" % [eid, str(entry_v)]}) continue - var entry: Dictionary = entry_v + var entry: Dictionary[Variant, Variant] = entry_v var sid: String = String(entry.get("skill_id", "")) if sid == "": errs.append({"category": "domain", "message": "%s pool 条目 skill_id 空" % eid}) diff --git a/game/data/schemas/feat_table.gd b/game/data/schemas/feat_table.gd index 1e91f85a..51c028a8 100644 --- a/game/data/schemas/feat_table.gd +++ b/game/data/schemas/feat_table.gd @@ -24,7 +24,7 @@ class_name FeatTable extends Resource -@export var rows: Array = [] # Array[Dictionary] 键 feat_id +@export var rows: Array[Variant] = [] # Array[Dictionary] 键 feat_id ## 稀有度词表(四级——与令牌池 TIERS 同名词族;令牌档 = rarity 直映射) const RARITIES := ["common", "excellent", "rare", "legendary"] @@ -37,7 +37,7 @@ const TRIGGER_KEYS := ["on", "when", "do"] func validate() -> Array: - var errs: Array = [] + var errs: Array[Variant] = [] if rows.is_empty(): return errs # 空表合法态(T1 基建先行——逐件填行) var seen := {} diff --git a/game/data/schemas/forge_table.gd b/game/data/schemas/forge_table.gd index 4106ee8a..04624214 100644 --- a/game/data/schemas/forge_table.gd +++ b/game/data/schemas/forge_table.gd @@ -62,7 +62,7 @@ func validate() -> Array: continue # 类型轴:武器=attack_type 域、甲=armor_type 域;每型恰一路线(三武器三甲各一) var axis := String(row.get("type_axis", "")) - var domain: Array = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES + var domain: Array[Variant] = ATTACK_TYPES if kind == "weapon" else ARMOR_TYPES if not domain.has(axis): errs.append({"category": "enum", "message": "%s type_axis 出域 %s: %s" % [rid, domain, axis]}) var type_key := "%s|%s" % [kind, axis] diff --git a/game/data/schemas/level_table.gd b/game/data/schemas/level_table.gd index e33554a1..d4a6f3dd 100644 --- a/game/data/schemas/level_table.gd +++ b/game/data/schemas/level_table.gd @@ -49,7 +49,7 @@ func validate() -> Array: # 模板池(关卡节点图生成 v0.4——M-3 迭代 5 T5):row.templates 非空 = 池形态,池内全模板加载期校验 # (违规致命——不只抽中的那张);空 = 旧单图形态(row.nodes 直读,向后兼容零迁移)。 var node_lists: Array[Array] = [] # [ [上下文标签, nodes 数组], ... ] - var templates: Array = row.get("templates", []) + var templates: Array[Variant] = row.get("templates", []) if templates.is_empty(): node_lists.append(["", row.get("nodes", [])]) else: @@ -115,7 +115,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> var nexts: Array[Array] = [] # 每节点出边 id 数组 var id_to_idx := {} for i in nodes.size(): - var node: Dictionary = nodes[i] + var node: Dictionary[Variant, Variant] = nodes[i] var nid: int = int(node.get("node_depth", i)) if is_graph else i if is_graph and not node.has("node_depth"): errs.append({"category": "structural", "message": "level %d%s 图形态节点[%d] 缺 node_depth(全节点须编号化)" % [L, tag, i]}) @@ -190,7 +190,7 @@ func _validate_graph(L: int, nodes: Array, is_graph: bool, tag: String = "") -> errs.append({"category": "structural", "message": "level %d 节点 %d 沿连线不可达 Boss" % [L, ids[i]]}) # ⑧ U2 目检新规则(迭代 5 返工批 2026-09-29):BFS 同深度层节点数上限 5(分支宽度天花板) var layer_of := {} - var bfs: Array = [entries[0]] + var bfs: Array[Variant] = [entries[0]] layer_of[entries[0]] = 0 while not bfs.is_empty(): var cur: int = bfs.pop_front() @@ -214,7 +214,7 @@ func _reachable(start: int, adj: Array, n: int) -> Dictionary: var seen := {} if start < 0 or start >= n: return seen - var queue: Array = [start] + var queue: Array[Variant] = [start] seen[start] = true while not queue.is_empty(): var cur: int = queue.pop_front() diff --git a/game/data/schemas/meta_tree_table.gd b/game/data/schemas/meta_tree_table.gd index fb6b4e7e..77d3b5d9 100644 --- a/game/data/schemas/meta_tree_table.gd +++ b/game/data/schemas/meta_tree_table.gd @@ -27,7 +27,7 @@ class_name MetaTreeTable extends Resource -@export var rows: Array = [] # Array[Dictionary] 键 node_id +@export var rows: Array[Variant] = [] # Array[Dictionary] 键 node_id const VEINS := ["spine", "forge", "keystone", "attune"] const KINDS := ["stat", "rule", "unlock", "socket", "fork", "easter"] @@ -47,7 +47,7 @@ const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy"] func validate() -> Array: - var errs: Array = [] + var errs: Array[Variant] = [] if rows.is_empty(): return errs # 空表合法态(建表先行——内容随换代批落) var seen := {} @@ -81,7 +81,7 @@ func validate() -> Array: var price := int(row.get("price", 0)) if price < 0 or price > 9999: errs.append({"category": "domain", "message": "%s price 越域 [0,9999]: %d" % [nid, price]}) - var fx: Dictionary = row.get("effects", {}) as Dictionary + var fx: Dictionary[Variant, Variant] = row.get("effects", {}) as Dictionary for ek in fx.keys(): if not (STAT_KEYS.has(String(ek)) or RULE_KEYS.has(String(ek))): errs.append({"category": "zero_flat", "message": "%s effects 键出域(stat 线/rule 词表封闭): %s" % [nid, String(ek)]}) @@ -100,7 +100,7 @@ func validate() -> Array: if not fx.is_empty(): errs.append({"category": "cross_field", "message": "%s fork 行主 effects 须空(效果只放 a/b 双键)" % nid}) for skey in ["effects_a", "effects_b"]: - var sd: Dictionary = row.get(skey, {}) as Dictionary + var sd: Dictionary[Variant, Variant] = row.get(skey, {}) as Dictionary if sd.is_empty(): errs.append({"category": "domain", "message": "%s %s 缺失或空(双态节点须双侧效果)" % [nid, skey]}) for k in sd.keys(): diff --git a/game/data/schemas/settlement_table.gd b/game/data/schemas/settlement_table.gd index 6c8d60c3..2769e883 100644 --- a/game/data/schemas/settlement_table.gd +++ b/game/data/schemas/settlement_table.gd @@ -83,7 +83,7 @@ func validate() -> Array: if d < 1 or d > 99 or (i > 0 and d >= int(roll_tier_discounts[i - 1])): errs.append({"category": "cross_field", "message": "roll_tier_discounts[%d] 越域/非降序(档位不设零档): %d" % [i, d]}) for atk_type in ATTACK_TYPE_KEYS: - var row: Dictionary = armor_effectiveness.get(atk_type, {}) + var row: Dictionary[Variant, Variant] = armor_effectiveness.get(atk_type, {}) for armor in ARMOR_KEYS: var pct: int = int(row.get(armor, -99)) if pct < -3 or pct > 3: diff --git a/game/data/schemas/shop_table.gd b/game/data/schemas/shop_table.gd index fd257f72..cb05058e 100644 --- a/game/data/schemas/shop_table.gd +++ b/game/data/schemas/shop_table.gd @@ -57,7 +57,7 @@ func validate() -> Array: errs.append({"category": "cross_field", "message": "gp_drops %s 违反 roll_min(%d) ≤ mean(%d) ≤ roll_max(%d)——均值恒等期望锚" % [tier, rmin, mean, rmax]}) # level_gp_bonus / price_level_bonus:level_index 连续从 1 递增 + 加值域 for family in ["level_gp_bonus", "price_level_bonus"]: - var arr: Array = get(family) + var arr: Array[Variant] = get(family) var seen := {} for row in arr: var li: int = int(row.get("level_index", 0)) diff --git a/game/data/schemas/treasure_item_table.gd b/game/data/schemas/treasure_item_table.gd index f6db6863..76bc12cc 100644 --- a/game/data/schemas/treasure_item_table.gd +++ b/game/data/schemas/treasure_item_table.gd @@ -32,7 +32,7 @@ class_name TreasureItemTable extends Resource -@export var rows: Array = [] # Array[Dictionary] 键 item_id +@export var rows: Array[Variant] = [] # Array[Dictionary] 键 item_id # 计数键封闭枚举(原 AttunementGrowthTable.COUNTER_KEYS——迭代 21 T5 谓词表退役后就地内联; # 迭代 22 T1 换代:clear_count(旧工坊按关累计渠道)与 C6 通关口径(通关=打通整局冒险)不符, @@ -85,7 +85,7 @@ const KEY_WHITELIST := ["item_id", "display_name", "dimension_tags", "effect_kin func validate() -> Array: - var errs: Array = [] + var errs: Array[Variant] = [] if rows.is_empty(): # 空表合法态(2026-09-17 用户拍板——切片期无道具内容;F2 覆盖检查仅对非空清单执行) return errs @@ -104,13 +104,13 @@ func validate() -> Array: var retired: bool = bool(row.get("retired", false)) var deferred: bool = bool(row.get("deferred", false)) # dimension_tags:五维词表 + 非空(退役/失义行仍须登记维度——语义面保留) - var tags: Array = row.get("dimension_tags", []) + var tags: Array[Variant] = row.get("dimension_tags", []) if tags.is_empty(): errs.append({"category": "domain", "message": "%s dimension_tags 空" % iid}) for tag in tags: if not DIMENSIONS.has(String(tag)): errs.append({"category": "enum", "message": "%s dimension_tag 出域 %s: %s" % [iid, str(DIMENSIONS), str(tag)]}) - var kinds: Array = row.get("effect_kinds", []) + var kinds: Array[Variant] = row.get("effect_kinds", []) var target: String = String(row.get("target", "none")) if not TARGETS.has(target): errs.append({"category": "enum", "message": "%s target 出域 %s: %s" % [iid, str(TARGETS), target]}) @@ -217,7 +217,7 @@ func validate() -> Array: if not kind_seen.has("ap_charge_safe") and charge_n != 0: errs.append({"category": "cross_field", "message": "%s 非 ap_charge_safe 行 charge_n 应 0,实得 %d" % [iid, charge_n]}) # unlock_requires 谓词行:空 = 已解锁(首批全解锁合法态,底稿 §2.2);非空键 ∈ 计数键封闭枚举 - var ureqs: Array = row.get("unlock_requires", []) + var ureqs: Array[Variant] = row.get("unlock_requires", []) var ukeys := {} for req in ureqs: var ukey: String = String(req.get("key", "")) @@ -234,7 +234,7 @@ func validate() -> Array: # 同调行词表(form 非空)——装配覆盖层/效果接线/同调位上限的唯一取数口径(迭代 11 第①批 T1) func attunement_rows() -> Array: - var out: Array = [] + var out: Array[Variant] = [] for row in rows: if String(row.get("form", "")) != "" and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)): out.append(row) @@ -244,7 +244,7 @@ func attunement_rows() -> Array: # 过渡行词表(form 空 = 切片期过渡形态活行)——掉落投放/停点池/过渡覆盖面的取数口径; # 同调行永不入过渡渠道。T7 退役面切换后本口径随渠道消亡。 func transitional_rows() -> Array: - var out: Array = [] + var out: Array[Variant] = [] for row in rows: if String(row.get("form", "")) == "" and not bool(row.get("retired", false)) and not bool(row.get("deferred", false)): out.append(row) @@ -253,7 +253,7 @@ func transitional_rows() -> Array: # 活行词表(非 retired 非 deferred)——投放校验/奖励节点池/F2 覆盖面的唯一取数口径 func active_rows() -> Array: - var out: Array = [] + var out: Array[Variant] = [] for row in rows: if not bool(row.get("retired", false)) and not bool(row.get("deferred", false)): out.append(row) diff --git a/game/presentation/battle_narrator.gd b/game/presentation/battle_narrator.gd index ef71cafa..b43ea54b 100644 --- a/game/presentation/battle_narrator.gd +++ b/game/presentation/battle_narrator.gd @@ -73,9 +73,9 @@ static func phase_text(round_index: int, battle_result: String, waiting: bool, b # 零内容期每坐标恰一行,行为零变化) static func boss_name(runner, db) -> String: if runner != null: - var node: Dictionary = runner.current_node() + var node: Dictionary[Variant, Variant] = runner.current_node() if String(node.get("type", "")) == "boss": - var rowset: Array = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) + var rowset: Array[Variant] = db.resolve_enemy_rowset(int(runner.level_row.level_index), int(node.get("node_depth", -1))) if not rowset.is_empty(): return String(rowset[0].get("display_name", "")) return "遗迹守卫" @@ -173,12 +173,12 @@ static func adventure_summary_text(summary: Dictionary) -> String: # 钥石入账明细文案(元进度实现批;迭代 10 改口径——钥石全源 = Boss 逐杀掉落,「大奖励」行退役; # name_lookup = item_id → 显示名投影,由组合根自 meta 表喂入) static func keystone_settle_text(detail: Dictionary, name_lookup: Dictionary = {}) -> String: - var parts: Array = [] + var parts: Array[Variant] = [] for d in detail.get("boss_drops", []): parts.append("第 %d 关 Boss %s +%d" % [int(d.get("level", 0)), "首杀" if bool(d.get("first", false)) else "重复杀", int(d.get("amount", 0))]) var line := "钥石入账:" + (" · ".join(parts) if not parts.is_empty() else "本局无掉落") - var unlocks: Array = detail.get("new_unlocks", []) + var unlocks: Array[Variant] = detail.get("new_unlocks", []) if not unlocks.is_empty(): var names: Array[String] = [] for iid in unlocks: diff --git a/game/presentation/camp_flow_context.gd b/game/presentation/camp_flow_context.gd new file mode 100644 index 00000000..7d0b6d41 --- /dev/null +++ b/game/presentation/camp_flow_context.gd @@ -0,0 +1,38 @@ +class_name CampFlowContext +extends RefCounted +# 营地/商店投影的构造上下文(迭代 27 T4——审计 P1-5:控制器构造参数收拢为 context 对象, +# 原 13 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 +# 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 + +var db +var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) +var save_get: Callable # () -> SaveManagerCore +var camp_overlay: CampOverlay +var shop_overlay: ShopOverlay +var toast: ToastStack +var start_overlay: Control +var logger # PlaytestLogger(试玩日志验收取数面) +var runner_get: Callable # () -> LevelRunner +var open_modal: Callable +var close_modal: Callable +var refresh_presentations: Callable +var refresh_resume_button: Callable + + +func _init(db_ref, meta_get_cb: Callable, save_get_cb: Callable, camp_overlay_ref: CampOverlay, + shop_overlay_ref: ShopOverlay, toast_ref: ToastStack, start_overlay_ref: Control, + logger_ref, runner_get_cb: Callable, open_modal_cb: Callable, close_modal_cb: Callable, + refresh_presentations_cb: Callable, refresh_resume_button_cb: Callable) -> void: + db = db_ref + meta_get = meta_get_cb + save_get = save_get_cb + 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_cb + open_modal = open_modal_cb + close_modal = close_modal_cb + refresh_presentations = refresh_presentations_cb + refresh_resume_button = refresh_resume_button_cb diff --git a/game/presentation/camp_flow_context.gd.uid b/game/presentation/camp_flow_context.gd.uid new file mode 100644 index 00000000..2ab42493 --- /dev/null +++ b/game/presentation/camp_flow_context.gd.uid @@ -0,0 +1 @@ +uid://dxj1wy8n1yp6k diff --git a/game/presentation/camp_shop_presenter.gd b/game/presentation/camp_shop_presenter.gd index 945a6dbf..e9548e4f 100644 --- a/game/presentation/camp_shop_presenter.gd +++ b/game/presentation/camp_shop_presenter.gd @@ -22,23 +22,21 @@ 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 +func _init(ctx: CampFlowContext) -> void: + # 迭代 27 T4——构造参数收拢 context 对象(P1-5:13 参长签名收一处命名面;字段逐一同名迁移) + db = ctx.db + _meta_get = ctx.meta_get + _save_get = ctx.save_get + camp_overlay = ctx.camp_overlay + shop_overlay = ctx.shop_overlay + toast = ctx.toast + start_overlay = ctx.start_overlay + logger = ctx.logger + _runner_get = ctx.runner_get + _open_modal = ctx.open_modal + _close_modal = ctx.close_modal + _refresh_presentations = ctx.refresh_presentations + _refresh_resume_button = ctx.refresh_resume_button # 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后, diff --git a/game/presentation/curio_flow_context.gd b/game/presentation/curio_flow_context.gd new file mode 100644 index 00000000..8a0dfee0 --- /dev/null +++ b/game/presentation/curio_flow_context.gd @@ -0,0 +1,45 @@ +class_name CurioFlowContext +extends RefCounted +# 三选一/替换子交互的构造上下文(迭代 27 T4——审计 P1-5:控制器构造参数收拢为 context 对象, +# 原 16 参长签名收一处命名面——单测可直接 new() 本 context + 控制器获得独立实例化缝)。 +# 字段即原构造参数逐一同名迁移;依赖仅视图句柄 + 查询/编排 Callable(组合根注入)。 + +var db +var meta_get: Callable # () -> MetaProgressionCore(现值回调——组合根 lambda) +var curio_overlay: CurioPickOverlay +var end_overlay: BattleEndOverlay +var board_view: BoardView +var toast: ToastStack +var save_get: Callable # () -> SaveManagerCore +var adventure_get: Callable # () -> AdventureRunner(会话现值) +var runner_get: Callable # () -> LevelRunner +var adventure_mode_get: Callable # () -> bool +var open_modal: Callable # (Control) -> void +var open_modal_after_playback: Callable +var close_modal: Callable +var refresh_presentations: Callable +var fatal_screen: Callable +var gp_summary: Callable # (Dictionary) -> String + + +func _init(db_ref, meta_get_cb: Callable, curio_overlay_ref: CurioPickOverlay, end_overlay_ref: BattleEndOverlay, + board_view_ref: BoardView, toast_ref: ToastStack, save_get_cb: Callable, + adventure_get_cb: Callable, runner_get_cb: Callable, adventure_mode_get_cb: Callable, + open_modal_cb: Callable, open_modal_after_playback_cb: Callable, close_modal_cb: Callable, + refresh_presentations_cb: Callable, fatal_screen_cb: Callable, gp_summary_cb: Callable) -> void: + db = db_ref + meta_get = meta_get_cb + curio_overlay = curio_overlay_ref + end_overlay = end_overlay_ref + board_view = board_view_ref + toast = toast_ref + save_get = save_get_cb + adventure_get = adventure_get_cb + runner_get = runner_get_cb + adventure_mode_get = adventure_mode_get_cb + open_modal = open_modal_cb + open_modal_after_playback = open_modal_after_playback_cb + close_modal = close_modal_cb + refresh_presentations = refresh_presentations_cb + fatal_screen = fatal_screen_cb + gp_summary = gp_summary_cb diff --git a/game/presentation/curio_flow_context.gd.uid b/game/presentation/curio_flow_context.gd.uid new file mode 100644 index 00000000..5b1a0669 --- /dev/null +++ b/game/presentation/curio_flow_context.gd.uid @@ -0,0 +1 @@ +uid://lrba2xy83jwv diff --git a/game/presentation/curio_interact_controller.gd b/game/presentation/curio_interact_controller.gd index 85a13c76..71c55110 100644 --- a/game/presentation/curio_interact_controller.gd +++ b/game/presentation/curio_interact_controller.gd @@ -31,34 +31,27 @@ var _pending_curio := {} # 三选一停点载荷暂存(迭代 1 var _curio_context := "level_clear" # 三选一面上下文(level_clear=通关停点→推进下一关 / reward_node=奖励节点→回图选路) -func _init(db_ref, meta_get: Callable, curio_overlay_ref: CurioPickOverlay, 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 - curio_overlay = curio_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 +func _init(ctx: CurioFlowContext) -> void: + # 迭代 27 T4——构造参数收拢 context 对象(P1-5:16 参长签名收一处命名面;字段逐一同名迁移) + db = ctx.db + _meta_get = ctx.meta_get + curio_overlay = ctx.curio_overlay + end_overlay = ctx.end_overlay + board_view = ctx.board_view + toast = ctx.toast + _save_get = ctx.save_get + _adventure_get = ctx.adventure_get + _runner_get = ctx.runner_get + _adventure_mode_get = ctx.adventure_mode_get + _open_modal = ctx.open_modal + _open_modal_after_playback = ctx.open_modal_after_playback + _close_modal = ctx.close_modal + _refresh_presentations = ctx.refresh_presentations + _fatal_screen = ctx.fatal_screen + _gp_summary = ctx.gp_summary # 停点在途判据(组合根 _on_battle_end_continue 分派面——战斗总结确认后开三选一) -func has_pending() -> bool: - return not _pending_curio.is_empty() - - # 续玩停点恢复入口(true=已接手呈现,调用方中止后续分派)。空候选降级(2026-10-06 用户令): # 候选 0 件不开选择面(空面无卡无钮=死面)——消化停点后按上下文直落后续推进(战斗总结面已 # 给过玩家确认拍,三选一环节整体跳过) @@ -126,8 +119,8 @@ func on_presented(payload: Dictionary) -> void: func present_node_curio() -> void: var adventure = _adventure_get.call() _curio_context = "reward_node" - var node_draw: Dictionary = adventure.node_curio_candidates() - var node_cands: Array = node_draw.get("candidates", []) as Array + var node_draw: Dictionary[Variant, Variant] = adventure.node_curio_candidates() + var node_cands: Array[Variant] = node_draw.get("candidates", []) as Array if node_cands.is_empty(): # 空池降级(2026-10-06 用户令)——不开面、不挂节点停点(present_node_curio 未调即未挂), # 兑付确认直落回图选路(auto-sim「node_entered 回调内立即 continue_after_node」先例) @@ -144,7 +137,7 @@ func present_node_curio() -> void: # 三选一面开出(战斗总结结算确认 / 续玩停点恢复) func show_pick() -> void: - var payload: Dictionary = _pending_curio + var payload: Dictionary[Variant, Variant] = _pending_curio _open_modal.call(curio_overlay) curio_overlay.open_pick(Narrator.curio_pick_header(payload), _curio_card_texts(payload.get("candidates", []) as Array)) var adventure = _adventure_get.call() @@ -154,7 +147,7 @@ func show_pick() -> void: # 刷新呈现投喂单点(免费余量 X/Y + GP 付费价 + 冒险钱包可付——读数与商店 set_refresh_display 同构) -func _feed_refresh_display(adventure) -> void: +func _feed_refresh_display(adventure: AdventureRunner) -> void: var runner = _runner_get.call() var gp := int(runner.gp) if runner != null else 0 var cost: int = adventure.curio_refresh_gp_cost() @@ -164,7 +157,7 @@ func _feed_refresh_display(adventure) -> void: func _curio_card_texts(candidates: Array) -> Array: # TP0 令牌池——候选 = {"kind","tier"} 令牌字典;卡片 id = "kind|tier|feat_id" 复合键(同 kind # 大小两档/同档多 relic 可同屏,kind 单键不构成唯一身份),确认回程拆键(on_pick_confirmed) - var out: Array = [] + var out: Array[Variant] = [] for tok in candidates: var kind := String((tok as Dictionary).get("kind", "")) var tier := String((tok as Dictionary).get("tier", "")) @@ -206,7 +199,7 @@ func on_refresh() -> void: if not refreshed: return if _curio_context == "reward_node": - var st: Dictionary = adventure.node_curio_pending_state() + var st: Dictionary[Variant, Variant] = adventure.node_curio_pending_state() curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st.get("candidates", []) as Array)) _feed_refresh_display(adventure) @@ -227,7 +220,7 @@ func on_pick_confirmed(item_id: String) -> void: # T5——节点渠道门票拿到即开:候选已被替换为专长面,本地读在途态重绘(零 emit——context 保持 reward_node, # 后续兑付/确认仍走节点路径回图选路) if _curio_context == "reward_node" and kind == "feat_ticket": - var st2: Dictionary = adventure.node_curio_pending_state() + var st2: Dictionary[Variant, Variant] = adventure.node_curio_pending_state() curio_overlay.open_pick(Narrator.node_curio_header(), _curio_card_texts(st2.get("candidates", []) as Array)) diff --git a/game/presentation/debug_session.gd b/game/presentation/debug_session.gd index 4979cb51..04feb149 100644 --- a/game/presentation/debug_session.gd +++ b/game/presentation/debug_session.gd @@ -77,7 +77,7 @@ func reset() -> void: func _start_battle(level: int, carry_hp: int) -> bool: if not _level_mode or _level_idx == 0: rng.new_adventure(_seed_value.call()) - var ov: Dictionary = _hero_override.call() # E4:生效值经 encounter 传入——零表写 + var ov: Dictionary[Variant, Variant] = _hero_override.call() # E4:生效值经 encounter 传入——零表写 settlement = BattleOrchestrator.new(db, rng) battle_created.emit(settlement, level, carry_hp) var hp: int = carry_hp if carry_hp > 0 else int(ov.max_hp) diff --git a/game/presentation/gp_shadow_ledger.gd b/game/presentation/gp_shadow_ledger.gd deleted file mode 100644 index d075d9bf..00000000 --- a/game/presentation/gp_shadow_ledger.gd +++ /dev/null @@ -1,31 +0,0 @@ -# VERTICAL SLICE - NOT FOR PRODUCTION -# GP 冒险钱包呈现侧影子账本(迭代 6 T7 卡⑩;T3 拆件批 2026-10-02 自 main.gd 迁出——规范 1.1): -# 冒险进行中余额影子 + 收束清零拍转存的最终合计,供冒险终局面板 GP 行取数。 -# 手工镜像边界:wallet_changed gain/buy 载荷 = 入账后总额(level_runner.gd:482/539),"clear" 到达即 -# 影子转存 final 并归零——纯呈现侧账本,零核心触碰(C1 呈现层方案);写入口唯一 = apply_event -#(组合根 _on_wallet_changed 投喂;探针合成注入经 set_view_total——组合根委托面转发)。 -extends RefCounted - -var _view := 0 # 冒险进行中余额影子(gain/buy 载荷直录) -var _final := 0 # 收束清零拍转存的最终合计("clear" 到达时自影子转存) - - -func apply_event(event: String, gp: int) -> void: - match event: - "gain", "buy": - _view = gp - "clear": - _final = _view - _view = 0 - - -func set_view_total(v: int) -> void: - _view = v # 探针合成注入面(ui_probe 影子账清零转存断言) - - -func view_total() -> int: - return _view - - -func final_total() -> int: - return _final diff --git a/game/presentation/gp_shadow_ledger.gd.uid b/game/presentation/gp_shadow_ledger.gd.uid deleted file mode 100644 index 496868fd..00000000 --- a/game/presentation/gp_shadow_ledger.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://uk77ei6hjhca diff --git a/game/presentation/hud_presenter.gd b/game/presentation/hud_presenter.gd index fbd0793a..85a75f9f 100644 --- a/game/presentation/hud_presenter.gd +++ b/game/presentation/hud_presenter.gd @@ -23,7 +23,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB # 共账本〔level_runner bonus{atk,max_hp,armor_class}〕;空加算=整节隐藏〔视图内空态纪律〕。 # 呈现只读投影零核心触碰;行文格式=代拍板白名单项) if runner != null and level_active: - var b: Dictionary = runner.bonus + var b: Dictionary[Variant, Variant] = runner.run_ledger.bonus # 迭代 27 T3——加算区住 RunLedger(P1-4 收口) var lines: Array[String] = [] if int(b.get("atk", 0)) != 0: lines.append("攻击 %+d" % int(b.get("atk", 0))) @@ -44,11 +44,11 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB info.set_board_variants([]) # 战斗域角标出战斗即隐(首发批 2026-10-04——空态纪律) _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) return - var s: Dictionary = settlement.presentation_snapshot() + var s: Dictionary[Variant, Variant] = settlement.presentation_snapshot() # 地形变异常驻角标投影(全局作用域变异首发批 2026-10-04——板级变异快照 → 文案行; # 空态隐藏=特性段先例;战斗全程常驻防中局遗忘) info.set_board_variants(board_variant_lines(db, s.get("board_variants", []))) - var hero: Dictionary = s.hero + var hero: Dictionary[Variant, Variant] = s.hero if hero.is_empty() or not bool(hero.get("alive", false)): _refresh_out_of_battle(stage, info, ap_bar, runner, level_active, db) return @@ -60,7 +60,7 @@ static func refresh(stage: BattleStageView, info: HeroInfoPanelView, ap_bar: ApB # ---- B 区主角站桩(HP/状态唯一展示面)+ C 区数据框(攻击/AC) ---- static func _refresh_hero(stage: BattleStageView, info: HeroInfoPanelView, s: Dictionary) -> void: - var hero: Dictionary = s.hero + var hero: Dictionary[Variant, Variant] = s.hero var hp: int = int(hero.hp) var max_hp: int = int(hero.max_hp) var atk: int = int(hero.atk) @@ -102,7 +102,7 @@ static func feat_holdings_lines(db, held: Array) -> Array[String]: return rlines var counts := {} for rid in held: - var rrow: Dictionary = db.feat_row(String(rid)) + var rrow: Dictionary[Variant, Variant] = db.feat_row(String(rid)) var fname := String(rrow.get("display_name", String(rid))) counts[fname] = int(counts.get(fname, 0)) + 1 for fname in counts.keys(): @@ -118,7 +118,7 @@ static func board_variant_lines(db, ids: Array) -> Array[String]: if db == null or db.variation == null: return lines for vid in ids: - var row: Dictionary = db.variation.row_by_id(String(vid)) + var row: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid)) if not row.is_empty(): lines.append(String(row.get("display_name", vid))) lines.append("回复与护甲资源减少") diff --git a/game/presentation/main.gd b/game/presentation/main.gd index 5a0c2d7e..4b219c02 100644 --- a/game/presentation/main.gd +++ b/game/presentation/main.gd @@ -6,7 +6,8 @@ # current_board / current_battle 等——禁链式穿透 settlement.enemy_system.hero 一类路径);调试模式编排抽离 # debug_session.gd(规范 1.1 审视线 + 调试语义单一实现)。 # 组合根(composition root):唯一的模式流转与 core↔view 接线处。 -# 职责边界:① 构建四区视图并接线信号(节点构建归 ui_assembler.gd——规范 1.1,本类只接线) +# 职责边界:① 构建三屏场景并接线信号(屏壳场景 menu_screen/camp_screen/adventure_screen——迭代 27 T1 +# 场景拆分波,视图构建仍归 ui_assembler.gd——规范 1.1,本类只接线;模态层仍归本类程序化装配) # ② 模式管理(正式冒险 AdventureRunner——唯一试玩入口;调试两层编排归 debug_session.gd;单场原语仅探针使用) # ③ 核心写调用只在此处发生(minesweeper.settle_batch / 刷新编排入口 / runner 推进; # E4 收口:加成值零表写——面板百分比经 runner.set_hero_override / debug_session 生效值 Callable @@ -29,8 +30,26 @@ # settlement.action_economy / settlement.db 链式穿透。 # ADR-0001 纪律:呈现层永不直接订阅 排雷玩法/行动经济——只订 战斗结算/runner 转发信号;相位读取仅用于 # 驱动视图状态;信号连接只在组合根(row_pacer/treasure_interact_controller 零连接,只收注入)。 +# 迭代 27 T1(场景拆分+局生命周期物化波——审计 P0-1/P0-2): +# · 三屏场景化——menu_screen/camp_screen/adventure_screen .tscn 屏壳(视图代码原样构建, +# 装配器/视图类零 .tscn 化),本类保留全部接线句柄(ADR-0001 组合根职责),_build_ui 解体为 +# 「三屏实例化 + 模态装配 + 接线」;树序 = [菜单屏, 冒险屏, 模态层...]——屏内两容器无同时可见态, +# 全部模态仍在屏壳之上(视觉共存对逐对验算零漂移——实现报告留痕)。 +# · 会话物化——run_scope.gd 节点持全部局态(adventure/runner/settlement/局态旗/账本), +# _begin_session 创建、会话更替/终局整节点 queue_free(),旧手工复位清单 +# (_reset_run_scope 三项 + _on_battle_end_continue 置空四件)消亡。 +# · SaveManager/MetaProgression 升格 autoload(裁点①甲——RngService 先例:类名加 Core 后缀、 +# autoload 占净名),存档读盘挂 autoload _ready(时序与旧 main._ready 读盘等价)。 extends Control +# autoload 节点路径单点(迭代 27 T6 P2-2 降格实现:审计处方「编译期全局名」与本仓 --script +# 模式编译器不解析裸 autoload 标识符冲突〔run_all.gd 头注+pacing 实证〕——字符串拼写风险 +# 收敛到四常量,类型面由读视图属性承载) +const AUTOLOAD_PATH_CFG := "/root/ConfigDB" +const AUTOLOAD_PATH_RNG := "/root/RngService" +const AUTOLOAD_PATH_SAVE := "/root/SaveManager" +const AUTOLOAD_PATH_META := "/root/MetaProgression" + const UiAssembler := preload("res://presentation/ui_assembler.gd") const Narrator := preload("res://presentation/battle_narrator.gd") const ModalStack := preload("res://presentation/modal_stack.gd") @@ -38,51 +57,42 @@ const TuneSimController := preload("res://presentation/tune_sim_controller.gd") const DebugSessionScript := preload("res://presentation/debug_session.gd") const RowPacer := preload("res://presentation/row_pacer.gd") # 结算行排播泵(T3 拆件批) const TreasureInteractController := preload("res://presentation/treasure_interact_controller.gd") # 秘宝两步交互(T3 拆件批) -const GpShadowLedgerScript := preload("res://presentation/gp_shadow_ledger.gd") # GP 影子账本(T3 拆件批) const CurioInteractController := preload("res://presentation/curio_interact_controller.gd") # 三选一与替换子交互(T4/D4 拆件批) const CampShopPresenter := preload("res://presentation/camp_shop_presenter.gd") # 营地/商店投影(T4/D4 拆件批) -const SaveManagerScript := preload("res://core/save_manager.gd") # 存档承载(preload 避开全局类缓存时序——与 DebugSessionScript 同款) -const MetaProgressionScript := preload("res://core/meta_progression.gd") # 元进度账本服务(元进度实现批——同 preload 纪律) +const MenuScreenScene := preload("res://presentation/scenes/menu_screen.tscn") # 三屏壳场景(迭代 27 T1) +const CampScreenScene := preload("res://presentation/scenes/camp_screen.tscn") +const AdventureScreenScene := preload("res://presentation/scenes/adventure_screen.tscn") var db -var settlement = null # 当前战斗编排器(关卡与调试模式统一读它) -var runner = null # 关卡推进器(冒险模式每关重取 adventure.runner();单关 UI 直入口已退役——LevelRunner 仅存 headless 测试面) -var adventure = null # 冒险结构编排器(冒险结构实现批——三关连探;转发信号照 runner 接线原样对接) -var save_manager = null # 存档序列化承载(存档序列化批——启动读盘/冒险收束写盘;只经访问器与 apply 原语,禁读内部结构) -var meta = null # 元进度账本服务(元进度实现批——余额/定价/购买/工坊解锁/Boss两档入账/注入计算;零信号纯被动) -var _adventure_mode := false # 冒险模式标记(inter 层确认分派与终局分派) -var _debug_entry := false # 调试会话门控(迭代 18 卡⑦——DS_DEBUG=1:启动即开浮窗+两屏角标点亮;_ready 赋值) +var run_scope = null # 会话状态承载(run_scope.gd——迭代 27 T1 物化;_ready 即建闲时态,永非 null) +# SaveManager/MetaProgression 升格 autoload(迭代 27 T1 裁点①)——成员名保留为「autoload 读视图」 +#(属性 getter 经节点路径解析:--script 模式编译器不解析裸 autoload 标识符〔run_all.gd 头注在案〕, +# 节点路径双模式通用;组合根调用点/探针挂载面 main.save_manager 语义零迁移)。 +var save_manager: SaveManagerCore: + get: + return get_node(AUTOLOAD_PATH_SAVE) +var meta: MetaProgressionCore: + get: + return get_node(AUTOLOAD_PATH_META) var logger = null # 试玩日志器(JSONL 持久化 + 卡死诊断转储——呈现层组件) -var _level_active := false +var _debug_entry := false # 调试会话门控(迭代 18 卡⑦——DS_DEBUG=1:启动即开浮窗+两屏角标点亮;_ready 赋值) var _seed_edit: LineEdit var _resume_btn: Button # 继续上局按钮句柄(迭代 8 卡②——有断点才亮) var confirm_dialog: ConfirmDialog # 通用确认弹窗(二轮修改批 A——新冒险互斥确认弹窗化) -var _settlement_stop_timer := ArmedTimer.new() # 卡⑥ 结算框挂起计时(T3/D3 P4 乙——裸布尔暂存机制收口;窗口 0=挂起到显式回落) -var _settlement_stop_pending: bool: # 挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm) - get: - return _settlement_stop_timer.is_armed() - set(v): - if v: - _settlement_stop_timer.arm() - else: - _settlement_stop_timer.disarm() -var _pending_candidates: Array = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷) -var _last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) var curio_overlay: CurioPickOverlay # 提升三选一覆盖层(迭代 10——level-clear.md v2 选择面承载;追加批起奖励节点共用) var _start_overlay: Control var _end_overlay: BattleEndOverlay var _inter_overlay: Control var _inter_label: Label -var level_screen: LevelMapScreen # 关卡画面(独立屏——2026-09-28 形态升格批:激活期战斗屏宿主整体隐藏;零自动推进停点语义不变) -var _battle_root: Control # 战斗屏宿主(B/C/D 三区容器——独立屏激活期 visible=false 级互斥) -var _level_screen_active := false # 独立屏激活状态(覆盖兑付覆盖层在屏域内弹出期间——战斗屏保持隐藏) +var level_screen: LevelMapScreen # 关卡画面句柄(指向 adventure_screen.level_screen——迭代 27 T1 场景拆分) +var _battle_root: Control # 战斗屏宿主句柄(指向 adventure_screen.battle_root——同上) # 调试模式编排(单场/连续两层——状态机与战斗生命周期归 debug_session.gd,本类只接线与出 UI) var _debug = null # 视图(views/)——battle.md 三区(A 区关卡进度条顶栏随 停点前移批 2026-09-23 退役——节点链进度 # 由关卡画面本体选路覆盖层承载;E 区道具面板随可选类删除下线) -var stage: BattleStageView # B 区(含 log_view 明细带) +var stage: BattleStageView # B 区(含 log_view 明细带)——句柄指向 adventure_screen var log_view: BattleLogView var info_panel: HeroInfoPanelView # C 区(攻击/AC 数据框+两预留区——2026-09-26 信息归属拍板精简) var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) @@ -90,25 +100,19 @@ var board_view: BoardView # 探针兼容别名(= board_zone.bo var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) var settlement_fx: SettlementFxView # 结算演出覆盖层(跨区弹道/光带——A–D 之上、模态之下) +# 三屏壳实例(迭代 27 T1——屏成员经本类句柄暴露,探针挂载假设 main.X 一级路径零迁移) +var _menu_screen = null # MenuScreen(开始菜单屏壳) +var _camp_screen = null # CampScreen(营地屏壳) +var _adventure_screen = null # AdventureScreen(冒险屏壳——内含关卡地图/战斗两容器) + # T3 拆件批(2026-10-02)——三件迁出后的组合根持有面(职责见各类头注;装配见 _build_ui) var _row_pacer = null # 结算行排播泵(row_pacer.gd——本类只入队/排空/读队列) var _treasure_ui = null # 秘宝两步交互(treasure_interact_controller.gd——本类只接线与转发意图) var _bonus = null # 三选一与替换子交互(curio_interact_controller.gd——本类只接线与回调注入) var _camp_pres = null # 营地/商店投影(camp_shop_presenter.gd——本类只接线、探针缝保缝转发) -var _gp_ledger := GpShadowLedgerScript.new() # GP 冒险钱包影子账(gp_shadow_ledger.gd——wallet 事件投喂) - -# GP 影子账委托面(探针/终局面板取数保留——ui_probe 合成注入经 set 转发账本,读面即账本实况) -var _adventure_gp_view: int: - get: - return _gp_ledger.view_total() - set(value): - _gp_ledger.set_view_total(value) -var _adventure_gp_final: int: - get: - return _gp_ledger.final_total() # 排播队列只读委托面(探针/测试读 main._row_queue——RowPacer 迁移后保留的同名可读面,共享同一 Array 引用) -var _row_queue: Array: +var _row_queue: Array[Variant]: get: return _row_pacer.pending_rows() @@ -116,82 +120,87 @@ var _row_queue: Array: var legend_overlay: LegendOverlay var node_overlay: NodeOverlay var shop_overlay: ShopOverlay # 商店覆盖层(GP+商店房实施批——商店房节点兑付呈现) -var camp_overlay: CampOverlay # 营地覆盖层(元进度实现批——主菜单营地入口兑付呈现) +var camp_overlay: CampOverlay # 营地覆盖层句柄(指向 camp_screen.camp_overlay——迭代 27 T1) var settings_store # 系统级设置独立存储(迭代 8 卡④——音量/全屏/减动效) var settings_overlay: SettingsOverlay # 设置覆盖层(迭代 8 卡④——最小设置面) var status_panel: StatusPanel # 局内状态面板·只读(迭代 17 卡③——钥石余额+营地三柜台进度查看;登记设置层同档) var career_overlay: CareerOverlay # 生涯页·只读全屏页(迭代 18 卡④——跨局汇总六类九项;登记营地同档〔设置层旁、确认弹窗之前〕) var debug_overlay: DebugOverlay # 调试浮窗(迭代 14.5 调试整合批——种子/数值调整/批量模拟/两层调试入口收一) -var _modals = null # 模态层级状态机(modal_stack.gd——缺陷⑤纪律载体) +var _modal_flow = null # 模态编排纯逻辑类(modal_flow.gd——迭代 27 T4 P1-5/P2-7 抽取;依赖仅 ModalStack+查询 Callable) +var _modals = null # 模态层级状态机(=_modal_flow.stack——探针 _list 登记序断言缝保留) var _tune_sim = null # 调试浮窗批量模拟控制器(tune_sim_controller.gd) -var _hints_shown := {} # 一次性非模态提示去重(每 run 一次:chord/no_adjacent) -var _refresh_used_round := 0 # 本轮已受理刷新次数(遥测 round_refresh_index——每轮 round_started 归零) -var _boss_active := false # Boss 遭遇变体(阶段指示附 Boss 名) -var _was_settling := false # 结算相位上升沿检测(结算轮光带触发——演出链批) - -var _locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放,玩家操作零丢失) +# 结算框挂起态属性化(探针/冒烟直读写接缝语义零变化——读=armed,写=arm/disarm;计时器随会话住 +# run_scope.settlement_stop_timer——迭代 27 T1) +var _settlement_stop_pending: bool: + get: + return run_scope.settlement_stop_timer.is_armed() if run_scope != null else false + set(v): + if run_scope == null: + return + if v: + run_scope.settlement_stop_timer.arm() + else: + run_scope.settlement_stop_timer.disarm() # 锁期点击重放(v3.1):延迟一帧重入唯一动作通道——此刻队列空锁已解,按普通动作受理 #(局面在演播期无变化:逻辑先行、演播只是回放——点击语义保真;_row_queue=RowPacer 委托面, # 排播泵本体已迁 row_pacer.gd——T3 结构段)。 -var _deferred_modal: Control = null # 演播后开窗挂起(T17——胜利/Boss 总结/终局面等排播放完再开) +var _deferred_modal: Control: # 演播后开窗挂起只读视图(T17——t17_defer_probe 挂起态断言缝;本体归 _modal_flow) + get: + return _modal_flow._deferred if _modal_flow != null else null # 演播后排窗(T17 用户令「伤害过程演完才切结算窗口」):排播队列在播=挂起,演播完边沿 # (_on_pump_ended 派发)再开——旧路径 _open_modal 前置 flush 会把击杀轮的伤害/死亡演出 # 同帧倒掉(「直接跳战斗胜利」缺陷本体)。队列空=立即开(行为同旧路径)。 func _open_modal_after_playback(m: Control) -> void: - if _row_pacer.pending_rows().is_empty(): - _open_modal(m) - return - _deferred_modal = m - m.visible = false # 挂起含视觉(open_* 已置 visible=true——不压掉则窗在排播期照会上屏,延迟形同虚设) + _modal_flow.open_after_playback(m) # 迭代 27 T4——演播后排窗挂起编排归 ModalFlow(T17 语义逐字随迁) -# 演播完边沿派发(row_pacer.pump_ended 注入口——原直连锁期点击重放,现统一派发): -# 先开挂起 modal(胜利窗接管呈现),后锁期点击重放——modal 开启后 _input_locked 吞掉 -# 重放点击(战斗已收场,重放无意义)。 +# 演播完边沿派发(row_pacer.pump_ended 注入口——先开挂起 modal,后锁期点击重放): func _on_pump_ended() -> void: - if _deferred_modal != null: - var m: Control = _deferred_modal - _deferred_modal = null - _open_modal(m) - return - _replay_locked_click() + _modal_flow.pump_ended() # 迭代 27 T4——派发编排归 ModalFlow(重放经注入 Callable 回本类) func _replay_locked_click() -> void: - if _locked_click.is_empty() or not _row_queue.is_empty(): + if run_scope.locked_click.is_empty() or not _row_queue.is_empty(): return - var click: Dictionary = _locked_click - _locked_click = {} + var click: Dictionary[Variant, Variant] = run_scope.locked_click + run_scope.locked_click = {} _on_board_action(click["cell"], click["kind"]) +# 会话生命周期(迭代 27 T1——P0-2 物化):_ready 建闲时态(永非 null——全组合根零空判责), +# 开局/续玩/调试两层各建新会话,旧会话整节点 queue_free()(帧末回收;新节点先挂树无真空窗)。 +func _begin_session(adventure_session: bool) -> void: + if run_scope != null: + run_scope.queue_free() + run_scope = RunScope.new() + run_scope.is_adventure = adventure_session + add_child(run_scope) + + func _ready() -> void: - db = get_node("/root/ConfigDB") + db = get_node(AUTOLOAD_PATH_CFG) if not db.is_ready: var ok: bool = db.load_tables("res://data/tables", false) if not ok: _fatal_screen("数值表加载失败——见输出错误清单(fail-fast)") return - # 存档读盘(存档序列化批):拒读/损坏均已在 SaveManager 内部兜底,失败不阻断启动 - # (fatal 只留给数值表——fail-fast 边界在表加载,不在档);高版本拒读态后续写盘被类内拒绝。 - save_manager = SaveManagerScript.new() - save_manager.load_from_disk() + # SaveManager/MetaProgression 升格 autoload(迭代 27 T1 裁点①):读盘已在各自 _ready 完成 + #(autoload 先于主场景就绪——时序与旧 main._ready 读盘等价),此处零动作。 # 系统级设置(迭代 8 卡④——独立存储不随存档):启动即恢复音量/全屏/减动效引擎态 settings_store = SettingsStore.new() settings_store.load_settings() settings_store.apply() - # 元进度账本(元进度实现批):存档就绪后建——余额/定价/购买/入账/注入全经此单点 - meta = MetaProgressionScript.new(db, save_manager) - _debug = DebugSessionScript.new(db, get_node("/root/RngService"), - Callable(self, "_debug_hero_override"), Callable(self, "_seed_value")) + _debug = DebugSessionScript.new(db, get_node(AUTOLOAD_PATH_RNG), + _debug_hero_override, _seed_value) _debug.battle_created.connect(_on_debug_battle_created) _debug.battle_ready.connect(_on_debug_battle_ready) _build_ui() + _begin_session(false) # 闲时会话(gp_ledger 等探针注入面自 _ready 起恒在——ui_probe 合成段时序) _refresh_resume_button() # 迭代 8 卡②——启动读盘后按断点在档态点亮「继续上局」 _start_overlay.visible = true _debug_entry = OS.get_environment("DS_DEBUG") == "1" # 迭代 18 卡⑦ P3 甲——DS_DEBUG=1 调试会话门控 @@ -199,20 +208,27 @@ func _ready() -> void: _show_debug_overlay() # 启动即开(手动可关、两屏角标可再开——P3 甲) -# ==================== 视图接线(battle.md 四区布局 1280×800;节点构建归 UiAssembler) ==================== +# ==================== 视图接线(三屏壳 + 模态层装配;构建归屏壳/UiAssembler,本类只接线) ==================== func _build_ui() -> void: - var root := VBoxContainer.new() - add_child(root) - _battle_root = root # 独立屏互斥面(2026-09-28 形态升格批) - var battle_ui: Dictionary = UiAssembler.build_battle_ui(root, db) - stage = battle_ui.stage - log_view = battle_ui.log_view - info_panel = battle_ui.info_panel - board_zone = battle_ui.board_zone - board_view = battle_ui.board_view # 探针兼容别名(= board_zone.board_view) - ap_bar = battle_ui.ap_bar # 探针兼容别名(= board_zone.ap_bar) - settlement_fx = battle_ui.settlement_fx + # 三屏壳实例化(迭代 27 T1 场景拆分):树序 = [菜单屏, 冒险屏, 模态层...]——屏壳全在模态之下。 + # 屏内 level_screen 与 battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后零视觉差; + # 视觉共存对逐对验算:legend/battle(图例战斗中开——legend 在冒险屏壳之后=其上 ✓)、 + # debug/level_screen(浮窗最后子=其上 ✓)、shop/level_screen(商店节点域弹出——模态均在屏壳上 ✓)。 + _menu_screen = MenuScreenScene.instantiate() + add_child(_menu_screen) + _adventure_screen = AdventureScreenScene.instantiate() + add_child(_adventure_screen) + _start_overlay = _menu_screen.start_overlay + _battle_root = _adventure_screen.battle_root + stage = _adventure_screen.stage + log_view = _adventure_screen.log_view + info_panel = _adventure_screen.info_panel + board_zone = _adventure_screen.board_zone + board_view = _adventure_screen.board_view # 探针兼容别名(= board_zone.board_view) + ap_bar = _adventure_screen.ap_bar # 探针兼容别名(= board_zone.ap_bar) + settlement_fx = _adventure_screen.settlement_fx + level_screen = _adventure_screen.level_screen # 结算行排播泵(T3 拆件批——迁 row_pacer.gd;本类注入视图句柄与呈现刷新回调,零信号连接) _row_pacer = RowPacer.new() @@ -227,7 +243,7 @@ func _build_ui() -> void: # 秘宝两步交互(T3 拆件批——迁 treasure_interact_controller.gd;settlement/runner 随关卡更替, # 经现值回调注入——仿 identity_resolver 只读回调先例) _treasure_ui = TreasureInteractController.new(db, meta, info_panel, board_zone.toast, board_view, - func() -> Variant: return settlement, func() -> Variant: return runner) + func() -> Variant: return run_scope.settlement, func() -> Variant: return run_scope.runner) board_zone.action_requested.connect(_on_board_action) board_zone.status_badge.pressed.connect(_on_status_badge_pressed) # 状态面板入口(迭代 17 卡③——战斗屏 HUD 角标) @@ -249,18 +265,18 @@ func _build_ui() -> void: logger.ap_per_round = int(db.action_economy.ap_per_round) # 模态覆盖层(登记序 = 树序 = z 遮挡序:图例 < 明细回看 < 节点 < 关卡画面 < 三选一 < 终局 - # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读·迭代 18 卡④) < 确认弹窗 < 调试浮窗) - var overlays: Dictionary = UiAssembler.build_overlays(self, db) + # < 商店 < 营地 < 设置 < 状态面板(只读) < 生涯页(只读·迭代 18 卡④) < 确认弹窗 < 调试浮窗; + # 关卡画面住冒险屏壳(树序在模态之下)——登记序仍夹在节点层与三选一层之间,遮挡语义零改动) + var overlays: Dictionary[Variant, Variant] = UiAssembler.build_overlays(self, db) legend_overlay = overlays.legend_overlay node_overlay = overlays.node_overlay _end_overlay = overlays.end_overlay shop_overlay = overlays.shop_overlay - camp_overlay = overlays.camp_overlay - level_screen = LevelMapScreen.new() - add_child(level_screen) - # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——关卡画面须夹在节点层与商店层之间 - #(兑付覆盖层在本屏域内弹出:模态栈打开低层时自动隐藏高层本屏、关闭自动恢复——零机制改动复用) - move_child(level_screen, node_overlay.get_index() + 1) + _camp_screen = CampScreenScene.instantiate() + add_child(_camp_screen) + camp_overlay = _camp_screen.camp_overlay + # 树序 = 模态登记序 = z 遮挡序(缺陷⑤纪律)——营地层须夹在商店层与设置层之间 + move_child(_camp_screen, shop_overlay.get_index() + 1) legend_overlay.closed.connect(_on_modal_closed.bind(legend_overlay)) node_overlay.continue_pressed.connect(_on_node_continue) level_screen.route_chosen.connect(_on_route_chosen) @@ -285,30 +301,29 @@ func _build_ui() -> void: # 提升三选一覆盖层(迭代 10——z 序 = 节点层之上、终局层之下;确认后同拍推进) curio_overlay = CurioPickOverlay.new() add_child(curio_overlay) + move_child(curio_overlay, _end_overlay.get_index()) # 提升三选一与替换子交互(T4/D4 拆件批——迁 curio_interact_controller.gd;本类只接线与回调注入, # ADR-0001——模态栈/演播后排窗/呈现刷新/fatal 经回调注入,adventure/runner 现值回调) - _bonus = CurioInteractController.new(db, func() -> Variant: return meta, curio_overlay, _end_overlay, board_view, - board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return adventure, - func() -> Variant: return runner, func() -> Variant: return _adventure_mode, + _bonus = CurioInteractController.new(CurioFlowContext.new(db, func() -> Variant: return meta, curio_overlay, _end_overlay, board_view, + board_zone.toast, func() -> Variant: return save_manager, func() -> Variant: return run_scope.adventure, + func() -> Variant: return run_scope.runner, func() -> Variant: return run_scope.is_adventure, _open_modal, _open_modal_after_playback, _close_modal, _refresh_presentations, - _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": _last_gain_amount})) + _fatal_screen, func() -> String: return _gp_summary_text({"gp_dropped": run_scope.last_gain_amount}))) # 迭代 27 T4——构造参数收拢 context(P1-5) curio_overlay.pick_confirmed.connect(_bonus.on_pick_confirmed) curio_overlay.continue_pressed.connect(_bonus.on_continue) curio_overlay.refresh_requested.connect(_bonus.on_refresh) - move_child(curio_overlay, _end_overlay.get_index()) - var start: Dictionary = UiAssembler.build_start_overlay(self, db) - _start_overlay = start.overlay - _resume_btn = start.resumebtn - start.lvbtn.pressed.connect(_start_adventure_from_menu) - start.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口 + # 菜单屏接线(原 build_start_overlay 返回值接线段——句柄经屏壳暴露) + _menu_screen.lvbtn.pressed.connect(_start_adventure_from_menu) + _menu_screen.resumebtn.pressed.connect(_resume_from_menu) # 迭代 8 卡②——续玩入口 + _resume_btn = _menu_screen.resumebtn # 调试角标入口退役(迭代 18 卡⑦ P2 甲——迁宿主:战斗屏 HUD+关卡地图屏各一枚, # 接线见两屏装配段;开始层不再有调试入口,DS_DEBUG=1 会话=启动即开浮窗) # 营地/商店投影(T4/D4 拆件批——迁 camp_shop_presenter.gd;本类只接线、探针缝保缝转发; # 购买写调用唯一经 MetaProgression/LevelRunner core 单口——组合根 lambda 现值注入) - _camp_pres = CampShopPresenter.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay, - board_zone.toast, _start_overlay, logger, func() -> Variant: return runner, - _open_modal, _close_modal, _refresh_presentations, _refresh_resume_button) + _camp_pres = CampShopPresenter.new(CampFlowContext.new(db, func() -> Variant: return meta, func() -> Variant: return save_manager, camp_overlay, shop_overlay, + board_zone.toast, _start_overlay, logger, func() -> Variant: return run_scope.runner, + _open_modal, _close_modal, _refresh_presentations, _refresh_resume_button)) # 迭代 27 T4——构造参数收拢 context(P1-5) shop_overlay.buy_requested.connect(_camp_pres.on_shop_buy) shop_overlay.refresh_requested.connect(_camp_pres.on_shop_refresh) # MC-1 shop_overlay.leave_pressed.connect(_camp_pres.on_shop_leave) @@ -320,12 +335,12 @@ func _build_ui() -> void: camp_overlay.forge_unlock_requested.connect(_camp_pres.on_camp_forge_unlock) camp_overlay.forge_select_requested.connect(_camp_pres.on_camp_forge_select) camp_overlay.closed.connect(_camp_pres.on_camp_closed) - start.campbtn.pressed.connect(_camp_pres.open_camp) - start.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组) - start.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 - start.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 + _menu_screen.campbtn.pressed.connect(_camp_pres.open_camp) + _menu_screen.careerbtn.pressed.connect(_open_career) # 迭代 18 卡④——生涯页一级入口(营地同组) + _menu_screen.settingsbtn.pressed.connect(_open_settings) # 迭代 8 卡④——最小设置面入口 + _menu_screen.quitbtn.pressed.connect(_quit_game) # 二轮修改批 D1——退出游戏提级一级菜单 - var inter: Dictionary = UiAssembler.build_inter_overlay(self) + var inter: Dictionary[Variant, Variant] = UiAssembler.build_inter_overlay(self) _inter_overlay = inter.overlay _inter_label = inter.label inter.next_btn.pressed.connect(_on_inter_next_pressed) @@ -353,11 +368,26 @@ func _build_ui() -> void: debug_overlay.debug_levels_requested.connect(_start_debug_levels) debug_overlay.diagnose_requested.connect(_on_diagnose_pressed) # 迭代 26 T5b——诊断转储入口随迁(原角落钮连接) - # 模态登记(缺陷⑤纪律——登记序 = 树序:含 move_child 调位后的关卡画面/三选一终位; + # 模态登记(缺陷⑤纪律——登记序 = 树序:关卡画面住冒险屏壳但登记位仍夹在节点层与三选一层之间; # 状态面板设置层同档、确认弹窗之前——P2 甲;生涯页同档随状态面板之后(迭代 18 卡④ P3 甲)) - _modals = ModalStack.new() - for m: Control in [legend_overlay, node_overlay, level_screen, curio_overlay, _end_overlay, shop_overlay, camp_overlay, settings_overlay, status_panel, career_overlay, confirm_dialog, debug_overlay]: - _register_modal(m) + # 模态编排注入(迭代 27 T4——ModalFlow 纯逻辑类:依赖仅 ModalStack+查询 Callable,P1-5 抽取) + _modal_flow = ModalFlow.new() + _modal_flow.start_visible_get = func() -> bool: return _start_overlay.visible + _modal_flow.inter_visible_get = func() -> bool: return _inter_overlay.visible + _modal_flow.settlement_get = func() -> Variant: return run_scope.settlement + _modal_flow.rows_empty_get = func() -> bool: return _row_pacer.pending_rows().is_empty() + _modal_flow.flush_rows = _flush_row_queue + _modal_flow.on_open_sync = func() -> void: + _sync_status_entry() # 迭代 17 卡③——状态面板入口角标可见性/置灰随开态同步 + _sync_debug_overlay() # 迭代 18 卡⑦——浮窗当局数据/按钮态随开态同步 + _modal_flow.on_close_sync = func() -> void: + _sync_status_entry() # 迭代 17 卡③——关闭恢复角标可点态(无残留——R3 同源口径) + _sync_debug_overlay() # 迭代 18 卡⑦——他层关闭复活浮窗时同步刷新(P1 甲关后自动恢复语义配套) + _modal_flow.replay_click = _replay_locked_click + _modals = _modal_flow.stack # 探针 _list 登记序断言缝(ui_struct 登记序=z 序纪律断言) + # 排除组随层登记(迭代 27 T4 P2-7——non_locking=调试浮窗〔迭代 18 卡⑦ T2 防线〕/screen_state=关卡地图屏) + for entry: Array in [[legend_overlay, []], [node_overlay, []], [level_screen, [ModalStack.TAG_SCREEN_STATE]], [curio_overlay, []], [_end_overlay, []], [shop_overlay, []], [camp_overlay, []], [settings_overlay, []], [status_panel, []], [career_overlay, []], [confirm_dialog, []], [debug_overlay, [ModalStack.TAG_NON_LOCKING]]]: + _register_modal(entry[0], entry[1]) _tune_sim = TuneSimController.new(db, debug_overlay, int(db.hero.max_hp)) @@ -369,53 +399,36 @@ func _fatal_screen(msg: String) -> void: # ==================== 模态 overlay 层级管理(pattern 6 / 缺陷⑤——状态机归 modal_stack.gd) ==================== -func _register_modal(m: Control) -> void: +func _register_modal(m: Control, tags: Array = []) -> void: # 撑满宿主(Control 默认锚 = 0×0——全屏变暗层与居中布局整体失效;route 选路层预览截图实证后 # 统一收口:六个模态同病,探针经信号直发绕过真实点击从未暴露。anchors_and_offsets 而非 anchors # ——后者会把控件重置回最小内容尺寸,ui_assembler 头注同款纪律) m.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) - _modals.register(m) + _modal_flow.register(m, tags) # 排除组随层登记(迭代 27 T4 P2-7——non_locking=调试浮窗/screen_state=关卡地图屏) func _open_modal(m: Control) -> void: - # 挂起兜底(T17 演播后排窗):其他路径(如回看)在播期排空队列会使演播完边沿不再触发—— - # 此处先结挂起 modal 防永不兑现(m==挂起对象时跳过=本调用自身)。 - if _deferred_modal != null and m != _deferred_modal: - var dm: Control = _deferred_modal - _deferred_modal = null - _open_modal(dm) - # 打开时显式隐藏树序在其上方的可见层(遮挡物隐藏),关闭恢复——单一模态栈 - _flush_row_queue() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行) - _modals.open(m) - _sync_status_entry() # 迭代 17 卡③——状态面板入口角标可见性/置灰随开态同步 - _sync_debug_overlay() # 迭代 18 卡⑦——浮窗当局数据/按钮态随开态同步(T3 刷新=模态开合信号驱动) - + _modal_flow.open(m) # 迭代 27 T4——挂起兜底/排播排空/遮挡记账/开侧同步编排归 ModalFlow(P1-5 抽取) func _close_modal(m: Control) -> void: - _modals.close(m) - _sync_status_entry() # 迭代 17 卡③——关闭恢复角标可点态(无残留——R3 同源口径) - _sync_debug_overlay() # 迭代 18 卡⑦——他层关闭复活浮窗时同步刷新(P1 甲关后自动恢复语义配套) - + _modal_flow.close(m) # 关闭恢复+关侧同步归 ModalFlow(同上) func _on_modal_closed(m: Control) -> void: _close_modal(m) func _input_locked() -> bool: - # 模态/非战斗期棋盘输入锁(T3 收口批——开态查询归 modal_stack.has_open(),登记册单一来源, - # 不再手工罗列各层;例外三件:开始层/过场层(非模态全屏态,不在登记册)与战斗缺位(棋盘输入无载体)) - # 迭代 18 卡⑦ T2 防护(审计🔴):判据改排除式——调试浮窗对局中可开(P2 甲),其开态不计入锁 - #(否则 DS_DEBUG 默认开×他层关闭复活浮窗×has_open 组合=棋盘永久锁死——S1+ 停机点防线) - return _modals.has_open_excluding(debug_overlay) or _start_overlay.visible or _inter_overlay.visible or settlement == null - + # 模态/非战斗期棋盘输入锁(迭代 27 T4——判据编排归 ModalFlow.locks_input:互斥域随层登记 + # [non_locking=调试浮窗,迭代 18 卡⑦ T2 防线]+ 三例外经查询 Callable 注入;本函数留探针缝) + return _modal_flow.locks_input() # ==================== 冒险模式入口(唯一试玩入口——单关测试直入口退役 2026-09-23 用户拍板)==================== func _start_adventure_from_menu() -> void: # 互斥确认弹窗化(二轮修改批 A——用户目检裁决「弹窗提示」):有断点时开新冒险弹实底确认框, # 「开新冒险」提交(弃旧断点+写盘)/「取消」回菜单 - if save_manager != null and save_manager.has_breakpoint(): - var bp: Dictionary = save_manager.breakpoint_snapshot() + if save_manager.has_breakpoint(): + var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() confirm_dialog.open("已有未完冒险(第 %d 关)——开新冒险将覆盖旧断点" % maxi(1, int(bp.get("level_index", 1))), "开新冒险", "取消") return @@ -425,9 +438,8 @@ func _start_adventure_from_menu() -> void: func _on_confirm_new_adventure() -> void: confirm_dialog.close() - if save_manager != null: - save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) - save_manager.write_to_disk() + save_manager.clear_breakpoint() # 提交即弃旧断点(覆盖语义对盘面即时生效) + save_manager.write_to_disk() _start_overlay.visible = false _start_adventure() @@ -445,32 +457,32 @@ func _quit_game() -> void: # 继续上局按钮可见性 = 断点在档态(迭代 8 卡②——启动读盘后与每次回开始菜单刷新) func _refresh_resume_button() -> void: - if _resume_btn == null or save_manager == null: + if _resume_btn == null: return var has: bool = save_manager.has_breakpoint() _resume_btn.visible = has if has: - var bp: Dictionary = save_manager.breakpoint_snapshot() + var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() _resume_btn.text = "▶ 继续上局(第 %d 关 · 自最近停点恢复)" % maxi(1, int(bp.get("level_index", 1))) # 继续上局(迭代 8 卡②——卡① resume_adventure 的消费入口):断点重建至最近停点 func _resume_from_menu() -> void: - if save_manager == null or not save_manager.has_breakpoint(): + if not save_manager.has_breakpoint(): return - var bp: Dictionary = save_manager.breakpoint_snapshot() + var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() _close_modal(debug_overlay) # 开冒险路径显式关浮窗(迭代 18 卡⑦ T2 防护——续玩同口径) _start_overlay.visible = false + _begin_session(true) _wire_adventure() - _level_active = true + run_scope.level_active = true _sync_status_entry() # 迭代 17 卡③——续玩恢复点亮状态面板入口角标 level_screen.set_abandon_available(true) - _reset_run_scope() log_view.clear() logger.start_session(int(bp.get("master_seed", 0))) - adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 迭代 19 卡⓪①——同调三键随续玩下传(build_adventure_params 统一构造,与开新局同源同口径) - if not adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, + if not run_scope.adventure.resume_adventure(bp, build_adventure_params(save_manager, meta, {"treasure_unlocked": _treasure_ui.unlocked_set()})): _fatal_screen("续玩恢复失败——见输出日志") return @@ -478,20 +490,19 @@ func _resume_from_menu() -> void: # 冒险编排构建+接线(开新局/续玩共用——信号连接只在组合根,ADR-0001;断点注入同口) func _wire_adventure() -> void: - adventure = AdventureRunner.new(db, get_node("/root/RngService")) - _adventure_mode = true - adventure.step_row.connect(_on_step_row) - adventure.round_started.connect(_on_round_started) - adventure.board_changed.connect(_on_board_changed) - adventure.node_entered.connect(_on_runner_node) - adventure.choices_presented.connect(_on_choices_presented) - adventure.wallet_changed.connect(_on_wallet_changed) + run_scope.adventure = AdventureRunner.new(db, get_node(AUTOLOAD_PATH_RNG)) + run_scope.adventure.step_row.connect(_on_step_row) + run_scope.adventure.round_started.connect(_on_round_started) + run_scope.adventure.board_changed.connect(_on_board_changed) + run_scope.adventure.node_entered.connect(_on_runner_node) + run_scope.adventure.choices_presented.connect(_on_choices_presented) + run_scope.adventure.wallet_changed.connect(_on_wallet_changed) # 迭代 10 奖励重构批——big_reward_paid 退役,三选一信号族接线 - adventure.curio_choice_presented.connect(_bonus.on_presented) - adventure.curio_resolved.connect(_bonus.on_resolved) - adventure.adventure_finished.connect(_on_adventure_finished) - adventure.level_started.connect(_on_adventure_level_started) - adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) + run_scope.adventure.curio_choice_presented.connect(_bonus.on_presented) + run_scope.adventure.curio_resolved.connect(_bonus.on_resolved) + run_scope.adventure.adventure_finished.connect(_on_adventure_finished) + run_scope.adventure.level_started.connect(_on_adventure_level_started) + run_scope.adventure.set_save_manager(save_manager) # 断点承载注入(迭代 8 卡①——停点快照写盘;调试/模拟不经过此路径) # 冒险开局参数面构造(主菜单开新/续玩两入口共用——迭代 19 卡⓪①:同调三键随开局下传,真实局此前恒空。 @@ -503,7 +514,7 @@ func _wire_adventure() -> void: # 注:runner 侧 resume_adventure 受理面现收 attunement/attune_refresh_y/tree_rules 三键 # (2026-10-08 尾裁微批b 卡⓪受理闭环——tree_rules 受理补齐,传参/受理两侧对齐,披露句退役)。 static func build_adventure_params(save_manager_ref, meta_ref, extra: Dictionary = {}) -> Dictionary: - var params: Dictionary = { + var params: Dictionary[Variant, Variant] = { "hero_override": meta_ref.hero_override() if meta_ref != null else {}, "attack_type": meta_ref.hero_attack_type() if meta_ref != null else "", "armor_type": meta_ref.hero_armor_type() if meta_ref != null else "", @@ -519,20 +530,20 @@ static func build_adventure_params(save_manager_ref, meta_ref, extra: Dictionary # 信号连接只在组合根——ADR-0001) func _start_adventure() -> void: _close_modal(debug_overlay) # 开冒险路径显式关浮窗(迭代 18 卡⑦ T2 防护——DS_DEBUG 默认开态下开局不带窗;对局中经角标再开) + _begin_session(true) _wire_adventure() - _level_active = true + run_scope.level_active = true _sync_status_entry() # 迭代 17 卡③——进对局点亮状态面板入口角标 level_screen.set_abandon_available(true) # 放弃入口挂选路停点(天然非战斗期——2026-09-21 用户拍板) - _reset_run_scope() log_view.clear() var seed_val := _seed_value() logger.start_session(seed_val) - adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) + run_scope.adventure.set_hero_override(debug_overlay.atk_pct(), debug_overlay.hp_pct()) # 永久强化注入(元进度实现批):冒险开始参数面——meta 现算 override 显式传参(全零 = 白板) # 首次冒险脚本(关卡节点图生成 v0.4):存档布尔位 false = 全关固定模板 0(教学重放);置位后逐关抽签 # 迭代 19 卡⓪①——同调三键随开局下传(build_adventure_params 统一构造,与营地投影同源) var first_script: bool = not save_manager.is_first_adventure_done() - if not adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, + if not run_scope.adventure.start_adventure(seed_val, build_adventure_params(save_manager, meta, {"first_script": first_script, "treasure_unlocked": _treasure_ui.unlocked_set(), "shop_mode": "pool"})): _fatal_screen("冒险启动失败——见输出日志") return @@ -541,35 +552,28 @@ func _start_adventure() -> void: # 每关开始(冒险模式):重取 runner 实例 + 层名行(L1 与单关模式同序——层名行随首战横幅后到达; # 停点前移批 2026-09-23:本回调返回后才有 choices_presented 到达——入口选路层随即弹出) func _on_adventure_level_started(level_index: int, display_name: String) -> void: - runner = adventure.runner() + run_scope.runner = run_scope.adventure.runner() # 迭代 19 卡⓪②——开场行改传当前生效值(runner.effective_atk/effective_max_hp 与战斗面板同源: # 表值+永久强化 flat+加算区 flat 后乘调试浮窗 pct,本回调时点 runner 已带全部加成);「含面板加成」 # 后缀判断同步改认局外加成——永久强化 atk/max_hp flat 或调试浮窗 pct 任一存在才带,白板局不带 - var ov: Dictionary = meta.hero_override() if meta != null else {} - log_view.append_bb(Narrator.level_open_line(display_name, int(adventure.master_seed), - runner.effective_atk(), runner.effective_max_hp(), + var ov: Dictionary[Variant, Variant] = meta.hero_override() + log_view.append_bb(Narrator.level_open_line(display_name, int(run_scope.adventure.master_seed), + run_scope.runner.effective_atk(), run_scope.runner.effective_max_hp(), int(ov.get("atk", 0)) > 0 or int(ov.get("max_hp", 0)) > 0 or debug_overlay.atk_pct() > 0 or debug_overlay.hp_pct() > 0)) -func _reset_run_scope() -> void: - # 每 run 一次的提示去重与轮内计数(弃图刷新遥测轮内序号) - _hints_shown = {} - _refresh_used_round = 0 - _boss_active = false - - func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 冒险模式:runner 逐关更替——每次节点事件重取(单关模式幂等);须先于 board_changed 回退读面 - if _adventure_mode and adventure != null: - runner = adventure.runner() + if run_scope.is_adventure and run_scope.adventure != null: + run_scope.runner = run_scope.adventure.runner() var t := String(node.get("type", "")) if t == "battle" or t == "boss": - settlement = runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) - logger.attach_battle(settlement) - _boss_active = t == "boss" + run_scope.settlement = run_scope.runner.current_battle() # 呈现层统一读 settlement(与调试模式共用接线) + logger.attach_battle(run_scope.settlement) + run_scope.boss_active = t == "boss" _show_board_fx_for_node(t) - log_view.append_bb(Narrator.node_banner(runner, node, t)) + log_view.append_bb(Narrator.node_banner(run_scope.runner, node, t)) elif t == "reward": # 奖励节点三选一(迭代 10 追加批——与通关三选一共享同池同权重同上限;CurioPickOverlay 共用面)。 # T4/D4 拆件:呈现编排归 curio_interact_controller.present_node_curio(_curio_context 随迁)。 @@ -585,15 +589,15 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 购买/离开归玩家意图(buy_requested/leave_pressed 接线),零自动推进。 # pool 面 = 共享池货架三选一同构卡(迭代 11 第②批 T4);legacy 面 = 固定三件(off 对照承载) _open_modal(shop_overlay) - if runner.is_shop_pool(): - shop_overlay.open_shelf(runner.shop_shelf(), int(runner.gp)) - shop_overlay.set_visit_info(int(runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行 - shop_overlay.set_refresh_display(runner.shop_refresh_free_left(), runner.shop_refresh_cost(), int(runner.gp) >= runner.shop_refresh_cost()) # MC-1 + if run_scope.runner.is_shop_pool(): + shop_overlay.open_shelf(run_scope.runner.shop_shelf(), int(run_scope.runner.gp)) + shop_overlay.set_visit_info(int(run_scope.runner.shop_bp_visit_buys()), float(db.shop_price_a())) # T6 爬价规则行 + shop_overlay.set_refresh_display(run_scope.runner.shop_refresh_free_left(), run_scope.runner.shop_refresh_cost(), int(run_scope.runner.gp) >= run_scope.runner.shop_refresh_cost()) # MC-1 else: - shop_overlay.open_shop(runner.shop_products(), int(runner.gp)) + shop_overlay.open_shop(run_scope.runner.shop_products(), int(run_scope.runner.gp)) shop_overlay.set_visit_info(-1, 1.0) # legacy 面无爬价行(off 对照承载) - log_view.append_bb(Narrator.shop_line(int(runner.gp))) - logger.log_node(node, info, runner.node_index) + log_view.append_bb(Narrator.shop_line(int(run_scope.runner.gp))) + logger.log_node(node, info, run_scope.runner.node_index) _refresh_presentations() @@ -601,9 +605,9 @@ func _on_runner_node(node: Dictionary, info: Dictionary) -> void: # 冒险结算(completed/failed/abandoned):复用 BattleEndOverlay——摘要五件事 + 钥石入账明细进文案 func _on_adventure_finished(summary: Dictionary) -> void: - _level_active = false + run_scope.level_active = false var result: String = String(summary.get("result", "")) - settlement = null + run_scope.settlement = null _exit_level_screen() # 终局兜底:独立屏在开态(放弃入口)也先回战斗屏呈现摘要——屏级互斥单一出口 _sync_status_entry() # 迭代 17 卡③——离局熄灭状态面板入口角标(防菜单误现) _sync_settlement_visuals() # → 全关 @@ -611,19 +615,18 @@ func _on_adventure_finished(summary: Dictionary) -> void: # 存档入账(元进度实现批):settle = Boss 两档入账 + 工坊解锁 + 摘要入档 + 写盘(唯一写档点不变—— # 调试两层不经过本处理器,天然不写档不污染生涯数据)。 var detail: Dictionary[String, Variant] = {} - if meta != null: - detail.assign(meta.settle_adventure(summary)) + detail.assign(meta.settle_adventure(summary)) var text := Narrator.adventure_summary_text(summary) if not detail.is_empty(): text += "\n" + Narrator.keystone_settle_text(detail, _bonus.item_name_lookup()) if result == "completed": _end_overlay.open_victory(text, "返回冒险", board_view.get_global_rect(), - "本局 GP 入账 · 合计 %d" % _adventure_gp_final) # 卡⑩ GP 行;钥石明细已在 text(keystone_settle_text)——M9 三行账 + "本局 GP 入账 · 合计 %d" % int(summary.get("gp_final", 0))) # 卡⑩ GP 行(迭代 27 T3——取数=summary gp_final 清账前快照,影子账已退役);钥石明细已在 text(keystone_settle_text)——M9 三行账 else: _end_overlay.open_defeat(text, "返回", board_view.get_global_rect()) logger.log_level_end(summary) # 试玩时间线:冒险终局(摘要随载荷) - if runner != null: - runner.restore_hero_base() + if run_scope.runner != null: + run_scope.runner.restore_hero_base() _open_modal_after_playback(_end_overlay) _refresh_presentations() @@ -639,18 +642,15 @@ func _on_battle_end_continue() -> void: if _settlement_stop_pending: _settlement_stop_pending = false _enter_level_screen() - level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, runner.feats_held()))) - level_screen.open_choices(_pending_candidates, runner) - _pending_candidates = [] + level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()))) + level_screen.open_choices(run_scope.pending_candidates, run_scope.runner) + run_scope.pending_candidates = [] _refresh_presentations() return + # 会话收束(迭代 27 T1 物化):旧手工置空清单(runner/settlement/adventure/_adventure_mode + + # _reset_run_scope 三项)整节点 queue_free() 消亡——新闲时会话即建,局态旗/账本/模态挂起全复位 _debug.reset() - if runner != null: - runner.restore_hero_base() - runner = null - settlement = null - adventure = null - _adventure_mode = false + _begin_session(false) debug_overlay.reset_sliders() _start_overlay.visible = true _refresh_resume_button() # 迭代 8 卡②——回菜单按断点在档态刷新(收束后已清=隐藏) @@ -660,37 +660,37 @@ func _on_battle_end_continue() -> void: func _start_debug_levels() -> void: # 守卫(迭代 18 卡⑦ T2——浮窗对局中可开后,调试对局与活跃冒险/结算并存不开放:并发会清写 # settlement/编排态=状态机破坏;随开随收逻辑改向——对局中开窗只读浮窗,不再视为开局前置) - if _adventure_mode or settlement != null: + if run_scope.is_adventure or run_scope.settlement != null: return debug_overlay.reset_sliders() _close_modal(debug_overlay) # 浮窗内入口发起——随开随收(对局期不可再开:门禁在 _show_debug_overlay) _start_overlay.visible = false log_view.clear() log_view.append_bb(Narrator.debug_open_line(_seed_value(), 1, true)) - _reset_run_scope() + _begin_session(false) if not _debug.start_debug_levels(): _fatal_screen("战斗启动失败——见输出日志") -func _on_debug_battle_created(settlement_ref, _level: int, _carry: int) -> void: +func _on_debug_battle_created(settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: # begin_battle 前接手 + 接线(ADR-0001:signal.connect 唯一归组合根)。settlement 必须先于 begin_battle # 就位——begin 期间发出的 board_changed 要用新板重建棋盘并触发一次性提示(缺陷①同款时序义务) - settlement = settlement_ref + run_scope.settlement = settlement_ref _wire_battle(settlement_ref) -func _on_debug_battle_ready(settlement_ref, _level: int, _carry: int) -> void: - settlement = settlement_ref - board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper) +func _on_debug_battle_ready(settlement_ref: BattleOrchestrator, _level: int, _carry: int) -> void: + run_scope.settlement = settlement_ref + board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) _check_board_hints() _refresh_presentations() func _on_inter_next_pressed() -> void: _inter_overlay.visible = false - if _adventure_mode: + if run_scope.is_adventure: # 冒险模式:大奖励屏确认 → 进下一关(AdventureRunner 序列推进——零自动推进的冒险级停点) - if adventure != null and not adventure.continue_to_next_level(): + if run_scope.adventure != null and not run_scope.adventure.continue_to_next_level(): _fatal_screen("关卡推进失败——见输出日志") return if not _debug.next_layer(): @@ -708,24 +708,24 @@ func _debug_hero_override() -> Dictionary: # 奖励徽章身份解析(组合根守卫:仅挖掘期 Active 相位内查询——地图战斗资源 Ready 域外零读、零诊断噪音; # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) func _identity_lookup(c: Vector2i): - if settlement == null or settlement.minesweeper.current_board() == null: + if run_scope.settlement == null or run_scope.settlement.minesweeper.current_board() == null: return null - if String(settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: + if String(run_scope.settlement.presentation_snapshot().get("phase", "")) != ActionEconomy.PHASE_ACTIVE: return null - return settlement.query_reward_identity(c) + return run_scope.settlement.query_reward_identity(c) # 地形变异 数字遮蔽解析(信息型 hide_number——格带该效果变异即遮蔽;读取方按格子变异数据行动。 # 变异表直读本根 db——settlement.db 即组合根注入的同一 ConfigDB 实例(AdventureRunner/DebugSession # 构造传参),免 settlement 内部链式穿透,T3 收口批) func _variant_mask_lookup(c: Vector2i) -> bool: - if settlement == null: + if run_scope.settlement == null: return false - var b = settlement.minesweeper.current_board() + var b = run_scope.settlement.minesweeper.current_board() if b == null: return false for vid in b.variant_ids(c): - var vrow: Dictionary = db.variation.row_by_id(String(vid)) + var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) == "hide_number": return true return false @@ -734,13 +734,13 @@ func _variant_mask_lookup(c: Vector2i) -> bool: # 地形变异 角标解析(迭代 6 T6——伤害型变异未开格视觉标记;读取方按格子变异数据行动,规则本体零特判。 # 策略纹理非数值墙:标记=玩家绕行/承担的决策输入,难度本体仍住数值包络。变异表直读本根 db——同上款收口) func _variant_mark_lookup(c: Vector2i) -> String: - if settlement == null: + if run_scope.settlement == null: return "" - var b = settlement.minesweeper.current_board() + var b = run_scope.settlement.minesweeper.current_board() if b == null: return "" for vid in b.variant_ids(c): - var vrow: Dictionary = db.variation.row_by_id(String(vid)) + var vrow: Dictionary[Variant, Variant] = db.variation.row_by_id(String(vid)) if String(vrow.get("effect", "")) == "flat_damage": return "flat_damage" return "" @@ -757,7 +757,7 @@ func _on_battle_finished(payload: Dictionary) -> void: var result: String = String(payload.get("result", "")) _sync_settlement_visuals() # → 全关(battle_over 反馈面) HudPresenter.battle_end_feedback(stage, ap_bar, true) # 灰化/idle 回正/RAP 清零/状态全清——HUD 单通道(T3 收口批) - var decision: Dictionary = _debug.on_battle_finished(payload) + var decision: Dictionary[Variant, Variant] = _debug.on_battle_finished(payload) match String(decision.outcome): "next_layer": _inter_label.text = "第 %d 层通过!\nHP %d/%d 带入下一层" % [ @@ -768,14 +768,14 @@ func _on_battle_finished(payload: Dictionary) -> void: "level_end": _end_overlay.open_victory(Narrator.battle_end_text("victory", true), "返回", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % runner.keystone, _bonus_summary_text(), - int(payload.get("gp_dropped", 0)), int(runner.gp)) + "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), + int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) "standalone_end": if result == "victory": _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "回到遭遇选择", board_view.get_global_rect(), _gp_summary_text(payload), - "钥石 · 累计 %d" % runner.keystone, _bonus_summary_text(), - int(payload.get("gp_dropped", 0)), int(runner.gp)) + "钥石 · 累计 %d" % run_scope.runner.keystone, _bonus_summary_text(), + int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)) else: _end_overlay.open_defeat(Narrator.battle_end_text("defeat", false), "返回", board_view.get_global_rect()) @@ -791,17 +791,17 @@ func _on_battle_finished(payload: Dictionary) -> void: # 〔resume 提前 return〕,本 lambda 求值时 runner 尚未赋值;合计改读断点快照 gp 兜底——恢复源快照 # 此刻必在档,语义=续玩恢复面的合计即存档合计;与 _bonus_summary_text 同款守卫式) func _gp_summary_text(payload: Dictionary) -> String: - if runner == null: - var bp: Dictionary = save_manager.breakpoint_snapshot() if save_manager != null else {} + if run_scope.runner == null: + var bp: Dictionary[Variant, Variant] = save_manager.breakpoint_snapshot() return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(bp.get("gp", 0))] - return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(runner.gp)] + return "战利品 GP +%d · 合计 %d" % [int(payload.get("gp_dropped", 0)), int(run_scope.runner.gp)] # M9 结算三行账·本关加成行装配(呈现只读投影——runner.bonus 统一加算区同源;零加成=行隐藏) func _bonus_summary_text() -> String: - if runner == null: + if run_scope.runner == null: return "" - var b: Dictionary = runner.bonus + var b: Dictionary[Variant, Variant] = run_scope.runner.run_ledger.bonus # 迭代 27 T3——加算区住 RunLedger var parts: Array[String] = [] if int(b.get("atk", 0)) != 0: parts.append("攻 %+d" % int(b.get("atk", 0))) @@ -814,7 +814,7 @@ func _bonus_summary_text() -> String: # ==================== core↔view 接线(战斗信号统一走这里)==================== -func _wire_battle(settlement_ref) -> void: +func _wire_battle(settlement_ref: BattleOrchestrator) -> void: settlement_ref.step_row.connect(_on_step_row) settlement_ref.battle_finished.connect(_on_battle_finished) settlement_ref.round_started.connect(_on_round_started) @@ -861,27 +861,26 @@ func _on_round_started(ri: int) -> void: if ri == 1: _sync_treasure_glow() # 巢穴预告呈现(迭代 22 秘宝批 T4——装板报数量读数口消费:≥0=本战装配命中,toast 一次性提示) - if settlement != null and int(settlement.nest_preview_count) >= 0: - board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(settlement.nest_preview_count), "info") + if run_scope.settlement != null and int(run_scope.settlement.nest_preview_count) >= 0: + board_zone.toast.show_toast("巢穴预告——本板危险格共 %d 个" % int(run_scope.settlement.nest_preview_count), "info") # 开战横幅——板级地形变异预告(全局作用域变异首发批 2026-10-04):首轮快照 board_variants # 非空才发射(Boss 图恒带一型=替换制;普通图空快照零发射);轮横幅下方叠行,view 内判减动效。 - if ri == 1 and settlement != null: - var bvs: Array = settlement.presentation_snapshot().get("board_variants", []) + if ri == 1 and run_scope.settlement != null: + var bvs: Array[Variant] = run_scope.settlement.presentation_snapshot().get("board_variants", []) if not bvs.is_empty(): var vlines: Array[String] = HudPresenter.board_variant_lines(db, bvs) if not vlines.is_empty(): settlement_fx.play_board_variant_banner("地形变异 · " + " · ".join(vlines)) - _refresh_used_round = 0 _refresh_presentations() func _on_board_changed() -> void: # 关卡模式下 board_changed 可能先于主场景接手 settlement 到达(begin_battle 期间发)——先从 runner 同步 - if settlement == null and _level_active and runner != null: - settlement = runner.current_battle() - if settlement == null: + if run_scope.settlement == null and run_scope.level_active and run_scope.runner != null: + run_scope.settlement = run_scope.runner.current_battle() + if run_scope.settlement == null: return - board_view.rebuild(settlement.minesweeper.current_board(), settlement.minesweeper) + board_view.rebuild(run_scope.settlement.minesweeper.current_board(), run_scope.settlement.minesweeper) _check_board_hints() _refresh_presentations() @@ -891,15 +890,15 @@ func _on_board_changed() -> void: # 内 refresh 全板重算雾态。调用点=每战首轮装配链(_on_round_started ri==1)——纯呈现边界 🔴 契约 2: # 只写呈现态参数,minesweeper 结算零触碰;off 态(未装配)写 0=与既有基线逐位一致。 func _sync_treasure_glow() -> void: - if settlement == null or board_view == null: + if run_scope.settlement == null or board_view == null: return - board_view.set_fog_glow_bonus(settlement.attune_param_sum("light_radius_bonus")) + board_view.set_fog_glow_bonus(run_scope.settlement.attune_param_sum("light_radius_bonus")) # ---- 一次性非模态提示(每 run 一次——pattern 10)与诚实提示 ---- func _check_board_hints() -> void: - var board = settlement.minesweeper.current_board() + var board = run_scope.settlement.minesweeper.current_board() if board == null: return # 零合法板(棋盘生成 EC5 降级)诚实提示——非静默(toast,非模态) @@ -911,17 +910,17 @@ func _check_board_hints() -> void: func _on_hover_forbidden(_cell: Vector2i) -> void: - if not _hints_shown.has("no_adjacent"): + if not run_scope.hints_shown.has("no_adjacent"): _show_hint("需先走近(点击已开格移动,免费)", "no_adjacent") func _on_chord_attempted(_cell: Vector2i) -> void: - if not _hints_shown.has("chord"): + if not run_scope.hints_shown.has("chord"): _show_hint("本作无双击快开——插旗后轮末自动判定标中;点数字格可移动小人", "chord") func _show_hint(text: String, hint_id: String) -> void: - _hints_shown[hint_id] = true + run_scope.hints_shown[hint_id] = true logger.log_ux("ux_hint_shown", {"hint_id": hint_id}) board_zone.toast.show_toast(text, "info") @@ -930,33 +929,33 @@ func _show_hint(text: String, hint_id: String) -> void: func _on_node_continue() -> void: _close_modal(node_overlay) - if runner != null: + if run_scope.runner != null: # 泛化语义(接口漂移清偿):确认 ≠ 推进——兑付确认后经 choices_presented 弹出选路口 - runner.continue_after_node() + run_scope.runner.continue_after_node() func _on_choices_presented(candidates: Array) -> void: # 零自动推进的停点呈现:整幅节点图 + 图上点击选路(候选按钮退役 2026-09-28——战斗胜利/兑付确认统一到达此处) - if runner == null: + if run_scope.runner == null: return - log_view.append_bb(Narrator.route_line(candidates, runner)) + log_view.append_bb(Narrator.route_line(candidates, run_scope.runner)) # 卡⑥ 战斗胜利总结结算(battle.md OQ14 拍板面接线——欠账清偿):战斗胜利停点先弹 BattleEndOverlay # 胜利形态(金蜡封+GP 增量+总量+判定行+「下一节点」+明细回看),确认后进选路页。判据 = 战斗节点 # 的选路停点且战斗已收场(battle 收尾分支 _settlement 置 null 先于本信号;入口/兑付/商店停点 # 的当前节点非 battle 型——三态天然分流)。Boss 胜/失败走 level_finished 另路不经此处。 - if _adventure_mode and runner.current_battle() == null \ - and String(runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): + if run_scope.is_adventure and run_scope.runner.current_battle() == null \ + and String(run_scope.runner.current_node().get("type", "")) == "battle" and not candidates.is_empty(): _settlement_stop_pending = true - _pending_candidates = candidates.duplicate() + run_scope.pending_candidates = candidates.duplicate() _end_overlay.open_victory(Narrator.battle_end_text("victory", false), "下一节点", board_view.get_global_rect(), - "战利品 GP +%d · 合计 %d" % [_last_gain_amount, int(runner.gp)]) + "战利品 GP +%d · 合计 %d" % [run_scope.last_gain_amount, int(run_scope.runner.gp)]) _open_modal_after_playback(_end_overlay) _refresh_presentations() return _enter_level_screen() - level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, runner.feats_held()))) - level_screen.open_choices(candidates, runner) + level_screen.set_feat_holdings(" · ".join(HudPresenter.feat_holdings_lines(db, run_scope.runner.feats_held()))) + level_screen.open_choices(candidates, run_scope.runner) _refresh_presentations() @@ -967,7 +966,7 @@ func _on_route_chosen(node_id: int) -> void: if not level_screen.visible: _close_modal(level_screen) _exit_level_screen() - if runner != null and not runner.pick_next_node(node_id): + if run_scope.runner != null and not run_scope.runner.pick_next_node(node_id): push_error("[组合根] 选路被拒(编号 %d 不在候选)" % node_id) @@ -976,14 +975,14 @@ func _on_route_chosen(node_id: int) -> void: # 关闭自动恢复本屏(遮挡语义复用),战斗屏不在此期间回显。 func _enter_level_screen() -> void: - _level_screen_active = true + run_scope.level_screen_active = true _battle_root.visible = false _open_modal(level_screen) _sync_status_entry() # 迭代 17 卡③——屏级互斥切换同步(_open_modal 单口不识屏级状态) func _exit_level_screen() -> void: - _level_screen_active = false + run_scope.level_screen_active = false _battle_root.visible = true if level_screen.visible: _close_modal(level_screen) @@ -994,9 +993,9 @@ func _on_abandon_requested() -> void: # 放弃入口(2026-09-21 用户拍板「放弃接口也要有」——推翻 冒险结构.md「MVP 无界面入口」后置条款; # 入口挂选路停点 = 天然非战斗期,拒绝分支仅防御在案):受理 → 冒险放弃结算(同失败型, # 已获大奖励不追回)→ adventure_finished 同步到达 → 终局面收层 - if not _adventure_mode or adventure == null: + if not run_scope.is_adventure or run_scope.adventure == null: return - if not adventure.abandon(): + if not run_scope.adventure.abandon(): logger.log_ux("ux_reject", {"source": "abandon", "sub_reason": "battle_in_flight"}) board_zone.toast.show_toast("战斗进行中——无法放弃", "warn") @@ -1084,8 +1083,7 @@ func _on_status_badge_pressed() -> void: var map_on: bool = level_screen != null and level_screen.visible # P1 守卫兜底——其他模态开着即拒开(常态防线=角标置灰,探针 pressed.emit 绕过 disabled 时由此兜住; # 地图屏自身的登记开态不计;迭代 18 卡⑦ T2——判据排除调试浮窗:对局中浮窗开着时状态角标仍可点) - if _modals == null or (map_on and _modals.has_open_excluding_any([level_screen, debug_overlay])) \ - or (not map_on and _modals.has_open_excluding(debug_overlay)): + if (map_on and _modal_flow.badge_greyed_map()) or (not map_on and _modal_flow.badge_greyed_battle()): return _open_status_panel() @@ -1097,12 +1095,12 @@ func _sync_status_entry() -> void: if status_panel == null or _modals == null or board_zone == null or level_screen == null: return var map_on: bool = level_screen.visible - var battle_live: bool = _level_active or settlement != null # 调试两层无 _level_active——settlement 判据补位(迭代 18 卡⑦) - board_zone.status_badge.visible = _level_active and not map_on + var battle_live: bool = run_scope.level_active or run_scope.settlement != null # 调试两层无 level_active——settlement 判据补位(迭代 18 卡⑦) + board_zone.status_badge.visible = run_scope.level_active and not map_on level_screen.status_badge.visible = map_on # 置灰判据改排除式(迭代 18 卡⑦ T2——调试浮窗开态不计入:对局中浮窗开着时角标仍可点) - board_zone.status_badge.disabled = _modals.has_open_excluding(debug_overlay) - level_screen.status_badge.disabled = _modals.has_open_excluding_any([level_screen, debug_overlay]) + board_zone.status_badge.disabled = _modal_flow.badge_greyed_battle() # 迭代 27 T4——互斥域谓词 + level_screen.status_badge.disabled = _modal_flow.badge_greyed_map() # 调试浮窗角标(迭代 18 卡⑦ P2 甲——迁宿主两屏,DS_DEBUG 门控;17卡③状态角标同形制同守卫): # 战斗屏=对局内(冒险或调试两层)且地图屏未激活;地图屏=随屏;开始菜单零角标(DS_DEBUG=1 启动即开) board_zone.debug_badge.visible = _debug_entry and not map_on and battle_live @@ -1124,14 +1122,13 @@ func _camp_data() -> Dictionary: func _on_wallet_changed(event: String, gp: int, extra: Dictionary) -> void: # wallet 行族转发(到账/购买/清零——试玩日志验收取数面)+ 常驻余额与在开商店覆盖层重刷 logger.log_wallet(event, gp, extra) - _gp_ledger.apply_event(event, gp) # GP 影子账(迭代 6 T7——收束清零前取值,纯呈现零核心触碰=C1;账本归 gp_shadow_ledger.gd) if String(event) == "gain": - _last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) - if shop_overlay.visible and runner != null: - if runner.is_shop_pool(): - shop_overlay.refresh_shelf_state(int(runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) + run_scope.last_gain_amount = int(extra.get("amount", 0)) # 卡⑥ GP 增量取数(每战恰一笔 gain) + if shop_overlay.visible and run_scope.runner != null: + if run_scope.runner.is_shop_pool(): + shop_overlay.refresh_shelf_state(int(run_scope.runner.gp)) # pool 面余额联动(货架内容仅买后重抽变) else: - shop_overlay.refresh_state(runner.shop_products(), int(runner.gp)) + shop_overlay.refresh_state(run_scope.runner.shop_products(), int(run_scope.runner.gp)) _refresh_presentations() @@ -1148,7 +1145,7 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: # **点击不丢弃**(v3.1——用户实机「卡死」根因):锁期点击缓存最后一次,演播完边沿 # (_process 泵 _was_pumping 分支)自动重放——玩家操作零丢失、体感=「演播一停动作就生效」。 if not _row_queue.is_empty(): - _locked_click = {"cell": cell, "kind": kind} + run_scope.locked_click = {"cell": cell, "kind": kind} return # 秘宝选中态劫持(迭代 7 T4——两步交互第二步):点格=使用目标拾取,不落挖格; # 只劫持挖掘点击(move/flag 语义不冲突——选中期玩家仍可移动/插旗) @@ -1156,21 +1153,21 @@ func _on_board_action(cell: Vector2i, kind: String) -> void: if treasure_sel != 0 and kind == "dig": _treasure_ui.attempt_use(treasure_sel, cell) return - var outcomes: Array = settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}]) + var outcomes: Array[Variant] = run_scope.settlement.minesweeper.settle_batch([{"action": kind, "cell": cell}]) var accepted: bool = not outcomes.is_empty() and bool(outcomes[0].accepted) logger.log_action(kind, cell, accepted) - if runner != null and runner.treasure_inventory != null: + if run_scope.runner != null and run_scope.runner.treasure_inventory != null: _treasure_ui.refresh_slots() # 迭代 7 T4——授予发生在 settle_batch 结算段内(同步走完,行尾重查) _handle_outcome_feedback(outcomes[0] if not outcomes.is_empty() else {}) # settle_batch 内部同步走完 结算→战斗结束→关卡推进(事件同步发射)——关卡终局时 settlement 已被 # runner 置空,清图演出判定须先判 settlement(基线既有重入隐患,2026-09-13 加固) - if accepted and kind == "dig" and settlement != null and settlement.minesweeper.current_board() != null and settlement.minesweeper.clear_map_cleared(): + if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper.current_board() != null and run_scope.settlement.minesweeper.clear_map_cleared(): board_zone.fx.play_clear_burst() - if accepted and kind == "dig" and settlement != null and settlement.minesweeper != null: + if accepted and kind == "dig" and run_scope.settlement != null and run_scope.settlement.minesweeper != null: # 挖掘演出(棋盘小人贴图批 2026-09-25):挥铲三帧 + 角色弹跳——触发=格子点开那一刻 #(2026-09-25 用户裁决;settle_batch 同步受理即状态已变,动画纯表现与 refresh 同拍零竞态; # 挖掘不移动英雄,出手方向 = 目标格−英雄格,圆心=英雄持握手位) - var hero_cell: Vector2i = settlement.minesweeper.hero_pos() + var hero_cell: Vector2i = run_scope.settlement.minesweeper.hero_pos() board_zone.fx.play_shovel_swing(board_view.hero_hand_point(), cell - hero_cell, board_view.hero_sprite_scale()) board_view.play_hero_dig_bounce() elif accepted and kind == "move": @@ -1183,7 +1180,7 @@ func _on_stop_pressed() -> void: if _input_locked(): return # stop 经批次提交(排雷玩法 D1×6①——不直调 行动经济) - var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) + var outcomes: Array[Variant] = run_scope.settlement.minesweeper.settle_batch([{"action": "stop"}]) logger.log_action("stop", Vector2i(-1, -1), not outcomes.is_empty() and bool(outcomes[0].accepted)) if not outcomes.is_empty() and String(outcomes[0].get("outcome", "")) == "voided": # voided ≠ 拒绝——独立文案(pattern 1:防静默改判读感欺骗) @@ -1198,21 +1195,20 @@ func _on_refresh_confirmed() -> void: if _input_locked(): return var ok: bool = false - if _level_active and runner != null: - ok = runner.player_request_refresh() - elif settlement != null: - ok = settlement.player_request_refresh() + if run_scope.level_active and run_scope.runner != null: + ok = run_scope.runner.player_request_refresh() + elif run_scope.settlement != null: + ok = run_scope.settlement.player_request_refresh() logger.log_action("refresh", Vector2i(-1, -1), ok) if ok: - _refresh_used_round += 1 - logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_round}) + logger.log_ux("ux_refresh_attempt", {"result": "accepted", "round_refresh_index": _refresh_used_snapshot()}) board_zone.toast.show_toast("RAP−%d · 旧板旗作废——新图已就位,AP 保留继续挖" % int(db.action_economy.rap_refresh_cost), "gold") else: # 资源面/上限拒绝(EC13 合法拒绝可见;行动经济 无载荷——客户端按上下文推断) - var rap: int = int(settlement.presentation_snapshot().get("rap", 0)) if settlement != null else 0 + var rap: int = int(run_scope.settlement.presentation_snapshot().get("rap", 0)) if run_scope.settlement != null else 0 var cost: int = int(db.action_economy.rap_refresh_cost) var result := "rejected_rap" if rap < cost else "rejected_cap" - logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_round + 1}) + logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1}) if result == "rejected_rap": board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn") ap_bar.pulse_rap() @@ -1235,10 +1231,18 @@ func _handle_outcome_feedback(outcome: Dictionary) -> void: # ==================== 全屏 States 状态机(挖掘/结算/窗口/终局)==================== +# 本轮已受理刷新次数只读面(迭代 27 T3——P1-2 刷新双账收口:main 副本已删, +# 单一事实源=战斗编排器 _refresh_used_this_round,经 presentation_snapshot 成文面读取) +func _refresh_used_snapshot() -> int: + if run_scope == null or run_scope.settlement == null: + return 0 + return int(run_scope.settlement.presentation_snapshot().get("refresh_used", 0)) + + func _refresh_presentations() -> void: # 每个核心信号/写调用返回后的全量重查装配(更新触发注记——无推送信号,命令式重查) - var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_round) - HudPresenter.refresh(stage, info_panel, ap_bar, settlement, runner, _level_active, db, refresh_remaining) + var refresh_remaining: int = maxi(0, int(db.action_economy.refresh_cap_per_round) - _refresh_used_snapshot()) + HudPresenter.refresh(stage, info_panel, ap_bar, run_scope.settlement, run_scope.runner, run_scope.level_active, db, refresh_remaining) _treasure_ui.refresh_slots() # C 区秘宝槽重查(反哺批⑤——试炼镐积点进度随挖格刷新悬停浮窗) _sync_settlement_visuals() @@ -1246,9 +1250,9 @@ func _refresh_presentations() -> void: func _sync_settlement_visuals() -> void: # 结算阶段(round_end → 下一轮):D 区去饱和+金橙偏移锁输入(明度不变)+ 明细带展开 84px var settling := _is_settling() - if settling and not _was_settling: + if settling and not run_scope.was_settling: settlement_fx.play_round_sweep(board_view.get_global_rect()) # 结算轮光带(演出链批)——时序敏感演出触发,例外留在组合根 - _was_settling = settling + run_scope.was_settling = settling HudPresenter.sync_settlement_visuals(stage, board_view, log_view, settling, _phase_text()) @@ -1259,9 +1263,9 @@ func _is_settling() -> bool: # 窗口不可作演播中标志——row_pacer._process 注)。battle 收尾演播例外: # overlay 开启即 _flush_row_queue 排空,视觉随队列空同步交接结算框。 # 相位取数经 presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透(T3 收口批)。 - if settlement == null or not settlement.is_battle_live(): + if run_scope.settlement == null or not run_scope.settlement.is_battle_live(): return not _row_queue.is_empty() - if String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: + if String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING: return true return not _row_queue.is_empty() @@ -1269,11 +1273,11 @@ func _is_settling() -> bool: func _phase_text() -> String: # 文案装配归 narrator(settlement 缺位 = 非战斗态——组合根模式状态守卫;相位取数经 # presentation_snapshot 成文面——禁 settlement.action_economy 链式穿透,T3 收口批) - if settlement == null: + if run_scope.settlement == null: return "第 — 轮 · —" - return Narrator.phase_text(int(settlement.round_index), String(settlement.battle_result), - String(settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, - _boss_active, Narrator.boss_name(runner, db)) + return Narrator.phase_text(int(run_scope.settlement.round_index), String(run_scope.settlement.battle_result), + String(run_scope.settlement.presentation_snapshot().get("phase", "")) == ActionEconomy.PHASE_WAITING, + run_scope.boss_active, Narrator.boss_name(run_scope.runner, db)) # ==================== B 区明细带 / 图例(模态入口)==================== @@ -1289,12 +1293,12 @@ func _on_legend_toggled() -> void: func _on_diagnose_pressed() -> void: # 迭代 18 卡⑦——本局种子现值(原 _seed_value() 重算冒充病灶改向 _current_master_seed 只读单点) - var ctx := {"mode": "level" if _level_active else "debug", "seed": _current_master_seed()} - if _level_active and runner != null: - ctx["runner"] = {"node_index": runner.node_index, "level_result": String(runner.level_result), - "hero_hp": runner.hero_hp, "settlement_is_null": runner.current_battle() == null} - if settlement != null: - ctx["settlement"] = settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12) + var ctx := {"mode": "level" if run_scope.level_active else "debug", "seed": _current_master_seed()} + if run_scope.level_active and run_scope.runner != null: + ctx["runner"] = {"node_index": run_scope.runner.node_index, "level_result": String(run_scope.runner.level_result), + "hero_hp": run_scope.runner.hero_hp, "settlement_is_null": run_scope.runner.current_battle() == null} + if run_scope.settlement != null: + ctx["settlement"] = run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读——code-review 2026-09-12) var path: String = logger.dump_diagnosis(ctx) board_zone.toast.show_toast("已转储诊断: " + path, "info") @@ -1314,8 +1318,7 @@ func _show_debug_overlay() -> void: # 调试浮窗角标点击(两屏同款处理器——17卡③状态角标同形制守卫;C6:仍收口 _show_debug_overlay 单口) func _on_debug_badge_pressed() -> void: var map_on: bool = level_screen != null and level_screen.visible - if _modals == null or (map_on and _modals.has_open_excluding_any([level_screen, debug_overlay])) \ - or (not map_on and _modals.has_open_excluding(debug_overlay)): + if (map_on and _modal_flow.badge_greyed_map()) or (not map_on and _modal_flow.badge_greyed_battle()): return _show_debug_overlay() @@ -1325,7 +1328,7 @@ func _on_debug_badge_pressed() -> void: func _sync_debug_overlay() -> void: if debug_overlay == null: return - debug_overlay.set_context_locked(settlement != null or _adventure_mode) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放——T2) + debug_overlay.set_context_locked(run_scope.settlement != null or run_scope.is_adventure) # 对局中批量模拟/两层调试入口禁用(长程异步模拟与活跃结算并存态不开放——T2) debug_overlay.update_run_data(_debug_run_data()) # T3 当局数据只读区块喂入(开窗+模态开合信号驱动) @@ -1334,21 +1337,21 @@ func _sync_debug_overlay() -> void: # 未播种 -1 显「—」);关卡/节点/HP/GP=runner 成文出口、钥石=adventure 冒险账本(LevelRunner 无此域); # 缺位=「—」占位。零动效(纯标签重查——17卡③契约)。 func _debug_run_data() -> Dictionary: - var r = adventure.runner() if adventure != null else null + var r = run_scope.adventure.runner() if run_scope.adventure != null else null var seed_now: int = _current_master_seed() var seed_txt: String = str(seed_now) if seed_now >= 0 else "—" if r == null: return {"seed": seed_txt, "level": "—", "node": "—", "hp": "—", "gp": "—", "keystone": "—"} - return {"seed": seed_txt, "level": int(adventure.level_index), "node": int(r.node_index), - "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(adventure.keystone)} + return {"seed": seed_txt, "level": int(run_scope.adventure.level_index), "node": int(r.node_index), + "hp": "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())], "gp": int(r.gp), "keystone": int(run_scope.adventure.keystone)} # 本局种子现值(只读单点——当局数据区块与诊断转储共用):冒险=adventure.master_seed 访问器现值; # 调试两层=RNG 服务 master_seed。禁 _seed_value() 重算冒充本局种子(main.gd 旧 :1174 病灶随批改向)。 func _current_master_seed() -> int: - if adventure != null: - return int(adventure.master_seed) - return int(get_node("/root/RngService").master_seed) + if run_scope.adventure != null: + return int(run_scope.adventure.master_seed) + return int(get_node(AUTOLOAD_PATH_RNG).master_seed) func _close_debug_overlay() -> void: @@ -1363,7 +1366,7 @@ func _on_tune_apply(atk_pct: int, hp_pct: int) -> void: func _on_tune_simulate(n: int) -> void: # 批量模拟编排 = 控制器承接(tune_sim_controller.gd);E4 后无表态需复位——模拟走 runner 覆盖参数。 # 对局中禁用兜底(迭代 18 卡⑦ T2——长程异步模拟与活跃结算并存态不开放;探针 emit 绕过 disabled 时由此守住) - if settlement != null: + if run_scope.settlement != null: return _tune_sim.run(n) @@ -1380,6 +1383,6 @@ func _seed_value() -> int: # 封闭流、禁 randomize()/自建 RNG——组合根建种子属 ADR-0007 豁免面,不进玩法 RNG 消费) if _seed_edit.text.is_valid_int(): return int(_seed_edit.text) - if save_manager != null and save_manager.is_first_adventure_done(): - return randi() + if save_manager.is_first_adventure_done(): + return randi() # ADR-0007-exempt(非首局空框引擎熵——组合根建种子不进玩法 RNG 消费,static_scan S1/S2 标记豁免) return 42 diff --git a/game/presentation/modal_flow.gd b/game/presentation/modal_flow.gd new file mode 100644 index 00000000..9dadf4dc --- /dev/null +++ b/game/presentation/modal_flow.gd @@ -0,0 +1,89 @@ +class_name ModalFlow +extends RefCounted +# 模态编排纯逻辑类(迭代 27 T4——审计 P1-5/P2-7:组合根可测化抽取 + 模态互斥域内聚)。 +# 依赖仅 ModalStack + 查询 Callable(审计处方原话)——零节点构建、零信号连接、零核心触碰 +#(ADR-0001 同构:连接归组合根,本类只收注入)。可直接 new() 单测(V6)。 +# 职责三件: +# ① 登记与开合编排(遮挡记账单口 / T17 演播后排窗挂起——原 main._open_modal 系逻辑随迁) +# ② 棋盘输入锁谓词(互斥域随层登记——原 main._input_locked 排除式拼装消亡) +# ③ 角标置灰谓词(状态/调试两屏同款守卫判据) +# 排除语义随层登记(ModalStack.register tags):debug_overlay=["non_locking"]、 +# level_screen=["screen_state"],其余层零标签——原调用点排除式拼装面消亡(P2-7 修法)。 + +const ModalStackScript := preload("res://presentation/modal_stack.gd") # 非全局类(main 同款 preload 引用) + +var stack = ModalStackScript.new() + +# ---- 组合根注入面(查询 Callable 组——依赖仅 ModalStack + 查询 Callable 纪律载体) ---- +var start_visible_get: Callable # () -> bool——开始层可见(非模态全屏态例外一) +var inter_visible_get: Callable # () -> bool——过场层可见(例外二) +var settlement_get: Callable # () -> Variant——当前战斗编排器(null=战斗缺位例外三) +var rows_empty_get: Callable # () -> bool——排播队列空(演播后排窗判据) +var flush_rows: Callable # 开层前排空排播(卡⑦——明细带恒完整) +var on_open_sync: Callable # 开侧同步钩子(状态角标+浮窗当局数据) +var on_close_sync: Callable # 关侧同步钩子 +var replay_click: Callable # 演播完边沿无挂起时的锁期点击重放(main._replay_locked_click) + +var _deferred: Control = null # 演播后开窗挂起(T17——排播放完再开) + + +func register(m: Control, tags: Array = []) -> void: + stack.register(m, tags) + + +func open(m: Control) -> void: + # 挂起兜底(T17 演播后排窗):其他路径(如回看)在播期排空队列会使演播完边沿不再触发—— + # 此处先结挂起 modal 防永不兑现(m==挂起对象时跳过=本调用自身)。 + if _deferred != null and m != _deferred: + var dm: Control = _deferred + _deferred = null + open(dm) # 原语义:先结挂起后继续开本层(不 return——两拍都走) + flush_rows.call() # 卡⑦——任何覆盖层开启前排空排播队列(明细带恒完整,回看/停点面不缺行) + stack.open(m) + on_open_sync.call() + + +func close(m: Control) -> void: + stack.close(m) + on_close_sync.call() + + +# 演播后排窗(T17 用户令「伤害过程演完才切结算窗口」):排播队列在播=挂起,演播完边沿 +# (pump_ended 派发)再开——旧路径前置 flush 会把击杀轮的伤害/死亡演出同帧倒掉。 +# 队列空=立即开(行为同旧路径)。 +func open_after_playback(m: Control) -> void: + if rows_empty_get.call(): + open(m) + return + _deferred = m + m.visible = false # 挂起含视觉(open_* 已置 visible=true——不压掉则窗在排播期照会上屏,延迟形同虚设) + + +# 演播完边沿派发(row_pacer.pump_ended 注入口):先开挂起 modal(胜利窗接管呈现), +# 后锁期点击重放——modal 开启后输入锁吞掉重放点击(战斗已收场,重放无意义)。 +func pump_ended() -> void: + if _deferred != null: + var m: Control = _deferred + _deferred = null + open(m) + return + replay_click.call() + + +# 棋盘输入锁谓词(原 main._input_locked 随迁):模态互斥域(non_locking 层开态不计——迭代 18 +# 卡⑦ T2 防线)+ 三例外(开始层/过场层非模态全屏态 + 战斗缺位棋盘输入无载体)。 +func locks_input() -> bool: + return stack.has_open_excluding_tags([ModalStackScript.TAG_NON_LOCKING]) \ + or bool(start_visible_get.call()) or bool(inter_visible_get.call()) or settlement_get.call() == null + + +# 角标置灰谓词·战斗屏形态(状态/调试两角标同判据):登记册上 non_locking 层外的任一层开着。 +# (地图屏此态恒关——屏级互斥,与旧排除式 [debug] 逐位同值) +func badge_greyed_battle() -> bool: + return stack.has_open_excluding_tags([ModalStackScript.TAG_NON_LOCKING]) + + +# 角标置灰谓词·地图屏形态(地图屏自身 screen_state 不计 + 调试浮窗 non_locking 不计—— +# 与旧排除式 [level_screen, debug] 逐位同值)。 +func badge_greyed_map() -> bool: + return stack.has_open_excluding_tags([ModalStackScript.TAG_NON_LOCKING, ModalStackScript.TAG_SCREEN_STATE]) diff --git a/game/presentation/modal_flow.gd.uid b/game/presentation/modal_flow.gd.uid new file mode 100644 index 00000000..febc42ba --- /dev/null +++ b/game/presentation/modal_flow.gd.uid @@ -0,0 +1 @@ +uid://nqnk82quv1lb diff --git a/game/presentation/modal_stack.gd b/game/presentation/modal_stack.gd index f78c48cb..3b51f27e 100644 --- a/game/presentation/modal_stack.gd +++ b/game/presentation/modal_stack.gd @@ -8,10 +8,16 @@ extends RefCounted var _list: Array[Control] = [] # 登记序 = z 遮挡序(低 → 高) var _occluded := {} # modal → 被其隐藏的上方层(关闭恢复) +var _tags := {} # modal → 排除组标签集(迭代 27 T4——互斥域随层登记;P2-7 开态谓词收口) + +# 排除组封闭词表(组合根登记用——新标签=改此处+评审,防调用点拼装回潮) +const TAG_NON_LOCKING := "non_locking" # 开态不计入棋盘输入锁与角标置灰(调试浮窗——迭代 18 卡⑦ T2 防线:DS_DEBUG 默认开×遮挡复活×has_open 组合=棋盘永久锁死) +const TAG_SCREEN_STATE := "screen_state" # 常驻屏态(关卡地图屏——自身开态不计入本屏角标判据) -func register(m: Control) -> void: +func register(m: Control, tags: Array = []) -> void: _list.append(m) + _tags[m] = tags func open(m: Control) -> void: @@ -43,21 +49,18 @@ func has_open() -> bool: return false -# 排除式开态查询(迭代 17 卡③ 2026-10-06——状态面板入口守卫判据):skip 层自身的开态不计入 -#(关卡地图屏系登记册模态成员=常态屏态,其上的地图屏角标必须可点;其余任一层开着=角标置灰)。 -# 只读查询零副作用——登记序/开合记账语义零改动。 -func has_open_excluding(skip: Control) -> bool: - return has_open_excluding_any([skip]) - - -# 多排除式开态查询(迭代 18 卡⑦ T2 防护——调试浮窗对局中可开后,棋盘输入锁与状态角标置灰 -# 判据须同时排除调试浮窗自身〔否则 DS_DEBUG 默认开×遮挡复活×has_open 组合=棋盘永久锁死—— -# 审计🔴 S1+ 停机点防线〕;地图屏场景=地图屏+调试浮窗双排除)。语义同 has_open_excluding, -# skip 集参数化;只读查询零副作用,登记序/开合记账语义零改动。 -func has_open_excluding_any(skips: Array) -> bool: +# 排除组开态查询(迭代 27 T4——P2-7 模态协议内聚:原「调用点排除式拼装」has_open_excluding/ +# has_open_excluding_any 退役,排除语义随层登记(register tags),查询按组名集合表达。 +# 只读查询零副作用——登记序/开合记账语义零改动;空标签集=全计(与旧 has_open 同形)。 +func has_open_excluding_tags(exclude_tags: Array) -> bool: for m in _list: - if skips.has(m): - continue if is_instance_valid(m) and m.visible: - return true + var mt: Array[Variant] = _tags.get(m, []) + var excluded := false + for t in exclude_tags: + if mt.has(t): + excluded = true + break + if not excluded: + return true return false diff --git a/game/presentation/playtest_logger.gd b/game/presentation/playtest_logger.gd index 9e312642..13d5e862 100644 --- a/game/presentation/playtest_logger.gd +++ b/game/presentation/playtest_logger.gd @@ -50,7 +50,7 @@ func log_path() -> String: return _path -func attach_battle(settlement) -> void: +func attach_battle(settlement: BattleOrchestrator) -> void: settlement.step_row.connect(_on_step_row) settlement.settlement_snapshot.connect(_on_settlement_snapshot) settlement.battle_finished.connect(_on_battle_finished_log) @@ -94,10 +94,10 @@ func log_action(kind: String, cell: Vector2i, accepted: bool, extra: Dictionary func _on_settlement_snapshot(snap: Dictionary) -> void: _append(snap) # ux_round_exit{path_tag}:对齐上游封闭枚举 {stopped, ap_exhausted, cleared} - var facts: Dictionary = snap.get("round_facts", {}) + var facts: Dictionary[Variant, Variant] = snap.get("round_facts", {}) log_ux("ux_round_exit", {"path_tag": String(facts.get("path", ""))}) # ux_round_input_ratio:原始输入动作数∶AP 消耗(本轮 AP 配额 − 快照时剩余 = 消耗) - var economy: Dictionary = snap.get("economy", {}) + var economy: Dictionary[Variant, Variant] = snap.get("economy", {}) var ap_left: int = int(economy.get("ap", 0)) var consumed: int = clampi(ap_per_round - ap_left, 0, ap_per_round) log_ux("ux_round_input_ratio", {"inputs": _ux_inputs, "ap_consumed": consumed, diff --git a/game/presentation/row_pacer.gd b/game/presentation/row_pacer.gd index ceec0e39..81e59bd4 100644 --- a/game/presentation/row_pacer.gd +++ b/game/presentation/row_pacer.gd @@ -11,7 +11,7 @@ extends Node const PACE_SEGMENT_GAP := 0.3 # 段间阶梯延迟(代拍板:白名单 0.2–0.4s 内取值,追认清单呈报) const PACE_ROW_GAP := 0.22 # 段内间隔(三轮 #3 上调 0.06→0.22——逐条演出可感知,配合数字浮现/状态脉冲) -var _rows: Array = [] # 待排播行(step_row 载荷原样入队;__round_line 标记=轮首分隔行) +var _rows: Array[Variant] = [] # 待排播行(step_row 载荷原样入队;__round_line 标记=轮首分隔行) var _pace_wait := 0.0 # 距下一行排播的剩余秒(_process 消耗) var _prev_segment := "" # 上一排播行的段型(段间/段内延迟判据) var _was_pumping := false # 排播活跃边沿(T17——末行消费帧触发结算视觉回挖掘态刷新) @@ -75,7 +75,7 @@ func _process(delta: float) -> void: _pace_wait -= delta if _pace_wait > 0.0: return - var entry: Dictionary = _rows.pop_front() + var entry: Dictionary[Variant, Variant] = _rows.pop_front() _consume_row(entry) var seg := String(entry.get("type", "")) _pace_wait = 0.0 if UiKit.motion_scale < 1.0 else (PACE_SEGMENT_GAP if seg != _prev_segment else PACE_ROW_GAP) diff --git a/game/presentation/run_scope.gd b/game/presentation/run_scope.gd new file mode 100644 index 00000000..bf379d05 --- /dev/null +++ b/game/presentation/run_scope.gd @@ -0,0 +1,22 @@ +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 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[Variant] = [] # 结算框挂起期的选路候选暂存(choices_presented 载荷) +var locked_click := {} # 锁期点击缓存(T17 v3.1——最后一次;演播完边沿重放) +var last_gain_amount := 0 # 最近一笔钱包入账(卡⑥ GP 增量取数面——wallet_changed gain 载荷) diff --git a/game/presentation/run_scope.gd.uid b/game/presentation/run_scope.gd.uid new file mode 100644 index 00000000..ce8f9dcb --- /dev/null +++ b/game/presentation/run_scope.gd.uid @@ -0,0 +1 @@ +uid://d05dkfqnbqdlx diff --git a/game/presentation/scenes/adventure_screen.gd b/game/presentation/scenes/adventure_screen.gd new file mode 100644 index 00000000..e5d6bf75 --- /dev/null +++ b/game/presentation/scenes/adventure_screen.gd @@ -0,0 +1,37 @@ +class_name AdventureScreen +extends Control +# 冒险屏壳(迭代 27 T1 场景拆分——审计 P0-1 第一步;内含关卡地图屏与战斗屏两容器)。 +# 两容器视图原样构建(build_battle_ui 归 ui_assembler 纪律不变、LevelMapScreen 类视图继续 +# 代码构建、零 .tscn 化改造——契约 2);战斗信号接线归组合根 main.gd(ADR-0001——信号连接 +# 只在组合根)。树序语义:本屏壳挂 main 首子位(全模态之下)——屏内 level_screen 与 +# battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后序零视觉差; +# 模态登记序不变(level_screen 实例仍按节点层与三选一层之间登记)。 +# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。 + +var battle_root: Control # 战斗屏宿主(B/C/D 三区容器——原 main._battle_root 句柄指向成员) +var stage: BattleStageView # B 区(含 log_view 明细带) +var log_view: BattleLogView +var info_panel: HeroInfoPanelView # C 区 +var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx) +var board_view: BoardView # 探针兼容别名(= board_zone.board_view) +var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar) +var settlement_fx: SettlementFxView # 结算演出覆盖层 +var level_screen: LevelMapScreen # 关卡画面(独立屏) + +const UiAssembler := preload("res://presentation/ui_assembler.gd") # 构建归装配器(规范 1.1——非全局类,preload 引用) + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) + battle_root = VBoxContainer.new() + add_child(battle_root) + var battle_ui: Dictionary[Variant, Variant] = UiAssembler.build_battle_ui(battle_root, get_node("/root/ConfigDB")) + stage = battle_ui.stage + log_view = battle_ui.log_view + info_panel = battle_ui.info_panel + board_zone = battle_ui.board_zone + board_view = battle_ui.board_view + ap_bar = battle_ui.ap_bar + settlement_fx = battle_ui.settlement_fx + level_screen = LevelMapScreen.new() + add_child(level_screen) diff --git a/game/presentation/scenes/adventure_screen.gd.uid b/game/presentation/scenes/adventure_screen.gd.uid new file mode 100644 index 00000000..2046833d --- /dev/null +++ b/game/presentation/scenes/adventure_screen.gd.uid @@ -0,0 +1 @@ +uid://csw5cl1xa7qu7 diff --git a/game/presentation/scenes/adventure_screen.tscn b/game/presentation/scenes/adventure_screen.tscn new file mode 100644 index 00000000..4645ba9d --- /dev/null +++ b/game/presentation/scenes/adventure_screen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://slice0advn0screen"] + +[ext_resource type="Script" path="res://presentation/scenes/adventure_screen.gd" id="1_advn"] + +[node name="AdventureScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_advn") diff --git a/game/presentation/scenes/camp_screen.gd b/game/presentation/scenes/camp_screen.gd new file mode 100644 index 00000000..fb32f571 --- /dev/null +++ b/game/presentation/scenes/camp_screen.gd @@ -0,0 +1,14 @@ +class_name CampScreen +extends Control +# 营地屏壳(迭代 27 T1 场景拆分——审计 P0-1 第一步)。 +# 营地覆盖层视图原样构建(CampOverlay 类视图继续代码构建、零 .tscn 化改造——契约 2); +# 购买/开合接线归组合根经 CampShopPresenter(ADR-0001——信号连接只在组合根)。 +# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。 + +var camp_overlay: CampOverlay # 营地覆盖层(原 build_overlays 营地段迁入——main 组合根句柄指向本屏成员) + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) + camp_overlay = CampOverlay.new() + add_child(camp_overlay) diff --git a/game/presentation/scenes/camp_screen.gd.uid b/game/presentation/scenes/camp_screen.gd.uid new file mode 100644 index 00000000..c817baf2 --- /dev/null +++ b/game/presentation/scenes/camp_screen.gd.uid @@ -0,0 +1 @@ +uid://dw1k6rn5w8gso diff --git a/game/presentation/scenes/camp_screen.tscn b/game/presentation/scenes/camp_screen.tscn new file mode 100644 index 00000000..5d1f5594 --- /dev/null +++ b/game/presentation/scenes/camp_screen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://slice0camp0screen"] + +[ext_resource type="Script" path="res://presentation/scenes/camp_screen.gd" id="1_camp"] + +[node name="CampScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_camp") diff --git a/game/presentation/scenes/menu_screen.gd b/game/presentation/scenes/menu_screen.gd new file mode 100644 index 00000000..ea57bb81 --- /dev/null +++ b/game/presentation/scenes/menu_screen.gd @@ -0,0 +1,31 @@ +class_name MenuScreen +extends Control +# 开始菜单屏壳(迭代 27 T1 场景拆分——审计 P0-1「场景作为组合单元/生命周期单元」第一步)。 +# 视图代码原样构建(节点构建归 ui_assembler.gd 纪律不变——本屏壳只持有与暴露句柄); +# 按钮接线归组合根 main.gd(ADR-0001——信号连接只在组合根)。 +# 可独立实例化(F6/headless 挂载)——V2「各新屏场景可独立实例化」探针断言载体。 +# main 组合根保留 _start_overlay 等句柄指向本屏成员——可见性开关与探针挂载假设零迁移。 + +var start_overlay: Control # 全屏菜单层(原 main._start_overlay——组合根可见性开关句柄不变) +var box: VBoxContainer +var lvbtn: Button +var resumebtn: Button +var campbtn: Button +var careerbtn: Button +var settingsbtn: Button +var quitbtn: Button + +const UiAssembler := preload("res://presentation/ui_assembler.gd") # 构建归装配器(规范 1.1——非全局类,preload 引用) + + +func _ready() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入) + var start: Dictionary[Variant, Variant] = UiAssembler.build_start_overlay(self, get_node("/root/ConfigDB")) + start_overlay = start.overlay + box = start.box + lvbtn = start.lvbtn + resumebtn = start.resumebtn + campbtn = start.campbtn + careerbtn = start.careerbtn + settingsbtn = start.settingsbtn + quitbtn = start.quitbtn diff --git a/game/presentation/scenes/menu_screen.gd.uid b/game/presentation/scenes/menu_screen.gd.uid new file mode 100644 index 00000000..fdee5253 --- /dev/null +++ b/game/presentation/scenes/menu_screen.gd.uid @@ -0,0 +1 @@ +uid://dqijp10rs1kaj diff --git a/game/presentation/scenes/menu_screen.tscn b/game/presentation/scenes/menu_screen.tscn new file mode 100644 index 00000000..b9cd0d63 --- /dev/null +++ b/game/presentation/scenes/menu_screen.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://slice0menu0screen"] + +[ext_resource type="Script" path="res://presentation/scenes/menu_screen.gd" id="1_menu"] + +[node name="MenuScreen" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_menu") diff --git a/game/presentation/treasure_interact_controller.gd b/game/presentation/treasure_interact_controller.gd index f5137d1b..ecd77a41 100644 --- a/game/presentation/treasure_interact_controller.gd +++ b/game/presentation/treasure_interact_controller.gd @@ -66,14 +66,14 @@ func on_slot_clicked(slot_handle: int) -> void: if slot_handle < 0: toast.show_toast("自动兑付件:满足条件自动生效,无需手动使用", "info") return - var rec: Dictionary = {} + var rec: Dictionary[Variant, Variant] = {} for r in inventory_projection(): if int(r.get("handle", 0)) == slot_handle: rec = r break if rec.is_empty() or bool(rec.get("used", false)): return - var arow: Dictionary = db.treasure_item_row(String(rec.get("item_id", ""))) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", ""))) if String(arow.get("target", "none")) == "none": attempt_use(slot_handle) # AP 类:显式无目标即用(不再伪造哨兵坐标) return @@ -92,7 +92,7 @@ func attempt_use(handle: int, cell: Variant = null) -> void: if settlement == null: return var target: Vector2i = cell if cell != null else NO_TARGET - var v: Dictionary = settlement.use_treasure(handle, target) + var v: Dictionary[Variant, Variant] = settlement.use_treasure(handle, target) if not bool(v.ok): toast.show_toast(String(v.get("reason", "无法使用")), "warn") return @@ -100,7 +100,7 @@ func attempt_use(handle: int, cell: Variant = null) -> void: refresh_slots() refresh_overlays() board_view.refresh() # 迭代 13 T8/D6——移除格即时落图(无移除态的件 diff 零变化幂等) - var arow: Dictionary = db.treasure_item_row(String(v.get("item_id", ""))) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(v.get("item_id", ""))) toast.show_toast("已使用 %s" % String(arow.get("display_name", "")), "info") @@ -115,11 +115,11 @@ func inventory_projection() -> Array: if ms != null: cb = func(_c: Vector2i) -> bool: return ms.has_unrevealed() # 只读查询面(排雷玩法 has_unrevealed——手搓全板双循环收口) - var out: Array = [] - var cs: Dictionary = settlement.attune_convert_state() if settlement != null else {} + var out: Array[Variant] = [] + var cs: Dictionary[Variant, Variant] = settlement.attune_convert_state() if settlement != null else {} var auto_seen := false # 自动兑付件是否已随骑库存出现(恒否——charges=0 不入骑库存;守合成行重复) for rec in runner.treasure_inventory.query_inventory(cb): - var arow: Dictionary = db.treasure_item_row(String(rec.get("item_id", ""))) + var arow: Dictionary[Variant, Variant] = db.treasure_item_row(String(rec.get("item_id", ""))) var dn := String(arow.get("display_name", "")) # 积点进度浮窗行(反哺批⑤——自动兑付件 C 区悬停 tooltip 补 N/M 进度;件 id 锁定=装配行唯一) var note := "" @@ -161,5 +161,5 @@ func refresh_overlays() -> void: var runner = _runner_get.call() if runner == null or runner.treasure_inventory == null or board_view == null: return - var ov: Dictionary = runner.treasure_inventory.query_overlays() + var ov: Dictionary[Variant, Variant] = runner.treasure_inventory.query_overlays() board_view.set_treasure_overlays(ov.get("mine_reveal", []), ov.get("reward_preview", [])) diff --git a/game/presentation/ui_assembler.gd b/game/presentation/ui_assembler.gd index 0430715e..5d352e23 100644 --- a/game/presentation/ui_assembler.gd +++ b/game/presentation/ui_assembler.gd @@ -62,9 +62,10 @@ static func build_battle_ui(root: Control, db) -> Dictionary: # 模态覆盖层组(z 序 = 登记序:图例 < 节点 < 终局 < 商店)。 -# 返回 {legend_overlay, node_overlay, end_overlay, shop_overlay, camp_overlay};调用方按本序 addChild。 +# 返回 {legend_overlay, node_overlay, end_overlay, shop_overlay};调用方按本序 addChild。 # (调整板已随迭代 14.5 调试整合批退役——调试浮窗由组合根直构挂树序最末,见 main._build_ui; -# 明细回看 overlay 已随 M9/P-C7 退役 2026-10-04——冒险中回看归「生涯」06 登记) +# 明细回看 overlay 已随 M9/P-C7 退役 2026-10-04——冒险中回看归「生涯」06 登记; +# 营地覆盖层已随迭代 27 T1 场景拆分迁 camp_screen 屏壳——本组不再构建) static func build_overlays(host: Control, db) -> Dictionary: var legend_overlay := LegendOverlay.new(int(db.action_economy.rap_refresh_cost)) # 打磨包 2026-09-29——RAP 行表值注入 host.add_child(legend_overlay) @@ -79,16 +80,11 @@ static func build_overlays(host: Control, db) -> Dictionary: var shop_overlay := ShopOverlay.new() host.add_child(shop_overlay) - # 营地覆盖层(元进度实现批 2026-09-21——主菜单营地入口兑付呈现;开合与开始层互斥仿设置面先例) - var camp_overlay := CampOverlay.new() - host.add_child(camp_overlay) - return { "legend_overlay": legend_overlay, "node_overlay": node_overlay, "end_overlay": end_overlay, "shop_overlay": shop_overlay, - "camp_overlay": camp_overlay, } diff --git a/game/presentation/views/battle_log_view.gd b/game/presentation/views/battle_log_view.gd index b3ebdd88..a99c05ea 100644 --- a/game/presentation/views/battle_log_view.gd +++ b/game/presentation/views/battle_log_view.gd @@ -130,8 +130,8 @@ func _flush_pending_agg() -> void: if _pending_agg.is_empty(): return for key in _pending_agg: - var item: Dictionary = _pending_agg[key] - var row: Dictionary = item["row"] + var item: Dictionary[Variant, Variant] = _pending_agg[key] + var row: Dictionary[Variant, Variant] = item["row"] var count: int = item["count"] var bb := _format_row(row, count) _round_rows.append(bb) diff --git a/game/presentation/views/board_view.gd b/game/presentation/views/board_view.gd index 7e8bd7ba..3316c866 100644 --- a/game/presentation/views/board_view.gd +++ b/game/presentation/views/board_view.gd @@ -275,7 +275,7 @@ func _refresh_slab() -> void: # 战斗启动/换板:按板尺寸重建格单元(minesweeper 引用随之更新) -func rebuild(board: BoardData, minesweeper) -> void: +func rebuild(board: BoardData, minesweeper: MinesweeperRules) -> void: _board = board _minesweeper = minesweeper for child in _world.get_children(): @@ -630,7 +630,7 @@ func _show_path_preview(c: Vector2i) -> void: # D3 合法性指示:悬停已开格 → BFS 路径蓝细线点迹(compute_move_path 只读面) if _minesweeper == null: return - var path: Array = _minesweeper.compute_move_path(c) + var path: Array[Variant] = _minesweeper.compute_move_path(c) for pc in path: if _cells.has(pc): _cells[pc].dot_visible = true @@ -648,7 +648,7 @@ func _update_badge(c: Vector2i) -> String: return "" if _r0.has(c): return "" # R₀ 初始展开格无奖励身份(地图战斗资源 资格域 = S∖R₀) - var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典) + var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面(禁直读 排雷玩法 状态字典) if rec.get("kind", "") != "safe": return "" var ident = _identity_cb.call(c) @@ -704,7 +704,7 @@ func _paint_cell(c: Vector2i, hero: Vector2i) -> void: flag = false u.tex_flag = null if revealed: - var rec: Dictionary = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面 + var rec: Dictionary[Variant, Variant] = _minesweeper.reveal_record(c) # E2 收口:已开记录经查询面 if rec.get("kind", "") == "mine": bg = COL_FLOOR glyph = "雷" @@ -910,7 +910,7 @@ func _load_board_tiles(board_type: String = "dungeon") -> void: # 方向稿 _v3_darken_floors 同式移植;现行瓦片 L≈0.036 低于带下沿故实为提亮;素材重产归资产批, # 本归一为程序代役(逐批装载时对 freshly-loaded 原图测光,幂等)。归一后逐像素明度顺路采集, # 驱动数字墨族按板自适应(_digit_ink_mode——均值分位同源同像素,禁二次解码) - var lumas: Array = [] + var lumas: Array[Variant] = [] for i in range(floors.size()): floors[i] = _normalize_floor_luma(floors[i], 0.10, lumas) var ink_mode := _digit_ink_mode(lumas) diff --git a/game/presentation/views/camp_overlay_view.gd b/game/presentation/views/camp_overlay_view.gd index a892bd16..420bc166 100644 --- a/game/presentation/views/camp_overlay_view.gd +++ b/game/presentation/views/camp_overlay_view.gd @@ -238,8 +238,8 @@ func _rebuild_tree(data: Dictionary) -> void: c.queue_free() _tree_buy_btns.clear() _tree_fork_btns.clear() - var tree: Array = data.get("tree", []) as Array - var forks: Array = data.get("fork_groups", []) as Array + var tree: Array[Variant] = data.get("tree", []) as Array + var forks: Array[Variant] = data.get("fork_groups", []) as Array var vein_names := {"spine": "·脊柱淬炼·", "forge": "·锻造脉·", "keystone": "·钥石脉·", "attune": "·同调脉·"} var last_vein := "" for row in tree: @@ -407,7 +407,7 @@ func refresh_camp(data: Dictionary) -> void: for row in data.get("workshop", []): ws_by_id[String(row.get("item_id", ""))] = row for card in _workshop_rows: - var row: Dictionary = ws_by_id.get(String(card.item_id), {}) + var row: Dictionary[Variant, Variant] = ws_by_id.get(String(card.item_id), {}) if row.is_empty(): continue var unlocked := bool(row.get("unlocked", false)) @@ -422,7 +422,7 @@ func refresh_camp(data: Dictionary) -> void: elif mode == "condition": var parts: Array[String] = [] for req_v in (row.get("reqs", []) as Array): - var req: Dictionary = req_v as Dictionary + var req: Dictionary[Variant, Variant] = req_v as Dictionary var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) prog_text = "、".join(parts) @@ -440,10 +440,10 @@ func refresh_camp(data: Dictionary) -> void: if card.get("buy_btn") != null: card.buy_btn.disabled = balance < int(card.get("price", 0)) # 同调装配段状态就地刷(迭代 11 第①批 T4——Slots 点亮/满位禁选/Y 升级行余额联动) - var att: Dictionary = data.get("attune", {}) + var att: Dictionary[Variant, Variant] = data.get("attune", {}) if _attune_header_l != null: var slots_n := int(att.get("slots", 0)) - var eq: Array = att.get("equipped", []) + var eq: Array[Variant] = att.get("equipped", []) _attune_header_l.text = "── 同调装配 Slots %d/%d ──" % [eq.size(), slots_n] var eq_set := {} for v in eq: @@ -454,7 +454,7 @@ func refresh_camp(data: Dictionary) -> void: # 满位禁选+未解锁禁装(迭代 22 T4——已装件恒可卸下;unlocked 快照随 rebuild 刷新) card2.btn.disabled = (not on and eq.size() >= slots_n) or (not on and not bool(card2.get("unlocked", false))) if _refresh_l != null: - var rf: Dictionary = att.get("refresh", {}) + var rf: Dictionary[Variant, Variant] = att.get("refresh", {}) _refresh_l.text = "三选一刷新 +%d 次/冒险 · Lv%d" % [int(rf.get("charges", 0)), int(rf.get("level", 0))] _refresh_btn.text = "已满级" if bool(rf.get("maxed", false)) else "升级 %d 钥石" % int(rf.get("price", 0)) _refresh_btn.disabled = bool(rf.get("maxed", false)) or balance < int(rf.get("price", 0)) @@ -508,7 +508,7 @@ func _rebuild_workshop(data: Dictionary) -> void: # 同调装配段重建(迭代 11 第①批 T4——秘宝匣柜台尾部段〔迭代 21 T6 随柜台迁入〕;Slots 点亮+ # 九件行板+Y 升级行,程序占位面) func _rebuild_attune(data: Dictionary) -> void: - var att: Dictionary = data.get("attune", {}) + var att: Dictionary[Variant, Variant] = data.get("attune", {}) _attune_header_l = UiKit.label("",UiKit.FS_HINT, true, UiKit.INK) _workshop_holder.add_child(_attune_header_l) _attune_nodes.append(_attune_header_l) @@ -548,7 +548,7 @@ func _rebuild_attune(data: Dictionary) -> void: func _rebuild_recent(data: Dictionary) -> void: var lines: Array[String] = [] - var recent: Array = data.get("recent", []) + var recent: Array[Variant] = data.get("recent", []) if recent.is_empty(): lines.append(" 最近冒险:(暂无记录——完成或结束一次冒险后入账)") for s in recent: diff --git a/game/presentation/views/career_overlay.gd b/game/presentation/views/career_overlay.gd index a405a122..64a7a3de 100644 --- a/game/presentation/views/career_overlay.gd +++ b/game/presentation/views/career_overlay.gd @@ -28,7 +28,7 @@ var _keystone_badge: UiKit.KeystoneBadge # 钥石余额(badge 工厂件— func _init() -> void: # 全屏页壳(T6/T7 对齐批先例——暖亮底+石纹弱质感+暗角+56px 通栏顶条+内容列) - var page: Dictionary = UiKit.page_shell(self, true) + var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, true) var top: HBoxContainer = page.top_row top.add_child(UiKit.label("生 涯",UiKit.FS_TITLE, true, UiKit.WARM)) var top_sp := Control.new() diff --git a/game/presentation/views/curio_pick_overlay.gd b/game/presentation/views/curio_pick_overlay.gd index a47113ea..de98d26d 100644 --- a/game/presentation/views/curio_pick_overlay.gd +++ b/game/presentation/views/curio_pick_overlay.gd @@ -24,8 +24,8 @@ var _refresh_allowance := 0 # 组合根投喂免费余量(X/Y var _refresh_gp_cost := 0 # 组合根投喂 GP 刷新价(免费位用尽后的付费档读数) var _selected := "" # 点选暂存(确认前可改选) var _header := "" # 停点头部文案(选择态保持显示) -var _card_meta: Array = [] # open_pick 缓存 [{id, text}]——高亮还原读面 -var _bcards: Array = [] # M7 bcard 引用列(select 高亮=set_selected) +var _card_meta: Array[Variant] = [] # open_pick 缓存 [{id, text}]——高亮还原读面 +var _bcards: Array[Variant] = [] # M7 bcard 引用列(select 高亮=set_selected) func _init() -> void: @@ -33,7 +33,7 @@ func _init() -> void: # 根底暗档)——56px 通栏顶条(标题+刷新图标钮/角标)+ 居中入账信息行(GP 汇总行承载)+ # bcard 三竖卡(D1 232×352)+ 底部确认主钮与免费选择注记。差异白名单=免费选择/✓章/无价签。 # 状态机不变:pick→result。M7 同构裁决遗产保留。 - var page: Dictionary = UiKit.page_shell(self, false) + var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, false) var top: HBoxContainer = page.top_row top.add_child(UiKit.label("战利品三选一",UiKit.FS_TITLE, true, UiKit.WARM)) var top_sp := Control.new() @@ -137,7 +137,7 @@ func _select_card(id: String) -> void: _selected = id # 选中态=木描边+✓章(demo ⑥;set_selected 逐卡置真值防改选残留) for i in _bcards.size(): - var meta: Dictionary = _card_meta[i] if i < _card_meta.size() else {} + var meta: Dictionary[Variant, Variant] = _card_meta[i] if i < _card_meta.size() else {} (_bcards[i] as UiKit.BCard).set_selected(String(meta.get("id", "")) == id) _confirm_btn.visible = true @@ -167,10 +167,6 @@ func press_refresh() -> void: _refresh_btn.pressed.emit() -func selected_id() -> String: - return _selected - - # 兑付反馈态(curio_resolved 到达——结果行追加在头部之后;唯一出口 = 继续) func show_result(text: String) -> void: _label.text = _header + "\n" + text diff --git a/game/presentation/views/debug_overlay.gd b/game/presentation/views/debug_overlay.gd index 4bedb82d..f4b90ef6 100644 --- a/game/presentation/views/debug_overlay.gd +++ b/game/presentation/views/debug_overlay.gd @@ -158,7 +158,7 @@ func effective_max_hp() -> int: # ---- 内部构建(标题/关闭固定于浮窗骨架,可扩展列表区居中滚动)---- func _build() -> void: - var shell: Dictionary = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(640, 0), 8) + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_OVERLAY, Vector2(640, 0), 8) var box: VBoxContainer = shell.box var title := UiKit.label("调试",UiKit.FS_TITLE, true, UiKit.WARM) diff --git a/game/presentation/views/hero_info_panel_view.gd b/game/presentation/views/hero_info_panel_view.gd index 3b972820..5923cf4c 100644 --- a/game/presentation/views/hero_info_panel_view.gd +++ b/game/presentation/views/hero_info_panel_view.gd @@ -16,9 +16,9 @@ var _atk_label: Label var _armor_label: Label signal treasure_slot_clicked(handle: int) # 迭代 7 T4——槽点击(组合根消费:AP 类即用/空间类进选中态) var item_slots: HBoxContainer # 秘宝槽(迭代 7 T4 接线——库存条常读+两步交互第一步) -var _slot_panels: Array = [] # 槽 PanelContainer 引用(选中高亮/置灰重刷) +var _slot_panels: Array[Variant] = [] # 槽 PanelContainer 引用(选中高亮/置灰重刷) var _selected_handle := 0 # 选中态(0=无;组合根回写维持单源) -var _slot_handles: Array = [] # 槽位→handle 映射(set_treasure_inventory 刷新) +var _slot_handles: Array[Variant] = [] # 槽位→handle 映射(set_treasure_inventory 刷新) var _curio_section: VBoxContainer # 特性(预留——空态隐藏,接线归大奖励/锻造呈现批) var _curio_list: VBoxContainer var _feat_section: VBoxContainer # 特性持有段(迭代 12 呈现补齐——空态隐藏) @@ -180,7 +180,7 @@ func set_treasure_inventory(items: Array, selected_handle: int) -> void: slot.add_theme_stylebox_override("panel", UiKit.panel_flat(UiKit.PANEL_DATA, UiKit.PANEL_BORDER, 0, 1, Vector4(4, 4, 4, 4))) continue - var rec: Dictionary = items[i] + var rec: Dictionary[Variant, Variant] = items[i] _slot_handles.append(int(rec.get("handle", 0))) var used := bool(rec.get("used", false)) var usable := bool(rec.get("usable", true)) diff --git a/game/presentation/views/level_map_screen.gd b/game/presentation/views/level_map_screen.gd index 9cbcb146..6003909a 100644 --- a/game/presentation/views/level_map_screen.gd +++ b/game/presentation/views/level_map_screen.gd @@ -282,7 +282,7 @@ func _process(delta: float) -> void: # 打开选路口(cands = 候选节点引用数组;runner 只读用于整图渲染与状态栏刷新) -func open_choices(cands: Array, runner) -> void: +func open_choices(cands: Array, runner: LevelRunner) -> void: var was_visible := visible # 进入转场判据(就地刷新/模态栈恢复不重放淡入) _runner = runner _hover_id = -1 @@ -357,8 +357,8 @@ func _refresh_status() -> void: var max_hp: int = _runner.effective_max_hp() _hp_value.text = "HP %d/%d" % [int(_runner.hero_hp), int(max_hp)] _hp_bar.queue_redraw() - var atk_bonus := int(_runner.bonus.get("atk", 0)) - var ac_bonus := int(_runner.bonus.get("armor_class", 0)) + var atk_bonus := int(_runner.run_ledger.bonus.get("atk", 0)) # 迭代 27 T3——加算区住 RunLedger + var ac_bonus := int(_runner.run_ledger.bonus.get("armor_class", 0)) var atk_txt := "%d" % _runner.effective_atk() if atk_bonus > 0: atk_txt += " +%d" % atk_bonus # 加成角标(ASCII 加号——防像素字体缺 ⁺ 字形出豆腐块) @@ -366,7 +366,7 @@ func _refresh_status() -> void: if ac_bonus > 0: ac_txt += " +%d" % ac_bonus _attr_value.text = "攻击 %s · 护甲 AC %s" % [atk_txt, ac_txt] - var hp_bonus := int(_runner.bonus.get("max_hp", 0)) + var hp_bonus := int(_runner.run_ledger.bonus.get("max_hp", 0)) var parts: Array[String] = [] if atk_bonus > 0: parts.append("攻 +%d" % atk_bonus) @@ -400,7 +400,7 @@ func _on_canvas_input(ev: InputEvent) -> void: func _candidate_at(local_pos: Vector2) -> int: if _runner == null: return -1 - var nodes: Array = _runner.nodes() + var nodes: Array[Variant] = _runner.nodes() var pos := _layout(nodes, _canvas) for nid in _cand_ids: var node := _node_by_id(nid, nodes) @@ -427,7 +427,7 @@ func _submit_choice(nid: int) -> void: func press_candidate(index: int) -> void: if index < 0 or index >= _cand_ids.size() or _runner == null: return - var nodes: Array = _runner.nodes() + var nodes: Array[Variant] = _runner.nodes() var node := _node_by_id(int(_cand_ids[index]), nodes) if node.is_empty(): return @@ -447,12 +447,12 @@ func _id_of(node: Dictionary) -> int: func _draw_graph(canvas: Control) -> void: if _runner == null or not visible: return - var nodes: Array = _runner.nodes() + var nodes: Array[Variant] = _runner.nodes() var pos := _layout(nodes, canvas) var current_id: int = _runner.current_node_id() # 当前节点 id(单链=下标 / 图=编号)——visited_ids() 已按编号空间解析 - var visited: Array = _runner.visited_ids() - var cand_ids: Array = _runner.next_candidate_ids() + var visited: Array[Variant] = _runner.visited_ids() + var cand_ids: Array[Variant] = _runner.next_candidate_ids() # 连线(通道)先行画(方块覆盖其上);悬停预承诺 = 所属通道再增亮(加粗) # T17 修复:选路停点(候选非空)非候选通道降透明——金岔路从结构网分离(干线共线重叠缺陷的 # 视觉主因:不同源通道共用同一垂直干线,金边与石色边焊接成「整列可走」假象) @@ -549,7 +549,7 @@ func _assign_layers(nodes: Array, layer: Dictionary, slot: Dictionary) -> void: queue.append(entry) layer[entry] = 0 while not queue.is_empty(): - var cur: Dictionary = queue.pop_front() + var cur: Dictionary[Variant, Variant] = queue.pop_front() var l: int = int(layer[cur]) layer_counts[l] = int(layer_counts.get(l, 0)) + 1 slot[cur] = int(layer_counts[l]) - 1 diff --git a/game/presentation/views/node_overlay.gd b/game/presentation/views/node_overlay.gd index 46bfd38a..59554438 100644 --- a/game/presentation/views/node_overlay.gd +++ b/game/presentation/views/node_overlay.gd @@ -13,7 +13,7 @@ var _btn: Button func _init() -> void: # M5(UI 落码批):rest 兑付面板 plate9 石板化+暖金头带字(demo ④休息弹层;暗字上亮板 §7.2) - var shell: Dictionary = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(0, 0, 0, 0.6), Vector2.ZERO, 14, true, UiKit.panel_skinned(Vector4(26, 18, 26, 18))) var box: VBoxContainer = shell.box _label = UiKit.label("",UiKit.FS_SUB, true, UiKit.INK_TITLE) diff --git a/game/presentation/views/settings_overlay.gd b/game/presentation/views/settings_overlay.gd index f44918a4..7673bcd9 100644 --- a/game/presentation/views/settings_overlay.gd +++ b/game/presentation/views/settings_overlay.gd @@ -26,7 +26,7 @@ func _init(store) -> void: # 探针隔离换注(冒烟/探针禁碰真实 user://settings.cfg——换注后按新 store 重挂控件态) -func retarget(store) -> void: +func retarget(store: SettingsStore) -> void: _store = store _sync() @@ -37,7 +37,7 @@ func _build() -> void: # 减动效与结算演出动画=同一设置(语义命名批合并),一行承载。 # T8(UI 设计对齐批 2026-10-06):两列对齐(label 列定宽 140——demo .setrow label/控制两列)+ # 后果说明内联随行(demo .why 同排)+返回钮紧凑化(demo .btn9 紧凑——原 216 宽砖)。 - var shell: Dictionary = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, UiKit.BG_BRIGHT, Vector2(560, 0), 8, true, UiKit.panel_skinned(Vector4(20, 16, 20, 16))) var box: VBoxContainer = shell.box diff --git a/game/presentation/views/shop_overlay_view.gd b/game/presentation/views/shop_overlay_view.gd index f612aeb4..1b4b4c41 100644 --- a/game/presentation/views/shop_overlay_view.gd +++ b/game/presentation/views/shop_overlay_view.gd @@ -33,7 +33,7 @@ func _init() -> void: # 角标+GP 框)+ 爬价规则行 + bcard 三竖卡居中 + 底部离开钮与池源注记。 # M6 遗产:库存余量退役(关卡节点地图 v1.5-a 用户裁);刷新钮呈现位照摆(机制=MC-1 投喂)。 # legacy 面(open_shop)行卡同壳呈现(shop=off 对照承载原样)。高内聚边界不变:纯视图 setter 面。 - var page: Dictionary = UiKit.page_shell(self, true) + var page: Dictionary[Variant, Variant] = UiKit.page_shell(self, true) var top: HBoxContainer = page.top_row top.add_child(UiKit.label("商 店",UiKit.FS_TITLE, true, UiKit.WARM)) var top_sp := Control.new() @@ -110,7 +110,7 @@ func refresh_state(products: Array, gp: int) -> void: for p in products: by_id[String(p.product_id)] = p for card in _card_rows: - var p: Dictionary = by_id.get(String(card.product_id), {}) + var p: Dictionary[Variant, Variant] = by_id.get(String(card.product_id), {}) if p.is_empty(): continue card.price.text = "现价 %d 金币" % int(p.price) diff --git a/game/presentation/views/stage_world.gd b/game/presentation/views/stage_world.gd index 173af733..36d4fe77 100644 --- a/game/presentation/views/stage_world.gd +++ b/game/presentation/views/stage_world.gd @@ -13,9 +13,4 @@ extends Node2D var world_font: Font = null # 字体注入槽位(null = ThemeDB.fallback_font——BattleStageView.set_world_font 注入) # C6 headless 可验判据(ADR-0003 Decision 4):注入字体非空 + 中文字形 has_char + 采样文本宽度 > 0。 -# 实际光栅观感归批末目检清单。 -func font_probe() -> Dictionary: - var f: Font = world_font if world_font != null else ThemeDB.fallback_font - var has_char: bool = f.has_char("人".unicode_at(0)) - var width: float = f.get_string_size("兑现演出区", HORIZONTAL_ALIGNMENT_LEFT, -1, 14).x - return {"font": f, "has_char": has_char, "width": width} +# 实际光栅观感归批末目检清单。 \ No newline at end of file diff --git a/game/presentation/views/standee_body.gd b/game/presentation/views/standee_body.gd index 85dd8685..38ca6ffe 100644 --- a/game/presentation/views/standee_body.gd +++ b/game/presentation/views/standee_body.gd @@ -37,7 +37,10 @@ var _portrait_flip := false # 主角三态切换(迭代 6 T3):state 名 → 纹理(battle_stage_view 组合点注册);缺态=保持现纹理 # (缺位回退链延伸——三态素材缺任一不炸不空)。敌站桩不注册不消费。 var _hero_tex := {} -var _hero_state := "idle" +enum HeroState { IDLE, RESET, ATTACK, HIT } # 主角三态+复位哨兵(迭代 27 T6——裸字符串 enum 化;外部 String 契约=register/set 形参零变化) +const _HS_NAME := {HeroState.IDLE: "idle", HeroState.RESET: "reset", HeroState.ATTACK: "attack", HeroState.HIT: "hit"} +const _HS_VALUE := {"idle": HeroState.IDLE, "reset": HeroState.RESET, "attack": HeroState.ATTACK, "hit": HeroState.HIT} +var _hero_state: int = HeroState.IDLE var _state_revert_tw: Tween var _order := 1 var _intent_glyph := "" @@ -330,7 +333,7 @@ func _revive() -> void: modulate = Color(1, 1, 1) rotation_degrees = 0.0 if _kind == "hero": - _hero_state = "reset" # 强制回 idle 态(绕过同态早退) + _hero_state = HeroState.RESET # 强制回 idle 态(绕过同态早退) set_hero_state("idle") _refresh_portrait_glyph() if slot_row != null: @@ -368,11 +371,12 @@ func register_hero_state(state: String, tex: Texture2D) -> void: # 状态切换:现纹理换注册态(attack/hit);未注册态名或缺位 = 保持现纹理不炸。 # 非 idle 态自动短驻回归 idle(0.35s——演出一拍即回,结算收尾另有显式 idle 兜底)。 func set_hero_state(state: String) -> void: - if _kind != "hero" or state == _hero_state: + var target: int = _HS_VALUE.get(state, -1) + if _kind != "hero" or target == _hero_state: return if not _hero_tex.has(state) and state != "idle": return - _hero_state = state + _hero_state = target if _portrait_sprite != null and _hero_tex.has(state): _portrait_sprite.texture = _hero_tex[state] if _state_revert_tw != null and _state_revert_tw.is_valid(): @@ -384,14 +388,14 @@ func set_hero_state(state: String) -> void: func _settle_hero_idle() -> void: - _hero_state = "idle" + _hero_state = HeroState.IDLE if _portrait_sprite != null and _hero_tex.has("idle"): _portrait_sprite.texture = _hero_tex["idle"] # 三态探针出口(fx_probe——当前态名+注册态集合) func hero_state_probe() -> Dictionary: - return {"state": _hero_state, "registered": _hero_tex.keys()} + return {"state": _HS_NAME.get(_hero_state, "idle"), "registered": _hero_tex.keys()} func _portrait_color() -> Color: diff --git a/game/presentation/views/status_panel.gd b/game/presentation/views/status_panel.gd index abec822c..a00652b8 100644 --- a/game/presentation/views/status_panel.gd +++ b/game/presentation/views/status_panel.gd @@ -21,7 +21,7 @@ var _tree_label: Label func _init() -> void: - var shell: Dictionary = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, true, + var shell: Dictionary[Variant, Variant] = UiKit.modal_shell(self, Color(UiKit.DIM, 0.55), Vector2(560, 0), 10, true, UiKit.panel_skinned(Vector4(20, 16, 20, 16))) # 石板皮(缺位回退数据板——同设置面先例) var box: VBoxContainer = shell.box @@ -103,7 +103,7 @@ func _rebuild_workshop(data: Dictionary) -> void: else: var parts: Array[String] = [] for req_v in (row.get("reqs", []) as Array): - var req: Dictionary = req_v as Dictionary + var req: Dictionary[Variant, Variant] = req_v as Dictionary var lbl := String(UiKit.TREASURE_REQ_LABELS.get(String(req.get("key", "")), "条件")) parts.append("%s %d/%d" % [lbl, int(req.get("progress", 0)), int(req.get("threshold", 0))]) prog_text = "、".join(parts) diff --git a/game/presentation/views/status_panel.gd.uid b/game/presentation/views/status_panel.gd.uid new file mode 100644 index 00000000..a22797d1 --- /dev/null +++ b/game/presentation/views/status_panel.gd.uid @@ -0,0 +1 @@ +uid://dd071t8g8cjb3 diff --git a/game/presentation/views/status_slot_row.gd b/game/presentation/views/status_slot_row.gd index 3cd3c09d..b773b070 100644 --- a/game/presentation/views/status_slot_row.gd +++ b/game/presentation/views/status_slot_row.gd @@ -165,7 +165,7 @@ func _on_slot_hover(slot: Control) -> void: # tooltip:来源 + 语义 + 剩余(≥13px;悬停只读反馈通道——无点击无写) if not slot.has_meta("eff"): return - var eff: Dictionary = slot.get_meta("eff") + var eff: Dictionary[Variant, Variant] = slot.get_meta("eff") var eid := String(eff.effect_id) var src: String = String(SOURCE_NAMES.get(String(eff.get("source_class", "")), String(eff.get("source_class", "")))) var semantic: String = String(EFFECT_SEMANTIC.get(eid, String(eff.get("display_name", "")))) diff --git a/game/project.godot b/game/project.godot index 66438d00..ce7ab42d 100644 --- a/game/project.godot +++ b/game/project.godot @@ -14,10 +14,16 @@ config/name="DungeonSweeperVerticalSlice" run/main_scene="res://presentation/main.tscn" config/features=PackedStringArray("4.7") +[debug] + +gdscript/warnings/untyped_declaration=1 + [autoload] ConfigDB="*res://core/config_db.gd" RngService="*res://core/rng_service.gd" +SaveManager="*res://core/save_manager.gd" +MetaProgression="*res://core/meta_progression.gd" [display] diff --git a/game/smoke/fx_shot_probe.gd b/game/smoke/fx_shot_probe.gd index c29fddd4..ae6f2d33 100644 --- a/game/smoke/fx_shot_probe.gd +++ b/game/smoke/fx_shot_probe.gd @@ -51,11 +51,11 @@ func _ready() -> void: main.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame - var st = main.settlement + var st = main.run_scope.settlement if st == null: _fail("首战未开(settlement 空)") return - var rules = main.settlement.minesweeper + var rules = main.run_scope.settlement.minesweeper var board = rules.current_board() # 自有驱动(只挖/走——测试驱动可用板面数据挑安全格;新手策略档的插旗拒绝会耗尽预算):挖到 3 格停 var dug := 0 diff --git a/game/smoke/probe_ui_util.gd b/game/smoke/probe_ui_util.gd index fbcc6ce1..3b93bd76 100644 --- a/game/smoke/probe_ui_util.gd +++ b/game/smoke/probe_ui_util.gd @@ -90,8 +90,11 @@ static func _text_match(text: String, needle: String, mode: String) -> bool: # tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。 # mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注 #(隔离 cfg 名 = _settings.cfg,与被收口探针原文件名逐一同名);refresh_resume = 组合根 -# 「继续上局」按钮重刷;swap_save = 是否换注 main.save_manager(营地先行段只换 meta 不换 save_manager)。 +# 「继续上局」按钮重刷;swap_save = 是否换注全局 SaveManager(营地先行段只换 meta 侧不换 SaveManager)。 # 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。 +# 迭代 27 T1——SaveManager/MetaProgression 升格 autoload 后隔离面改单例原地重置 +#(attach_path 沿 RngService.set_master_seed 先例;MetaProgression 持 save 引用透传, +# swap_save=false 分支换 meta 侧引用=独立 iso 实例,语义同旧「只换 meta 不换 save_manager」)。 static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false, refresh_resume: bool = false, swap_save: bool = true): var save_path := "user://%s.json" % tag @@ -102,9 +105,15 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_ iso.load_from_disk() if mark_first_done: iso.mark_first_adventure_done() + # --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析 + var sm = main.get_node("/root/SaveManager") if swap_save: - main.save_manager = iso - main.meta = MetaProgressionScript.new(main.db, iso) + sm.attach_path(save_path) + if mark_first_done: + sm.mark_first_adventure_done() + iso = sm + else: + main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动 if with_settings: main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag) if refresh_resume: diff --git a/game/smoke/smoke_all.gd b/game/smoke/smoke_all.gd index 75d38edd..ccb0e191 100644 --- a/game/smoke/smoke_all.gd +++ b/game/smoke/smoke_all.gd @@ -73,10 +73,139 @@ 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 + _smoke_type_gate() + 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 T5 V7——project.godot warning 配置在位〔源码断言〕+审计器脚本在位; +# 审计器归零判定走 static_scan S9 闸机,此处兜配置面)---- +func _smoke_type_gate() -> void: + var pj: String = FileAccess.get_file_as_string("res://project.godot") + check(pj.contains("gdscript/warnings/untyped_declaration=1"), + "V7 类型闸 warning 配置在位(untyped_declaration=1 显形)〔迭代27 T5〕") + var scan_src: String = FileAccess.get_file_as_string("res://tests/static_scan.sh") + check(scan_src.contains("audit_type_hints.py"), + "V7 类型审计器闸机在位(static_scan S9 → tools/audit_type_hints.py)〔迭代27 T5〕") + + +# ---- 三屏场景化断言(迭代 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 not main.run_scope.is_adventure, + "V2 闲时会话自 _ready 即在(永非 null;迭代 27 T3——gp 影子账退役无账本成员)") + 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(), + "V3 新会话局态零值") + # 注入脏状态(旧手工清单时代的典型泄漏源——新会话必须整体吞掉;迭代 27 T3——刷新计数已单源化入编排器,不再属会话脏面) + main.run_scope.hints_shown["chord"] = true + 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 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(), + "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 @@ -160,7 +289,7 @@ func _smoke_node_map_templates() -> void: rng0.new_adventure(910001) var runner0 = load("res://core/level_runner.gd").new(_db, rng0) var c_before: int = rng0.counter() - check(runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, {}, + check(runner0.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, # 迭代 27 T3——start_level_external 签名收口(bonus 参数随 RunLedger 单类下传退役) Callable(self, "_smoke_rest_stub"), {}, true, 0), "强制 template_pick=0 起关受理") check(rng0.counter() == c_before, "强制取模板零 RNG 消耗(零抽签——首次脚本语义)") var tpl0: Dictionary = {} @@ -175,7 +304,7 @@ func _smoke_node_map_templates() -> void: var rg = load("res://core/rng_service.gd").new() rg.new_adventure(910002) var rn = load("res://core/level_runner.gd").new(_db, rg) - rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, {}, + rn.start_level_external(2, {"hero_hp": 20, "gp": 0}, {}, Callable(self, "_smoke_rest_stub"), {}, true, -1) names.append(rn.active_template_name) check(names[0] == names[1] and String(names[0]) != "", @@ -614,7 +743,7 @@ func _smoke_adventure() -> void: check(gp0 >= hp_price + l2_price_bonus, "进店余额 ≥ hp_up L2 首价(%d = 基价 %d + 关卡加成 %d——配置推导)" % [hp_price + l2_price_bonus, hp_price, l2_price_bonus]) check(r8.buy_product("hp_up"), "购买 hp_up 受理(出账 + 加算区)") - var acc_shop := int(a8.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子) + var acc_shop := int(a8.run_ledger.bonus.get("max_hp", -1)) # L1 三选一后 + 商店 3(首件随种子) check(acc_shop >= 3, "统一冒险加算区含商店值(max_hp=%d ≥ 商店 3——L1 三选一件值另计)" % acc_shop) a8._on_level_finished({"result": "victory", "hp": int(r8.hero_hp), "node_index": r8.node_index, "node_id": r8.current_node_id(), "node_type": "shop"}) # 模拟 L2 通关(victory 载荷直调——非末关) @@ -625,8 +754,8 @@ func _smoke_adventure() -> void: check(a8.pick_curio(String(l2_pick.get("kind", "")), String(l2_pick.get("tier", "")), String(l2_pick.get("feat_id", ""))), "三选一点选兑付受理(候选取首件)") var picked2: String = String(a8._curio_result.get("picked", "")) var expect_acc: int = acc_shop + (int(a8._curio_result.get("value", 0)) if picked2 == "max_hp" else 0) - check(int(a8.bonus.get("max_hp", -1)) == expect_acc, - "三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.bonus.get("max_hp", -1)), acc_shop, picked2]) + check(int(a8.run_ledger.bonus.get("max_hp", -1)) == expect_acc, + "三选一属性件与商店购买同账累加(max_hp=%d = %d + 三选一%s)" % [int(a8.run_ledger.bonus.get("max_hp", -1)), acc_shop, picked2]) var max_eff8: int = a8.effective_max_hp() # T17 口径修复随批:账本生效上限含调试浮窗 pct(a8 已 set 400) check(int(a8.ledger["hero_hp"]) <= max_eff8, "通关部分回血封顶(hp %d ≤ 生效上限 %d——血回满语义退役)" % [int(a8.ledger["hero_hp"]), max_eff8]) @@ -2217,6 +2346,10 @@ func _smoke_active_refresh() -> void: minesweeper.settle_batch([{"action": "dig", "cell": plan3.dig_cell}]) var ok3: bool = settlement.player_request_refresh() check(not ok3, "第 3 次刷新被拒(每轮兜底上限 2——RAP %d 足额仍拒)" % settlement.action_economy.rap) + # V5(迭代 27 T3——刷新双账收口):受理计数单源=结算快照 economy.refresh_used,与 can_refresh 门一致 + check(int(settlement.presentation_snapshot().get("refresh_used", -1)) == 2, + "V5 快照刷新计数=2(受理单源——main 副本已删)〔迭代27 T3〕") + check(not settlement.can_refresh(), "V5 上限达门禁一致(can_refresh 与快照计数同源)〔迭代27 T3〕") # 跨图 facts 完整兑现:拒绝后挖到 AP 耗尽自动结算(扩大跨图样本——三段挖掘分属三张图) var dig_guard := 20 while settlement.action_economy.phase == "Active" and settlement.round_index == 4 and dig_guard > 0: @@ -4204,7 +4337,7 @@ func _smoke_shop_room() -> void: var gp0: int = runner.gp check(runner.buy_product("atk_up"), "购买 atk_up 受理(现价 6)") check(runner.gp == gp0 - 6, "钱包扣现价 6(%d → %d)" % [gp0, runner.gp]) - check(int(runner.bonus.get("atk", -1)) == 1 and int(runner.bonus.get("max_hp", -1)) == 0 and int(runner.bonus.get("armor_class", -1)) == 0, + check(int(runner.run_ledger.bonus.get("atk", -1)) == 1 and int(runner.run_ledger.bonus.get("max_hp", -1)) == 0 and int(runner.run_ledger.bonus.get("armor_class", -1)) == 0, # 迭代 27 T3——加算区住 RunLedger "统一冒险加算区 atk +1(bonus={atk,max_hp,armor_class} 账本)") by_id = _shop_products_by_id(runner.shop_products()) check(int(by_id["atk_up"]["price"]) == 8 and int(by_id["atk_up"]["purchased"]) == 1, @@ -4395,10 +4528,10 @@ func _smoke_shop_pool() -> void: var val_map := {"attack": "atk", "max_hp": "max_hp", "armor": "armor_class", "ap": "ap"} var gp0: int = int(r8.gp) var taken0: int = int(a8._curio_taken.get(kind, 0)) - var val0: int = int(a8.bonus.get(String(val_map[kind]), -999)) + var val0: int = int(a8.run_ledger.bonus.get(String(val_map[kind]), -999)) check(r8.buy_pool_item(kind), "购买受理(pool 口——%s|%s)" % [kind, tier]) check(int(r8.gp) == gp0 - int(buy_c.get("price", 0)), "钱包扣现价(%d → %d)" % [gp0, int(r8.gp)]) - check(int(a8.bonus.get(String(val_map[kind]), -999)) == val0 + int(_db.adventure.curio_value(kind, tier)), + check(int(a8.run_ledger.bonus.get(String(val_map[kind]), -999)) == val0 + int(_db.adventure.curio_value(kind, tier)), "加算区 +令牌档位件值(池件=奖励件本体——%s|%s %+d)" % [kind, tier, int(_db.adventure.curio_value(kind, tier))]) check(int(a8._curio_taken.get(kind, 0)) == taken0 + 1, "共享池账本 -1(跨渠道单点——taken %d → %d)" % [taken0, int(a8._curio_taken.get(kind, 0))]) # A 链:同visit 第二购现价 = ceil(base×b^(l2−1)×A^1)(货架已全重抽——新卡新价同公式) @@ -5076,7 +5209,7 @@ func _meta_settle_and_workshop() -> void: var a12 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(12)) check(a12.start_adventure(12, {"attunement": ["incense_stick", "sky_eye"]}), "充能重置场景:装配冒险启动(seed 12)〔秘宝批 V8 换主语〕") - check(int(a12.attune_charges.get("incense_stick", -1)) == 1 and int(a12.attune_charges.get("sky_eye", -1)) == 1, + check(int(a12.run_ledger.attune_charges.get("incense_stick", -1)) == 1 and int(a12.run_ledger.attune_charges.get("sky_eye", -1)) == 1, # 迭代 27 T3——充能账住 RunLedger "充能账本按装备 active_charged 件重置(新目录表值:线香 battle 级 1 次/天眼 level 级 1 次——周期重置点归 V8 逐形态面)") a12._attunement = [] a12._attune_refresh_x = 1 @@ -5211,7 +5344,7 @@ func _smoke_tree() -> void: # ---- 天赋树六键行为冒烟(迭代 21 T3——V10:开局姿态窗/危险直觉限次/财富之道聚散/守侧失误补偿; -# 引擎直构 + 编排授予段直注(_judge_flag=结算授予段旗判定单口))---- +# 引擎直构 + 编排授予段直注(_grant_seg._judge_flag=结算授予段旗判定单口——迭代27 T2 随段迁址))---- func _smoke_tree_rules() -> void: print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---") # 开局姿态组:每关首轮窗(battle_index 事实——引擎直构) @@ -5244,7 +5377,7 @@ func _smoke_tree_rules() -> void: fz2.counters_seed(exported) check((fz2.dispatch("mine_hit", {}, Callable()) as Array).is_empty(), "锋:限次跨战持久(adventure 域 counters_export/seed——断点保留同源,不新增存档域不升 v7)") - # 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _judge_flag 直注 + # 守侧五场景(契约 12):编排 rig——begin_battle 携 sense_guard,授予段 _grant_seg._judge_flag 直注 var orch = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777001)) check(orch.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": 40, "hero_atk": 8, "hero_max_hp": 40, "hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "attune_charges": {}, @@ -5261,16 +5394,16 @@ func _smoke_tree_rules() -> void: check(safe.x >= 0, "守侧 rig:安全格定位(非雷格=标错负样本)") check(int(orch.action_economy.pending_ap_delta()) == 0, "守侧 rig:首轮后 queue 干净(开局 posture queue 已兑现)") # 场景①:当回合新插旗、结算判标错且旗仍在场 → queue +1(本轮 AP 零变化=queue 承载、下轮 notify_round_start 兑现) - orch._judge_flag({"cell": safe, "hit": false}, 1) + orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 1) check(int(orch.action_economy.pending_ap_delta()) == 1, "守①:当回合标错且旗在场 → 下轮 AP+1(queue 排队——本轮 AP 逐位零变化)") # 场景③④:之前回合标错重判(旗在场状态)与同格重复——去重闸挡回,最多返还一次 - orch._judge_flag({"cell": safe, "hit": false}, 2) - orch._judge_flag({"cell": safe, "hit": false}, 3) + orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 2) + orch._grant_seg._judge_flag({"cell": safe, "hit": false}, 3) check(int(orch.action_economy.pending_ap_delta()) == 1, "守③④:旧旗重判零触发+同格最多一次(_flag_miss_seen 去重闸=「当回合标错动作」判别器)") # 场景②负样本:标中旗(hit=true 正向兑付路径)零触发 - orch._judge_flag({"cell": safe, "hit": true}, 4) + orch._grant_seg._judge_flag({"cell": safe, "hit": true}, 4) check(int(orch.action_economy.pending_ap_delta()) == 1, "守②负样本:标中/被挖旗零触发(标错事件只在 miss 分支发射——发射面过滤承载)") # 场景⑤:末轮判出→无下一轮=queue 不兑现(编排器每战新建——队列实例随战亡,合法作废态) var orch2 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777002)) @@ -5284,10 +5417,10 @@ func _smoke_tree_rules() -> void: "守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)") # 按关重置清零点(adventure_runner._start_current_level) var ar = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(777003)) - ar.adventure_counters = {"tree_sense_sharp_used": 2, "tree_sense_guard_used": 1, "unrelated_key": 9} + ar.run_ledger.adventure_counters = {"tree_sense_sharp_used": 2, "tree_sense_guard_used": 1, "unrelated_key": 9} # 迭代 27 T3——计数账住 RunLedger ar._reset_tree_limited_counters() - check(not ar.adventure_counters.has("tree_sense_sharp_used") and not ar.adventure_counters.has("tree_sense_guard_used") - and int(ar.adventure_counters.get("unrelated_key", 0)) == 9, + check(not ar.run_ledger.adventure_counters.has("tree_sense_sharp_used") and not ar.run_ledger.adventure_counters.has("tree_sense_guard_used") + and int(ar.run_ledger.adventure_counters.get("unrelated_key", 0)) == 9, "限次按关重置:_start_current_level 清零点恰两键(他键零扰动——生命周期四态闭合)") # 财富之道·散:商品标价折价(level_runner 计价单点) var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004)) diff --git a/game/smoke/t17_defer_probe.gd b/game/smoke/t17_defer_probe.gd index 1837d9b7..90aa8fb1 100644 --- a/game/smoke/t17_defer_probe.gd +++ b/game/smoke/t17_defer_probe.gd @@ -45,32 +45,32 @@ func _ready() -> void: main.level_screen.press_candidate(0) for i in 20: await get_tree().process_frame - if main.settlement == null: + if main.run_scope.settlement == null: _fail("战斗未启动") return # 直挖至击杀胜利(settle_batch 真实管线——step_row 信号→排播队列,与点击路径同链) - var ms = main.settlement.minesweeper + var ms = main.run_scope.settlement.minesweeper var guard := 200 var t_ap_out := -1 # AP 用完/停手帧 var ap_clock := "" # 墙上时钟时间戳 var settle_clock := "" - while guard > 0 and String(main.settlement.battle_result) == "": + while guard > 0 and String(main.run_scope.settlement.battle_result) == "": guard -= 1 var plan = load("res://smoke/auto_player.gd").choose_dig(ms) if plan.is_empty(): - main.settlement.settle_batch([{"action": "stop", "cell": Vector2i(-1, -1)}]) + main.run_scope.settlement.settle_batch([{"action": "stop", "cell": Vector2i(-1, -1)}]) await get_tree().process_frame continue if plan.hero_cell != ms.hero_pos(): ms.settle_batch([{"action": "move", "cell": plan.hero_cell}]) ms.settle_batch([{"action": "dig", "cell": plan.dig_cell}]) - if t_ap_out < 0 and main.settlement.action_economy.phase != "Active": + if t_ap_out < 0 and main.run_scope.settlement.action_economy.phase != "Active": t_ap_out = Time.get_ticks_msec() ap_clock = _clock_ms() settle_clock = ap_clock # 触发结算与 AP 用完同帧(核心同步)——同一墙上时刻 await get_tree().process_frame - if String(main.settlement.battle_result) != "victory": - _fail("战斗未胜利(result='%s')" % str(main.settlement.battle_result)) + if String(main.run_scope.settlement.battle_result) != "victory": + _fail("战斗未胜利(result='%s')" % str(main.run_scope.settlement.battle_result)) return var t_settle := Time.get_ticks_msec() var _rows_at_victory: int = main._row_pacer.pending_rows().size() # 时间戳①:结算入队完成帧(AP 耗尽/停手→五行段同步跑完→行全在队) diff --git a/game/smoke/ui_click_probe.gd b/game/smoke/ui_click_probe.gd index 25631a2b..1415c6d0 100644 --- a/game/smoke/ui_click_probe.gd +++ b/game/smoke/ui_click_probe.gd @@ -269,7 +269,7 @@ func _ready() -> void: _problems.append("三选一停点打开时输入未锁定") if main.curio_overlay._continue_btn.visible: main.curio_overlay.press_continue() - elif main.adventure != null and not main.adventure._curio_pending.is_empty(): + elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty(): main.curio_overlay.press_card(0) main.curio_overlay.press_confirm() else: @@ -319,7 +319,7 @@ func _ready() -> void: next_btn.pressed.emit() await get_tree().process_frame await get_tree().process_frame - if main.runner == null or int(main.runner.level_row.get("level_index", -1)) != 2: + if main.run_scope.runner == null or int(main.run_scope.runner.level_row.get("level_index", -1)) != 2: _problems.append("大奖励确认后未推进到第二关(level_index != 2)") else: _done = true @@ -332,8 +332,8 @@ func _ready() -> void: if not main._input_locked(): _problems.append("胜利结算框打开时输入未锁定") var gp_line: String = "%s %s" % [main._end_overlay._gp_name.text, main._end_overlay._gp_val.text] # 两列账行(审计差距④)——名/值列拼回整串断言 - if not gp_line.contains("GP +%d" % main._last_gain_amount) or not gp_line.contains("合计 %d" % int(main.runner.gp)): - _problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main._last_gain_amount, int(main.runner.gp)]) + if not gp_line.contains("GP +%d" % main.run_scope.last_gain_amount) or not gp_line.contains("合计 %d" % int(main.run_scope.runner.gp)): + _problems.append("胜利结算框 GP 增量/总量与钱包实态不符(%s|实态 +%d/%d)" % [gp_line, main.run_scope.last_gain_amount, int(main.run_scope.runner.gp)]) if main._end_overlay.verdict_text() == "": _problems.append("胜利结算框判定行缺失") _settlement_confirms += 1 @@ -350,8 +350,8 @@ func _ready() -> void: # T17 演播后排窗适配:真终局 vs 中途结算面判据=adventure.state(冒险结构三态—— # 战斗胜利停点=running / 真终局(失败/放弃/打穿)=settling)。旧判据 # (_adventure_mode)在真终局仍 true——会把真终局当中途面点掉返回键(实证)。 - var _adv_running: bool = main.adventure != null and String(main.adventure.state) == "running" - if main._adventure_mode and _adv_running: + var _adv_running: bool = main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running" + if main.run_scope.is_adventure and _adv_running: main._end_overlay._btn.pressed.emit() continue if not main._input_locked(): @@ -360,12 +360,12 @@ func _ready() -> void: print("[click_probe] 冒险终局:%s · 选路口点击 %d 次" % [ main._end_overlay.verdict_text().replace("\n", " "), _route_clicks]) break - if main.settlement == null or main.runner == null: + if main.run_scope.settlement == null or main.run_scope.runner == null: continue - var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) + var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) var st := { - "node": main.runner.node_index, "round": main.settlement.round_index, - "phase": main.settlement.action_economy.phase, "ap": main.settlement.action_economy.ap, + "node": main.run_scope.runner.node_index, "round": main.run_scope.settlement.round_index, + "phase": main.run_scope.settlement.action_economy.phase, "ap": main.run_scope.settlement.action_economy.ap, "settling": diag.settling, } if JSON.stringify(st) == JSON.stringify(_last_state): @@ -384,7 +384,7 @@ func _ready() -> void: _ap_spent_this_round = 0 _last_state = st _locktest_tick() - if main.settlement.battle_result == "" and main.settlement.action_economy.phase == "Active": + if main.run_scope.settlement.battle_result == "" and main.run_scope.settlement.action_economy.phase == "Active": # T1 D1——锁测在飞期(state 1)冻结探针动作面:排空帧 autoplay 的免费 move 会抢在 # 帧末 deferred 重放之前改变英雄位(flag 拒收 not_adjacent 实证)——「注入→重放」 # 窗口内局面零变化正是 v3.1 被测语义(演播期冻结),动作面恢复待断言收口。 @@ -402,14 +402,14 @@ func _ready() -> void: await get_tree().process_frame if main._end_overlay.is_open(): # 中途结算面(state=running)=点继续推进;真终局(settling)=断点段前提满足,收口。 - if main._adventure_mode and main.adventure != null and String(main.adventure.state) == "running": + if main.run_scope.is_adventure and main.run_scope.adventure != null and String(main.run_scope.adventure.state) == "running": main._end_overlay._btn.pressed.emit() else: break elif main.curio_overlay.visible: if main.curio_overlay._continue_btn.visible: main.curio_overlay.press_continue() - elif main.adventure != null and not main.adventure._curio_pending.is_empty(): + elif main.run_scope.adventure != null and not main.run_scope.adventure._curio_pending.is_empty(): main.curio_overlay.press_card(0) main.curio_overlay.press_confirm() else: @@ -432,8 +432,8 @@ func _ready() -> void: for i in 10: await get_tree().process_frame break - elif main.settlement != null and main.settlement.battle_result == "" \ - and main.settlement.action_economy.phase == "Active": + elif main.run_scope.settlement != null and main.run_scope.settlement.battle_result == "" \ + and main.run_scope.settlement.action_economy.phase == "Active": _play_one_action() # 排播期/Waiting 等待态=自然流逝(演播秒级、锁随队列空解除) # T17 适配:战败或放弃二途皆达终局面(放弃=预算耗尽收口路径——见上),断点段前提不变。 @@ -474,11 +474,11 @@ func _ready() -> void: iso_chk.load_from_disk() if not iso_chk.has_breakpoint(): _problems.append("断点段:入口停点断点未写盘(卡①写面失守)") - var cand_before: Array = main.runner.next_candidate_ids() + var cand_before: Array = main.run_scope.runner.next_candidate_ids() # 合成「关游戏重开回菜单」(探针专用——真实路径无此入口;断点已在盘) main._exit_level_screen() main._start_overlay.visible = true - main.adventure = null + main.run_scope.adventure = null main._refresh_resume_button() await get_tree().process_frame if not resume_btn.visible: @@ -487,10 +487,10 @@ func _ready() -> void: resume_btn.pressed.emit() # 真实按钮信号路径 → _resume_from_menu → resume_adventure await get_tree().process_frame await get_tree().process_frame - if main.adventure == null or main.runner == null or not main.level_screen.visible: + if main.run_scope.adventure == null or main.run_scope.runner == null or not main.level_screen.visible: _problems.append("断点段:续玩未恢复至停点(关卡画面未现)") else: - var cand_after: Array = main.runner.next_candidate_ids() + var cand_after: Array = main.run_scope.runner.next_candidate_ids() if cand_after.size() != cand_before.size() or (cand_after.size() > 0 and int(cand_after[0]) != int(cand_before[0])): _problems.append("断点段:续玩候选与中断前不一致(停点重建失准)") if not iso_chk.load_from_disk() or not iso_chk.has_breakpoint(): @@ -498,12 +498,12 @@ func _ready() -> void: # ③ 互斥确认弹窗(二轮修改批 A):断点在档点开始冒险 → 弹确认框;取消=回菜单;开新冒险=提交 main._exit_level_screen() main._start_overlay.visible = true - main.adventure = null + main.run_scope.adventure = null main._refresh_resume_button() await get_tree().process_frame lv_btn.pressed.emit() await get_tree().process_frame - if not main.confirm_dialog.is_open() or main.adventure != null: + if not main.confirm_dialog.is_open() or main.run_scope.adventure != null: _problems.append("断点段:有断点点开始冒险未弹确认框(二轮 A 失守)") var cancel_btn: Button = ProbeUiUtil.find_button(main.confirm_dialog, "取消", "contains") if cancel_btn == null: @@ -511,7 +511,7 @@ func _ready() -> void: else: cancel_btn.pressed.emit() await get_tree().process_frame - if main.confirm_dialog.is_open() or main.adventure != null or not main._start_overlay.visible: + if main.confirm_dialog.is_open() or main.run_scope.adventure != null or not main._start_overlay.visible: _problems.append("断点段:取消未回开始菜单(二轮 A)") lv_btn.pressed.emit() await get_tree().process_frame @@ -522,7 +522,7 @@ func _ready() -> void: confirm_btn.pressed.emit() await get_tree().process_frame await get_tree().process_frame - if main.adventure == null or main._start_overlay.visible or main.confirm_dialog.is_open(): + if main.run_scope.adventure == null or main._start_overlay.visible or main.confirm_dialog.is_open(): _problems.append("断点段:确认开新冒险后未开局(二轮 A)") print("[click_probe] 循环退出(done=%s budget=%s)问题=%d" % [ str(_done), str(action_budget), _problems.size()]) @@ -610,24 +610,29 @@ 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) + # V5(迭代 27 T3——刷新双账收口):轮内计数单源=结算快照(main 副本已删)+ HUD 余量一致 + var used_now: int = int(main.run_scope.settlement.presentation_snapshot().get("refresh_used", 0)) + if used_now != 1: + _problems.append("刷新轮内计数错误(%d)" % used_now) + var remain_expect: int = maxi(0, int(main.db.action_economy.refresh_cap_per_round) - used_now) + if not main.ap_bar.refresh_remaining_text().contains(str(remain_expect)): + _problems.append("HUD 刷新余量与快照推导失符(HUD='%s' 应含 %d)" % [main.ap_bar.refresh_remaining_text(), remain_expect]) 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 +701,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 +721,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 +738,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 +772,20 @@ func _report_stall(st: Dictionary) -> void: why = "结算停滞:Waiting 相位无推进(结算应同步走完——窗口段已删除)" elif st.phase == "Active" and st.ap == 0: why = "Active 相位 AP=0(应在归零瞬间翻 Waiting)" - elif st.phase == "Idle" and main.settlement.battle_result == "": + elif st.phase == "Idle" and main.run_scope.settlement.battle_result == "": why = "Idle 相位但战斗未结束(begin 失败?)" else: - why = "状态无推进(battle_result='%s')" % str(main.settlement.battle_result) + why = "状态无推进(battle_result='%s')" % str(main.run_scope.settlement.battle_result) print("[click_probe] 🔴 卡死复现(PROBE_SEED=%s):%s" % [OS.get_environment("PROBE_SEED"), why]) _dump_state() get_tree().quit(2) func _dump_state() -> void: - var diag: Dictionary = main.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) + var diag: Dictionary = main.run_scope.settlement.diagnostic_snapshot() # 诊断观测面(私有字段零直读) print(" node_index=%s round=%s phase=%s ap=%s rap=%s" % [ - str(main.runner.node_index), str(main.settlement.round_index), - str(main.settlement.action_economy.phase), str(main.settlement.action_economy.ap), str(main.settlement.action_economy.rap)]) + str(main.run_scope.runner.node_index), str(main.run_scope.settlement.round_index), + str(main.run_scope.settlement.action_economy.phase), str(main.run_scope.settlement.action_economy.ap), str(main.run_scope.settlement.action_economy.rap)]) print(" battle_result='%s' settling=%s" % [ str(diag.battle_result), str(diag.settling)]) print(" board_installed=%s facts_path='%s' refresh_used=%s flags=%s" % [ diff --git a/game/smoke/ui_input_probe.gd b/game/smoke/ui_input_probe.gd index 3876cb81..f84b04f0 100644 --- a/game/smoke/ui_input_probe.gd +++ b/game/smoke/ui_input_probe.gd @@ -40,11 +40,11 @@ func _ready() -> void: # 命中函数探针全绿、真鼠标点不中——本 case 封住输入管线,⑬封住几何拟合。 if main.level_screen.visible: var ro = main.level_screen - var cands: Array = main.runner.next_candidate_ids() - var nds: Array = main.runner.nodes() + var cands: Array = main.run_scope.runner.next_candidate_ids() + var nds: Array = main.run_scope.runner.nodes() var tgt := {} for n in nds: - if main.runner.node_id_of(n) == int(cands[0]): + if main.run_scope.runner.node_id_of(n) == int(cands[0]): tgt = n var npos: Vector2 = ro.layout_probe(nds)[tgt] var gpos: Vector2 = ro.canvas_probe()["origin"] + npos @@ -63,30 +63,30 @@ func _ready() -> void: main.get_viewport().push_input(evc, true) await get_tree().process_frame await get_tree().process_frame - if main.level_screen.visible or main.runner.awaiting_choice(): - _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.runner.awaiting_choice())]) + if main.level_screen.visible or main.run_scope.runner.awaiting_choice(): + _pre_fail("图上真实点击未提交选路(overlay 可见=%s 选路态=%s)" % [str(main.level_screen.visible), str(main.run_scope.runner.awaiting_choice())]) for i in range(3): await get_tree().process_frame # ---- ⑬ 画布随图拟合(同 bug 的几何面回归):复刻分层公式独立推导最深节点内容 x, # 断言画布 min 尺寸覆盖之(open_choices 每 stop 拟合——L1 单链 Boss x=70+6*122=802 曾越界) var ro2 = main.level_screen - var nds2: Array = main.runner.nodes() + var nds2: Array = main.run_scope.runner.nodes() var layer2 := {} var inbound2 := {} for n2 in nds2: - for tid2 in main.runner.node_out_ids(n2): + for tid2 in main.run_scope.runner.node_out_ids(n2): inbound2[tid2] = true var queue2: Array = [] for n2 in nds2: - if not inbound2.has(main.runner.node_id_of(n2)): + if not inbound2.has(main.run_scope.runner.node_id_of(n2)): queue2.append(n2) layer2[n2] = 0 while not queue2.is_empty(): var cur2: Dictionary = queue2.pop_front() - for tid2 in main.runner.node_out_ids(cur2): + for tid2 in main.run_scope.runner.node_out_ids(cur2): var tg2 := {} for n2 in nds2: - if main.runner.node_id_of(n2) == tid2: + if main.run_scope.runner.node_id_of(n2) == tid2: tg2 = n2 if tg2.is_empty() or layer2.has(tg2): continue @@ -102,9 +102,9 @@ func _ready() -> void: if float(cvmin["min_size"].x) < need_x2 - 0.5: _pre_fail("画布拟合不足(min.x=%.0f < 最深内容 %.0f——深层节点不可点回归)" % [cvmin["min_size"].x, need_x2]) var bv = main.board_view - var ms = main.settlement.minesweeper if main.settlement != null else null + var ms = main.run_scope.settlement.minesweeper if main.run_scope.settlement != null else null if ms == null or bv.cells_probe().is_empty(): - _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.settlement)]) + _pre_fail("战斗未就绪(cells=%d settlement=%s)" % [bv.cells_probe().size(), str(main.run_scope.settlement)]) return bv.action_requested.connect(func(c: Vector2i, kind: String): _actions.append([c, kind])) bv.chord_attempted.connect(func(c: Vector2i): _chords.append(c)) @@ -426,13 +426,13 @@ func _ready() -> void: var p_lantern: Dictionary = main.build_adventure_params(main.save_manager, main.meta) if not (p_lantern.get("attunement", []) as Array).has("wide_scout_band"): _problems.append("case⑮ 装配未进开局参数面(attunement=%s 应含 wide_scout_band)" % str(p_lantern.get("attunement", []))) - main.settlement.attunement = ["wide_scout_band"] + main.run_scope.settlement.attunement = ["wide_scout_band"] main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 4: _problems.append("case⑮ 装配后视野未扩(lantern 应=4——呈现桥未生效)") if int(bv.cells_probe()[ring_cell].fog_state) != 0: _problems.append("case⑮ 装配后 dist==R+1 格仍被遮(应转可见)") - main.settlement.attunement = [] + main.run_scope.settlement.attunement = [] main._sync_treasure_glow() if int(bv.fog_params_probe()["lantern"]) != 0: _problems.append("case⑮ 卸装后视野未复原(lantern 应=0——契约复原语义显式重算)") diff --git a/game/smoke/ui_layout_probe.gd b/game/smoke/ui_layout_probe.gd index 459a729b..ce4cde07 100644 --- a/game/smoke/ui_layout_probe.gd +++ b/game/smoke/ui_layout_probe.gd @@ -43,7 +43,7 @@ func _ready() -> void: continue print("%-16s pos=%s size=%s min=%s hflag=%d vflag=%d" % [k, ctl.position, ctl.size, ctl.custom_minimum_size, ctl.size_flags_horizontal, ctl.size_flags_vertical]) - var root_box: Control = main.get_child(0) + var root_box: Control = main._adventure_screen.battle_root # 迭代 27 T1——屏壳化后战斗四区根迁冒险屏壳(序数寻址漂移改名寻址) print("four-zone root: anchors=(%s,%s)-(%s,%s) offsets=(%s,%s,%s,%s) size=%s" % [ root_box.anchor_left, root_box.anchor_top, root_box.anchor_right, root_box.anchor_bottom, root_box.offset_left, root_box.offset_top, root_box.offset_right, root_box.offset_bottom, root_box.size]) diff --git a/game/smoke/ui_probe.gd b/game/smoke/ui_probe.gd index 78ec22ce..095c957e 100644 --- a/game/smoke/ui_probe.gd +++ b/game/smoke/ui_probe.gd @@ -67,8 +67,8 @@ func _ready() -> void: problems.append("关卡开始未弹入口选路停点(level_screen 应可见)") elif main._battle_root.visible: problems.append("独立屏激活期战斗屏宿主未隐藏(零透出契约失守)") - elif main.runner == null or not main.runner.awaiting_choice() \ - or main.runner.next_candidate_ids().size() != 1: + elif main.run_scope.runner == null or not main.run_scope.runner.awaiting_choice() \ + or main.run_scope.runner.next_candidate_ids().size() != 1: problems.append("入口停点状态失守(选路态 + 入口单候选)") else: main.level_screen.press_candidate(0) @@ -82,7 +82,7 @@ func _ready() -> void: else: main.level_screen.abandon_btn.pressed.emit() await get_tree().process_frame - if main.adventure == null or String(main.adventure.state) != "running": + if main.run_scope.adventure == null or String(main.run_scope.adventure.state) != "running": problems.append("首战中放弃未被拒(战斗域单事务在飞——state 应保持 running)") # ---- 既有断言(回归基线;A 区进度条断言随 停点前移批 2026-09-23 退役——节点链进度由关卡画面独立屏承载)---- if main.board_view.cells_probe().is_empty(): @@ -91,7 +91,7 @@ func _ready() -> void: problems.append("board 未装载") if main.info_panel.attack_text().contains("—") or main.info_panel.armor_text().contains("—"): problems.append("C 区攻击/护甲未显示(%s | %s)" % [main.info_panel.attack_text(), main.info_panel.armor_text()]) - if main.settlement == null or main.settlement.battle_result != "": + if main.run_scope.settlement == null or main.run_scope.settlement.battle_result != "": problems.append("首战未处于进行中") if not main.log_view.parsed_text().contains("节点 1/"): problems.append("节点头日志缺失") @@ -193,7 +193,7 @@ func _ready() -> void: # ---- L2 单场段(debug_session 单场原语 + 去饱和接口可用性;雾格机制已整体移除,不再断言变体呈现)---- # 2026-09-13 单场用户入口已删——探针直调 debug_session 原语(battle_created/ready 仍经组合根接线; # _reset_run_scope 复现原 _start_single 的提示去重复位) - main._reset_run_scope() + main._begin_session(false) main._debug.start_single(2) await get_tree().process_frame await get_tree().process_frame @@ -212,17 +212,18 @@ func _ready() -> void: break main.board_view.set_desaturated(true) main.board_view.set_desaturated(false) - # ---- GP 终局行(迭代 6 T7 卡⑩):影子账清零转存+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板)---- + # ---- GP 终局行(迭代 6 T7 卡⑩):summary gp_final 键取数+胜利面板 GP 行呈现(C5:GP 行只上冒险终局面板; + # 迭代 27 T3——影子账随批退役,原清零转存断言换代为 summary 键断言)---- # (置于战斗态断言之后——合成收束会灰化站桩并清 settlement 引用,具污染性) - main._adventure_gp_view = 57 - 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 + "stopped_at": 0, "keystone_total": 30, "master_seed": 42, "gp_final": 57}) + main.meta.save = meta_saved if not main._end_overlay.is_open(): problems.append("冒险终局面板未开(合成摘要路径)") elif not main._end_overlay._gp_row.visible or not main._end_overlay._gp_val.text.contains("57"): diff --git a/game/smoke/ui_struct_probe.gd b/game/smoke/ui_struct_probe.gd index e2e479e2..5a388ea8 100644 --- a/game/smoke/ui_struct_probe.gd +++ b/game/smoke/ui_struct_probe.gd @@ -167,7 +167,7 @@ func _ready() -> void: ProbeUiUtil.isolate_save(main, "smoke_ui_struct_probe_resume", true) main.save_manager.set_breakpoint(_resume_bp_fixture()) main._refresh_resume_button() - main.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) + main.run_scope.runner = null # 全新启动等价态(真机=菜单直进续玩,无前置冒险——runner 从未赋值;探针此前推流会残留旧 runner) main._resume_from_menu() _settle(8) if not main._end_overlay.visible: @@ -181,7 +181,7 @@ func _ready() -> void: if not s.contains("合计 24"): problems.append("[resume] _gp_summary_text runner=null 守卫未取快照 gp(%s)" % s) # 恢复现场(resume 改写了 main 的 adventure/save 态——后续无屏依赖,仅为整洁) - main._level_active = false + main.run_scope.level_active = false # ---- 判定 ---- if problems.is_empty(): diff --git a/game/smoke/ui_tune_probe.gd b/game/smoke/ui_tune_probe.gd index 7938229b..a897d779 100644 --- a/game/smoke/ui_tune_probe.gd +++ b/game/smoke/ui_tune_probe.gd @@ -99,14 +99,14 @@ func _ready() -> void: # ⑧b 开冒险路径显式关浮窗(18卡⑦ T2 防护——DS_DEBUG 会话默认开态下开局不带窗,对局中经角标再开) if main.debug_overlay.visible: problems.append("开冒险路径未显式关浮窗(T2 防护失守)") - if int(main.adventure.master_seed) != 42: - problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.adventure.master_seed)) + if int(main.run_scope.adventure.master_seed) != 42: + problems.append("显式种子未达冒险(master_seed=%s ≠ 42——真实路径显式优先失守)" % str(main.run_scope.adventure.master_seed)) # 停点前移批 2026-09-23:关卡开始 = 入口选路停点——真实按钮路径选入口候选进首战再读呈现快照 if main.level_screen.visible: main.level_screen.press_candidate(0) await get_tree().process_frame await get_tree().process_frame - var hero: Dictionary = main.settlement.presentation_snapshot().hero + var hero: Dictionary = main.run_scope.settlement.presentation_snapshot().hero if int(hero.get("atk", -1)) != boosted_atk or int(hero.get("max_hp", -1)) != boosted_hp: problems.append("战斗加成单次生效违例(atk=%s max_hp=%s,期望 %d/%d)" % [ str(hero.get("atk", "?")), str(hero.get("max_hp", "?")), boosted_atk, boosted_hp]) @@ -132,19 +132,19 @@ func _ready() -> void: problems.append("对局中批量模拟/两层调试按钮未禁用(T2 并存态不开放失守)") # 当局数据六项值对(T3——数值与只读访问器逐项一致;种子=adventure.master_seed 现值非 _seed_value 重算) var rd: Dictionary = main.debug_overlay.run_data_probe() - var r = main.adventure.runner() + var r = main.run_scope.adventure.runner() if String(rd.get("seed", "")) != "42": problems.append("当局数据种子 ≠ master_seed 现值(%s)" % str(rd.get("seed"))) - if int(rd.get("level", -1)) != int(main.adventure.level_index): - problems.append("当局数据关卡失对(%s ≠ %s)" % [str(rd.get("level")), str(main.adventure.level_index)]) + if int(rd.get("level", -1)) != int(main.run_scope.adventure.level_index): + problems.append("当局数据关卡失对(%s ≠ %s)" % [str(rd.get("level")), str(main.run_scope.adventure.level_index)]) if int(rd.get("node", -999)) != int(r.node_index): problems.append("当局数据节点失对(%s ≠ %s)" % [str(rd.get("node")), str(r.node_index)]) if String(rd.get("hp", "")) != "%d/%d" % [int(r.hero_hp), int(r.effective_max_hp())]: problems.append("当局数据 HP 失对(%s)" % str(rd.get("hp"))) if int(rd.get("gp", -1)) != int(r.gp): problems.append("当局数据 GP 失对(%s ≠ %s)" % [str(rd.get("gp")), str(r.gp)]) - if int(rd.get("keystone", -1)) != int(main.adventure.keystone): - problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.adventure.keystone)]) + if int(rd.get("keystone", -1)) != int(main.run_scope.adventure.keystone): + problems.append("当局数据钥石失对(%s ≠ %s)" % [str(rd.get("keystone")), str(main.run_scope.adventure.keystone)]) var close_btn: Button = ProbeUiUtil.find_button(main.debug_overlay, "关闭", "prefix") if close_btn == null: problems.append("浮窗关闭钮缺失(真实收窗路径中断)") @@ -161,14 +161,14 @@ func _ready() -> void: main.debug_overlay.seed_edit.text = "777" if main._seed_value() != 777: problems.append("显式种子输入未优先(_seed_value != 777——C1 ①失守)") - var fresh_sm = load("res://core/save_manager.gd").new("user://smoke_tune_probe_seed.json") - fresh_sm.load_from_disk() # 全新空档——first_adventure_done=false(首局态;新档缺省面) - var keep_sm = main.save_manager - main.save_manager = fresh_sm + # 迭代 27 T1——SaveManager 升格 autoload 后换注改原地 attach(首局态挂载+恢复隔离档); + # --script 模式无裸 autoload 标识符——经 main 节点路径解析 + var sm = main.get_node("/root/SaveManager") + sm.attach_path("user://smoke_tune_probe_seed.json") # 全新空档——first_adventure_done=false(首局态;新档缺省面) main.debug_overlay.seed_edit.text = "" if main._seed_value() != 42: problems.append("首局空框种子 != 42(P5 甲失守——教学态可复现破约)") - main.save_manager = keep_sm # 恢复隔离档(非首局态) + sm.attach_path("user://smoke_tune_probe_save.json") # 恢复隔离档(非首局态——isolate_save 已建档置位) var draws: Array[int] = [] for i in range(4): draws.append(main._seed_value()) diff --git a/game/tests/evidence/parity/anchor.json b/game/tests/evidence/parity/anchor.json new file mode 100644 index 00000000..c4fc1a2f --- /dev/null +++ b/game/tests/evidence/parity/anchor.json @@ -0,0 +1,166 @@ +{ + "variants": { + "base": { + "runsum": [ + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":39,\"flag_hit_gain\":208,\"flag_miss\":8,\"flag_miss_penalty\":16,\"flag_placed\":51,\"hero_dmg\":13,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}}],\"rounds\":11,\"route\":\"min\",\"seed\":71000,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":29,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":69,\"flag_hit_gain\":397,\"flag_miss\":6,\"flag_miss_penalty\":14,\"flag_placed\":81,\"hero_dmg\":30,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":79,\"heal_gained\":5,\"hp_after\":17,\"hp_before\":12,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":17,\"hp_before\":17,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":29,\"route\":\"min\",\"seed\":71001,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":46,\"visited_count\":9,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":34,\"flag_hit_gain\":388,\"flag_miss\":6,\"flag_miss_penalty\":6,\"flag_placed\":48,\"hero_dmg\":36,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":13,\"route\":\"min\",\"seed\":71002,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":44,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":1,\"choices\":0,\"cleared\":false,\"encounter_max_hp_seq\":[26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":6,\"flag_hit_gain\":-9,\"flag_miss\":7,\"flag_miss_penalty\":8,\"flag_placed\":13,\"hero_dmg\":4,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·赏战整\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":3,\"route\":\"min\",\"seed\":71003,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":20,\"visited_count\":1,\"wallet_count\":2}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":1,\"choices\":0,\"cleared\":false,\"encounter_max_hp_seq\":[26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":3,\"flag_hit_gain\":65,\"flag_miss\":2,\"flag_miss_penalty\":16,\"flag_placed\":5,\"hero_dmg\":4,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":1,\"route\":\"min\",\"seed\":71004,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":12,\"visited_count\":1,\"wallet_count\":2}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":80,\"flag_hit_gain\":138,\"flag_miss\":8,\"flag_miss_penalty\":24,\"flag_placed\":92,\"hero_dmg\":26,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"三干宽门\"],\"levels_cleared\":1,\"mine_dmg\":8,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"ap\",\"tier\":\"rare\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":5,\"hp_after\":10,\"hp_before\":5,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":10,\"hp_before\":10,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":25,\"route\":\"min\",\"seed\":71005,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":34,\"visited_count\":8,\"wallet_count\":7}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":65,\"flag_hit_gain\":286,\"flag_miss\":9,\"flag_miss_penalty\":22,\"flag_placed\":81,\"hero_dmg\":28,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":14,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"chain_traps\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":33,\"heal_gained\":5,\"hp_after\":14,\"hp_before\":9,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":14,\"hp_before\":14,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":25,\"route\":\"min\",\"seed\":71006,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":42,\"visited_count\":8,\"wallet_count\":7}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":48,\"flag_hit_gain\":319,\"flag_miss\":11,\"flag_miss_penalty\":8,\"flag_placed\":65,\"hero_dmg\":16,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":10,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"rap\",\"tier\":\"rare\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"rap\",\"tier\":\"rare\",\"value\":3}}],\"rounds\":16,\"route\":\"min\",\"seed\":71007,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":30,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":50,\"flag_hit_gain\":-30,\"flag_miss\":5,\"flag_miss_penalty\":14,\"flag_placed\":58,\"hero_dmg\":24,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}}],\"rounds\":19,\"route\":\"min\",\"seed\":71008,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":24,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":5,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":24,\"flag_hit_gain\":360,\"flag_miss\":6,\"flag_miss_penalty\":8,\"flag_placed\":35,\"hero_dmg\":17,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"rap\",\"tier\":\"rare\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"rap\",\"tier\":\"rare\",\"value\":3}}],\"rounds\":10,\"route\":\"min\",\"seed\":71009,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":33,\"visited_count\":6,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":52,\"flag_hit_gain\":255,\"flag_miss\":9,\"flag_miss_penalty\":6,\"flag_placed\":65,\"hero_dmg\":33,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":6,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"attune_battery\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attune_battery\",\"tier\":\"rare\",\"value\":1}}],\"rounds\":20,\"route\":\"min\",\"seed\":71010,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":39,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":10,\"flag_hit_gain\":178,\"flag_miss\":6,\"flag_miss_penalty\":6,\"flag_placed\":16,\"hero_dmg\":4,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":24,\"mine_hits\":5,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":4,\"route\":\"min\",\"seed\":71011,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":28,\"visited_count\":2,\"wallet_count\":3}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":10,\"flag_hit_gain\":79,\"flag_miss\":5,\"flag_miss_penalty\":6,\"flag_placed\":17,\"hero_dmg\":6,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":24,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":4,\"route\":\"min\",\"seed\":71012,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":30,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":23,\"flag_hit_gain\":64,\"flag_miss\":6,\"flag_miss_penalty\":8,\"flag_placed\":31,\"hero_dmg\":11,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":30,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seed\":71013,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":41,\"visited_count\":5,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":47,\"flag_hit_gain\":366,\"flag_miss\":6,\"flag_miss_penalty\":6,\"flag_placed\":54,\"hero_dmg\":21,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}}],\"rounds\":17,\"route\":\"min\",\"seed\":71014,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":37,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":36,\"flag_hit_gain\":891,\"flag_miss\":3,\"flag_miss_penalty\":0,\"flag_placed\":46,\"hero_dmg\":12,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":10,\"route\":\"min\",\"seed\":71015,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":28,\"visited_count\":4,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":5,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":34,\"flag_hit_gain\":-50,\"flag_miss\":4,\"flag_miss_penalty\":16,\"flag_placed\":41,\"hero_dmg\":20,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}}],\"rounds\":12,\"route\":\"min\",\"seed\":71016,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":28,\"visited_count\":6,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":5,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":31,\"flag_hit_gain\":545,\"flag_miss\":5,\"flag_miss_penalty\":0,\"flag_placed\":42,\"hero_dmg\":24,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}}],\"rounds\":10,\"route\":\"min\",\"seed\":71017,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":40,\"visited_count\":6,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":29,\"flag_hit_gain\":-119,\"flag_miss\":6,\"flag_miss_penalty\":6,\"flag_placed\":41,\"hero_dmg\":21,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":5,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"blood_tax\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"blood_tax\",\"feat_name\":\"血税\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":13,\"route\":\"min\",\"seed\":71018,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":35,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":1,\"choices\":0,\"cleared\":false,\"encounter_max_hp_seq\":[26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":4,\"flag_hit_gain\":5,\"flag_miss\":3,\"flag_miss_penalty\":14,\"flag_placed\":11,\"hero_dmg\":9,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":2,\"route\":\"min\",\"seed\":71019,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":17,\"visited_count\":1,\"wallet_count\":2}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":114,\"flag_hit_gain\":149,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":124,\"hero_dmg\":69,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"frontier_miner\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"perfect_insurance\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"perfect_insurance\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":34,\"heal_gained\":0,\"hp_after\":26,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"frontier_miner\",\"feat_name\":\"边陲矿工\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":36,\"route\":\"min\",\"seed\":71020,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":69,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":11,\"choices\":12,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":190,\"flag_hit_gain\":1509,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":205,\"hero_dmg\":86,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"market_close\",\"executioner\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":50,\"heal_gained\":5,\"hp_after\":18,\"hp_before\":13,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":18,\"hp_before\":18,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":18,\"heal_gained\":7,\"hp_after\":19,\"hp_before\":12,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":19,\"hp_before\":19,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":51,\"route\":\"min\",\"seed\":71021,\"shop_gp_seq\":[91],\"stopped_at\":3,\"taken\":86,\"visited_count\":15,\"wallet_count\":23}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":9,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":124,\"flag_hit_gain\":216,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":129,\"hero_dmg\":78,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"executioner\",\"seal_stake\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":28,\"heal_gained\":3,\"hp_after\":26,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":41,\"route\":\"min\",\"seed\":71022,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":78,\"visited_count\":11,\"wallet_count\":10}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":17,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35,35,35,35,35,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":242,\"flag_hit_gain\":1437,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":257,\"hero_dmg\":120,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"breaker_strike\",\"stone_skin\",\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":31,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":16,\"heal_gained\":7,\"hp_after\":13,\"hp_before\":6,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":13,\"hp_before\":13,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":82,\"route\":\"min\",\"seed\":71023,\"shop_gp_seq\":[49,60],\"stopped_at\":3,\"taken\":120,\"visited_count\":20,\"wallet_count\":26}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35,35,35,35,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":238,\"flag_hit_gain\":-139,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":250,\"hero_dmg\":120,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"breaker_strike\",\"executioner\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":29,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":28,\"heal_gained\":7,\"hp_after\":21,\"hp_before\":14,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":21,\"hp_before\":21,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":74,\"route\":\"min\",\"seed\":71024,\"shop_gp_seq\":[53,64],\"stopped_at\":3,\"taken\":120,\"visited_count\":19,\"wallet_count\":26}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38,38,38,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":255,\"flag_hit_gain\":-235,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":277,\"hero_dmg\":100,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"warning_beacon\",\"stone_skin\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"refresh\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"rap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"rap\",\"tier\":\"rare\",\"value\":3}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":45,\"heal_gained\":3,\"hp_after\":26,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"warning_beacon\",\"feat_name\":\"示警烟火\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":11,\"heal_gained\":7,\"hp_after\":36,\"hp_before\":29,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":36,\"hp_before\":36,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":79,\"route\":\"min\",\"seed\":71025,\"shop_gp_seq\":[76,49],\"stopped_at\":0,\"taken\":100,\"visited_count\":19,\"wallet_count\":28}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38,38,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":211,\"flag_hit_gain\":1073,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":221,\"hero_dmg\":99,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"hard_hat\",\"venom_edge\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":34,\"heal_gained\":5,\"hp_after\":26,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"venom_edge\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":21,\"heal_gained\":7,\"hp_after\":27,\"hp_before\":20,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"venom_edge\",\"feat_name\":\"淬毒刃纹\",\"hp_after\":27,\"hp_before\":27,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":62,\"route\":\"min\",\"seed\":71026,\"shop_gp_seq\":[68,48],\"stopped_at\":0,\"taken\":99,\"visited_count\":19,\"wallet_count\":26}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35,35,35,35,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":232,\"flag_hit_gain\":402,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":254,\"hero_dmg\":114,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"fuse_wire\",\"seal_stake\",\"breaker_strike\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"fuse_wire\",\"feat_name\":\"保险丝\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":41,\"heal_gained\":3,\"hp_after\":26,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":23,\"heal_gained\":7,\"hp_after\":20,\"hp_before\":13,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":20,\"hp_before\":20,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seed\":71027,\"shop_gp_seq\":[65,48],\"stopped_at\":0,\"taken\":114,\"visited_count\":19,\"wallet_count\":25}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35,35,35,35,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":240,\"flag_hit_gain\":1902,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":253,\"hero_dmg\":93,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"map_exchange\",\"seal_smith\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":40,\"heal_gained\":5,\"hp_after\":23,\"hp_before\":18,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":13,\"heal_gained\":7,\"hp_after\":22,\"hp_before\":15,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":22,\"hp_before\":22,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":72,\"route\":\"min\",\"seed\":71028,\"shop_gp_seq\":[61,34],\"stopped_at\":0,\"taken\":93,\"visited_count\":19,\"wallet_count\":24}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":119,\"flag_hit_gain\":557,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":127,\"hero_dmg\":73,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"fuse_wire\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"fuse_wire\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":47,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"fuse_wire\",\"feat_name\":\"保险丝\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":35,\"route\":\"min\",\"seed\":71029,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":73,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38,38,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":203,\"flag_hit_gain\":723,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":213,\"hero_dmg\":92,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"hard_hat\",\"flag_burst\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":49,\"heal_gained\":5,\"hp_after\":25,\"hp_before\":20,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":25,\"hp_before\":25,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"flag_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":7,\"hp_after\":38,\"hp_before\":31,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"flag_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":60,\"route\":\"min\",\"seed\":71030,\"shop_gp_seq\":[85,35],\"stopped_at\":0,\"taken\":92,\"visited_count\":19,\"wallet_count\":26}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":133,\"flag_hit_gain\":616,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":145,\"hero_dmg\":79,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":33,\"heal_gained\":7,\"hp_after\":31,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":31,\"hp_before\":31,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":40,\"route\":\"min\",\"seed\":71031,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":79,\"visited_count\":10,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":14,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38,38,38,38,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":217,\"flag_hit_gain\":1602,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":238,\"hero_dmg\":110,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"stone_skin\",\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":43,\"heal_gained\":5,\"hp_after\":26,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":18,\"heal_gained\":7,\"hp_after\":34,\"hp_before\":27,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":34,\"hp_before\":34,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":69,\"route\":\"min\",\"seed\":71032,\"shop_gp_seq\":[73],\"stopped_at\":3,\"taken\":110,\"visited_count\":17,\"wallet_count\":24}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35,35,35,35,35,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":160,\"flag_hit_gain\":693,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":187,\"hero_dmg\":50,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"blood_tax\",\"map_exchange\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_tax\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":38,\"heal_gained\":5,\"hp_after\":15,\"hp_before\":10,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_tax\",\"feat_name\":\"血税\",\"hp_after\":15,\"hp_before\":15,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":11,\"heal_gained\":7,\"hp_after\":33,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":33,\"hp_before\":33,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":46,\"route\":\"min\",\"seed\":71033,\"shop_gp_seq\":[50,42],\"stopped_at\":0,\"taken\":50,\"visited_count\":19,\"wallet_count\":25}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":17,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,38,38,38,38,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":278,\"flag_hit_gain\":1426,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":292,\"hero_dmg\":114,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"seal_stake\",\"trap_instinct\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":34,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":4,\"hp_after\":38,\"hp_before\":34,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":38,\"hp_before\":38,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":89,\"route\":\"min\",\"seed\":71034,\"shop_gp_seq\":[85,70],\"stopped_at\":0,\"taken\":114,\"visited_count\":20,\"wallet_count\":30}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38,38,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":231,\"flag_hit_gain\":1217,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":243,\"hero_dmg\":100,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"chain_traps\",\"map_exchange\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":59,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":7,\"hp_after\":21,\"hp_before\":14,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":21,\"hp_before\":21,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":70,\"route\":\"min\",\"seed\":71035,\"shop_gp_seq\":[88,40],\"stopped_at\":0,\"taken\":100,\"visited_count\":19,\"wallet_count\":27}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":118,\"flag_hit_gain\":430,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":127,\"hero_dmg\":76,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"seal_smith\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":98,\"heal_gained\":5,\"hp_after\":22,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":22,\"hp_before\":22,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":36,\"route\":\"min\",\"seed\":71036,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":76,\"visited_count\":11,\"wallet_count\":10}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":121,\"flag_hit_gain\":1364,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":124,\"hero_dmg\":60,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"seal_smith\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":87,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":35,\"route\":\"min\",\"seed\":71037,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":60,\"visited_count\":10,\"wallet_count\":10}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35,35,35,35,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":220,\"flag_hit_gain\":1090,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":231,\"hero_dmg\":116,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"chain_traps\",\"geologist\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":39,\"heal_gained\":5,\"hp_after\":22,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":22,\"hp_before\":22,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":7,\"hp_after\":23,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"geologist\",\"feat_name\":\"地质学家\",\"hp_after\":23,\"hp_before\":23,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":71,\"route\":\"min\",\"seed\":71038,\"shop_gp_seq\":[56,58],\"stopped_at\":0,\"taken\":116,\"visited_count\":19,\"wallet_count\":26}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":10,\"choices\":12,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":177,\"flag_hit_gain\":1689,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":185,\"hero_dmg\":98,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"skip_mining\",\"seal_echo\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":39,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":52,\"route\":\"min\",\"seed\":71039,\"shop_gp_seq\":[72],\"stopped_at\":2,\"taken\":98,\"visited_count\":14,\"wallet_count\":20}" + ], + "jsonl": { + "normal": { + "sha256": "4d3169532b996fdf04015ba8815b45fb236e44d25afc9ffa1f217846c4f25e77", + "rows": 8051 + }, + "perfect": { + "sha256": "8882d40d6d38db36f82ff010efe5e2b806b3b3b3dc0fccefad552edf13ba8707", + "rows": 41266 + } + } + }, + "attune": { + "runsum": [ + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":50,\"flag_hit_gain\":604,\"flag_miss\":8,\"flag_miss_penalty\":22,\"flag_placed\":60,\"hero_dmg\":18,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":24,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}}],\"rounds\":16,\"route\":\"min\",\"seed\":72000,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":42,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":57,\"flag_hit_gain\":416,\"flag_miss\":9,\"flag_miss_penalty\":16,\"flag_placed\":73,\"hero_dmg\":25,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":6,\"mine_hits\":9,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"time_sand\",\"tier\":\"rare\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}}],\"rounds\":17,\"route\":\"min\",\"seed\":72001,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":31,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":37,\"flag_hit_gain\":-73,\"flag_miss\":10,\"flag_miss_penalty\":14,\"flag_placed\":52,\"hero_dmg\":23,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":11,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":15,\"route\":\"min\",\"seed\":72002,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":31,\"visited_count\":5,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":60,\"flag_hit_gain\":192,\"flag_miss\":7,\"flag_miss_penalty\":0,\"flag_placed\":69,\"hero_dmg\":22,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":24,\"mine_hits\":10,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"trap_burst\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"trap_burst\",\"feat_name\":\"陷阱爆发\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":18,\"route\":\"min\",\"seed\":72003,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":46,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":19,\"flag_hit_gain\":-202,\"flag_miss\":5,\"flag_miss_penalty\":0,\"flag_placed\":27,\"hero_dmg\":10,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":6,\"route\":\"min\",\"seed\":72004,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":26,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":34,\"flag_hit_gain\":-7,\"flag_miss\":3,\"flag_miss_penalty\":14,\"flag_placed\":39,\"hero_dmg\":17,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":9,\"route\":\"min\",\"seed\":72005,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":17,\"visited_count\":4,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":66,\"flag_hit_gain\":134,\"flag_miss\":8,\"flag_miss_penalty\":6,\"flag_placed\":77,\"hero_dmg\":33,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attune_battery\",\"tier\":\"rare\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attune_battery\",\"tier\":\"rare\",\"value\":1}}],\"rounds\":20,\"route\":\"min\",\"seed\":72006,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":41,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":9,\"choices\":11,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,35],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":127,\"flag_hit_gain\":495,\"flag_miss\":9,\"flag_miss_penalty\":32,\"flag_placed\":144,\"hero_dmg\":39,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":12,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"fuse_wire\",\"field_dressing\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"fuse_wire\",\"feat_name\":\"保险丝\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":5,\"hp_after\":17,\"hp_before\":12,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":17,\"hp_before\":17,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":37,\"route\":\"min\",\"seed\":72007,\"shop_gp_seq\":[51],\"stopped_at\":2,\"taken\":55,\"visited_count\":13,\"wallet_count\":17}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":15,\"flag_hit_gain\":184,\"flag_miss\":3,\"flag_miss_penalty\":8,\"flag_placed\":23,\"hero_dmg\":8,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":2,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":5,\"route\":\"min\",\"seed\":72008,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":24,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":11,\"flag_hit_gain\":-8,\"flag_miss\":2,\"flag_miss_penalty\":8,\"flag_placed\":15,\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":3,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":3,\"route\":\"min\",\"seed\":72009,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":19,\"visited_count\":2,\"wallet_count\":3}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":39,\"flag_hit_gain\":494,\"flag_miss\":12,\"flag_miss_penalty\":6,\"flag_placed\":55,\"hero_dmg\":16,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":22,\"mine_hits\":10,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"rap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"rap\",\"tier\":\"rare\",\"value\":3}}],\"rounds\":13,\"route\":\"min\",\"seed\":72010,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":38,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":5,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":24,\"flag_hit_gain\":445,\"flag_miss\":3,\"flag_miss_penalty\":6,\"flag_placed\":30,\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attune_battery\",\"tier\":\"rare\"},{\"kind\":\"rap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attune_battery\",\"tier\":\"rare\",\"value\":1}}],\"rounds\":6,\"route\":\"min\",\"seed\":72011,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":31,\"visited_count\":6,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":21,\"flag_hit_gain\":116,\"flag_miss\":5,\"flag_miss_penalty\":8,\"flag_placed\":29,\"hero_dmg\":14,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seed\":72012,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":30,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,36,36,36,36,36],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":106,\"flag_hit_gain\":435,\"flag_miss\":15,\"flag_miss_penalty\":40,\"flag_placed\":135,\"hero_dmg\":48,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":11,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"map_exchange\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":47,\"heal_gained\":6,\"hp_after\":36,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":36,\"hp_before\":36,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":35,\"route\":\"min\",\"seed\":72013,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":48,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":52,\"flag_hit_gain\":410,\"flag_miss\":6,\"flag_miss_penalty\":14,\"flag_placed\":64,\"hero_dmg\":25,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"三干中廊\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":7,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"seal_stake\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":28,\"heal_gained\":5,\"hp_after\":9,\"hp_before\":4,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":9,\"hp_before\":9,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":18,\"route\":\"min\",\"seed\":72014,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":41,\"visited_count\":8,\"wallet_count\":7}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":75,\"flag_hit_gain\":375,\"flag_miss\":9,\"flag_miss_penalty\":16,\"flag_placed\":89,\"hero_dmg\":31,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":1,\"mine_dmg\":8,\"mine_hits\":8,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[\"trap_instinct\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":5,\"hp_after\":16,\"hp_before\":11,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":16,\"hp_before\":16,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":24,\"route\":\"min\",\"seed\":72015,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":39,\"visited_count\":9,\"wallet_count\":8}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,36,36],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":43,\"flag_hit_gain\":149,\"flag_miss\":7,\"flag_miss_penalty\":24,\"flag_placed\":55,\"hero_dmg\":14,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":9,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}}],\"rounds\":11,\"route\":\"min\",\"seed\":72016,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":28,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":26,\"flag_hit_gain\":-23,\"flag_miss\":8,\"flag_miss_penalty\":8,\"flag_placed\":35,\"hero_dmg\":14,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":9,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":10,\"route\":\"min\",\"seed\":72017,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":22,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":50,\"flag_hit_gain\":-84,\"flag_miss\":5,\"flag_miss_penalty\":8,\"flag_placed\":62,\"hero_dmg\":22,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}}],\"rounds\":17,\"route\":\"min\",\"seed\":72018,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":38,\"visited_count\":7,\"wallet_count\":7}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":21,\"flag_hit_gain\":257,\"flag_miss\":4,\"flag_miss_penalty\":22,\"flag_placed\":32,\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":4,\"policy\":\"normal\",\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seed\":72019,\"shop_gp_seq\":[],\"stopped_at\":1,\"taken\":5,\"visited_count\":4,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":131,\"flag_hit_gain\":975,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":142,\"hero_dmg\":84,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"breaker_strike\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"rap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"rap\",\"tier\":\"rare\",\"value\":3}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":51,\"heal_gained\":5,\"hp_after\":25,\"hp_before\":20,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":25,\"hp_before\":25,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":36,\"route\":\"min\",\"seed\":72020,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":84,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":124,\"flag_hit_gain\":287,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":133,\"hero_dmg\":65,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"blast_accounting\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blast_accounting\",\"feat_name\":\"爆破会计\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":29,\"route\":\"min\",\"seed\":72021,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":65,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":9,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":166,\"flag_hit_gain\":1207,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":179,\"hero_dmg\":93,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"breaker_strike\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"},{\"kind\":\"rap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":46,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":44,\"route\":\"min\",\"seed\":72022,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":93,\"visited_count\":11,\"wallet_count\":10}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":10,\"choices\":11,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":155,\"flag_hit_gain\":220,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":166,\"hero_dmg\":78,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"attune_battery\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attune_battery\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":35,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":49,\"route\":\"min\",\"seed\":72023,\"shop_gp_seq\":[74],\"stopped_at\":2,\"taken\":78,\"visited_count\":13,\"wallet_count\":20}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,35,35,35,35,35,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":238,\"flag_hit_gain\":2427,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":263,\"hero_dmg\":115,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"trap_burst\",\"stone_skin\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"trap_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":28,\"heal_gained\":5,\"hp_after\":24,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_burst\",\"feat_name\":\"陷阱爆发\",\"hp_after\":24,\"hp_before\":24,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":29,\"heal_gained\":7,\"hp_after\":24,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":24,\"hp_before\":24,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":66,\"route\":\"min\",\"seed\":72024,\"shop_gp_seq\":[60,60],\"stopped_at\":0,\"taken\":115,\"visited_count\":19,\"wallet_count\":28}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":107,\"flag_hit_gain\":431,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":124,\"hero_dmg\":61,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":33,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":29,\"route\":\"min\",\"seed\":72025,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":61,\"visited_count\":10,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38,38,38,38,38,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":289,\"flag_hit_gain\":2172,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":309,\"hero_dmg\":108,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"blood_insight\",\"last_drop\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":41,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_insight\",\"feat_name\":\"见血开窍\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":24,\"heal_gained\":7,\"hp_after\":23,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":23,\"hp_before\":23,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":73,\"route\":\"min\",\"seed\":72026,\"shop_gp_seq\":[89,62],\"stopped_at\":0,\"taken\":108,\"visited_count\":19,\"wallet_count\":31}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":118,\"flag_hit_gain\":490,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":128,\"hero_dmg\":65,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"map_exchange\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":48,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":32,\"route\":\"min\",\"seed\":72027,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":65,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":16,\"choices\":18,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,38,38,38,38,38,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":304,\"flag_hit_gain\":1216,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":326,\"hero_dmg\":111,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"trap_instinct\",\"stone_skin\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":38,\"heal_gained\":0,\"hp_after\":26,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":30,\"heal_gained\":7,\"hp_after\":28,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":28,\"hp_before\":28,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":77,\"route\":\"min\",\"seed\":72028,\"shop_gp_seq\":[85,59],\"stopped_at\":0,\"taken\":111,\"visited_count\":21,\"wallet_count\":31}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":125,\"flag_hit_gain\":26,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":131,\"hero_dmg\":72,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":76,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":35,\"route\":\"min\",\"seed\":72029,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":72,\"visited_count\":10,\"wallet_count\":10}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":122,\"flag_hit_gain\":184,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":136,\"hero_dmg\":53,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"trap_instinct\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":5,\"hp_after\":26,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_instinct\",\"feat_name\":\"陷阱本能\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":34,\"route\":\"min\",\"seed\":72030,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":53,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":16,\"choices\":17,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,41,41,41,41,41,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":299,\"flag_hit_gain\":1950,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":320,\"hero_dmg\":97,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"seal_echo\",\"executioner\",\"seal_stake\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":40,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":40,\"heal_gained\":6,\"hp_after\":41,\"hp_before\":35,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":41,\"hp_before\":41,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":78,\"route\":\"min\",\"seed\":72031,\"shop_gp_seq\":[111,73],\"stopped_at\":0,\"taken\":97,\"visited_count\":20,\"wallet_count\":33}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38,38,38,38,38],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":185,\"flag_hit_gain\":1671,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":200,\"hero_dmg\":61,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"flag_burst\",\"executioner\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"flag_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":55,\"heal_gained\":3,\"hp_after\":26,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"flag_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":22,\"heal_gained\":7,\"hp_after\":26,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":26,\"hp_before\":26,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":43,\"route\":\"min\",\"seed\":72032,\"shop_gp_seq\":[72,39],\"stopped_at\":0,\"taken\":61,\"visited_count\":19,\"wallet_count\":26}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,36,36,36,36,45,45,45,45,45,51],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":263,\"flag_hit_gain\":1731,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":288,\"hero_dmg\":108,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"warning_beacon\",\"last_drop\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":7,\"hp_after\":26,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"warning_beacon\",\"feat_name\":\"示警烟火\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":28,\"heal_gained\":2,\"hp_after\":45,\"hp_before\":43,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":45,\"hp_before\":45,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":70,\"route\":\"min\",\"seed\":72033,\"shop_gp_seq\":[65,77],\"stopped_at\":0,\"taken\":108,\"visited_count\":19,\"wallet_count\":28}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38,38,38,38,38,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":272,\"flag_hit_gain\":91,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":289,\"hero_dmg\":101,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"last_stand\",\"seal_echo\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_stand\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":49,\"heal_gained\":5,\"hp_after\":24,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_stand\",\"feat_name\":\"背水\",\"hp_after\":24,\"hp_before\":24,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":21,\"heal_gained\":7,\"hp_after\":26,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":26,\"hp_before\":26,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":73,\"route\":\"min\",\"seed\":72034,\"shop_gp_seq\":[95,60],\"stopped_at\":0,\"taken\":101,\"visited_count\":19,\"wallet_count\":31}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38,38,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":242,\"flag_hit_gain\":-570,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":255,\"hero_dmg\":103,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":2,\"relics\":[\"market_close\",\"seal_stake\",\"venom_edge\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":44,\"heal_gained\":5,\"hp_after\":20,\"hp_before\":15,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":20,\"hp_before\":20,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"venom_edge\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":36,\"heal_gained\":7,\"hp_after\":19,\"hp_before\":12,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"venom_edge\",\"feat_name\":\"淬毒刃纹\",\"hp_after\":19,\"hp_before\":19,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":71,\"route\":\"min\",\"seed\":72035,\"shop_gp_seq\":[84,59],\"stopped_at\":0,\"taken\":103,\"visited_count\":19,\"wallet_count\":28}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,41,41,41,41,41,44],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":251,\"flag_hit_gain\":-108,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":268,\"hero_dmg\":95,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"pain_memory\",\"breaker_strike\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"pain_memory\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":97,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"pain_memory\",\"feat_name\":\"疼痛记忆\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":11,\"heal_gained\":8,\"hp_after\":35,\"hp_before\":27,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":66,\"route\":\"min\",\"seed\":72036,\"shop_gp_seq\":[113,49],\"stopped_at\":0,\"taken\":95,\"visited_count\":19,\"wallet_count\":32}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":8,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":88,\"flag_hit_gain\":125,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":95,\"hero_dmg\":46,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"flag_burst\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"flag_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":5,\"hp_after\":24,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"flag_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":24,\"hp_before\":24,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":21,\"route\":\"min\",\"seed\":72037,\"shop_gp_seq\":[],\"stopped_at\":2,\"taken\":46,\"visited_count\":10,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":15,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,38,38,38,38,38,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":164,\"flag_hit_gain\":293,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":198,\"hero_dmg\":52,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":0,\"relics\":[\"blood_tax\",\"last_drop\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"refresh\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_tax\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":43,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_tax\",\"feat_name\":\"血税\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":12,\"heal_gained\":7,\"hp_after\":35,\"hp_before\":28,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":35,\"hp_before\":35,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":45,\"route\":\"min\",\"seed\":72038,\"shop_gp_seq\":[93,43],\"stopped_at\":0,\"taken\":52,\"visited_count\":19,\"wallet_count\":29}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"on\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,38,38,38,38,38,41],\"equipped\":[\"morning_order\",\"wide_scout_band\",\"trial_pick\"],\"feat_mode\":\"off\",\"flag_hit\":268,\"flag_hit_gain\":1293,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":289,\"hero_dmg\":106,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"refreshes\":1,\"relics\":[\"breaker_strike\",\"seal_smith\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":41,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":19,\"heal_gained\":7,\"hp_after\":34,\"hp_before\":27,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":34,\"hp_before\":34,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":70,\"route\":\"min\",\"seed\":72039,\"shop_gp_seq\":[72,59],\"stopped_at\":0,\"taken\":106,\"visited_count\":19,\"wallet_count\":28}" + ], + "jsonl": { + "normal": { + "sha256": "88e6f6120b4f2bc8b124f616ccbc08d9c7820c053ec26a106839c7149387fb54", + "rows": 10578 + }, + "perfect": { + "sha256": "43fd21de2450d6298d7119abb681a1abf330ec79e962da42add07f41801b1c21", + "rows": 40783 + } + } + }, + "shop": { + "runsum": [ + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":16,\"flag_hit_gain\":192,\"flag_miss\":3,\"flag_miss_penalty\":8,\"flag_placed\":22,\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":3,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":4,\"route\":\"min\",\"seed\":73000,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":21,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":25,\"flag_hit_gain\":0,\"flag_miss\":6,\"flag_miss_penalty\":6,\"flag_placed\":32,\"hero_dmg\":20,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":9,\"route\":\"min\",\"seed\":73001,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":28,\"visited_count\":5,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":35,\"flag_hit_gain\":-362,\"flag_miss\":2,\"flag_miss_penalty\":8,\"flag_placed\":44,\"hero_dmg\":30,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":2,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":13,\"route\":\"min\",\"seed\":73002,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":30,\"visited_count\":4,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":50,\"flag_hit_gain\":222,\"flag_miss\":5,\"flag_miss_penalty\":16,\"flag_placed\":60,\"hero_dmg\":25,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"max_hp\",\"tier\":\"excellent\",\"value\":10}}],\"rounds\":17,\"route\":\"min\",\"seed\":73003,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":33,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":26,\"flag_hit_gain\":272,\"flag_miss\":8,\"flag_miss_penalty\":6,\"flag_placed\":38,\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":8,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":11,\"route\":\"min\",\"seed\":73004,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":23,\"visited_count\":5,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":8,\"flag_hit_gain\":-91,\"flag_miss\":5,\"flag_miss_penalty\":8,\"flag_placed\":18,\"hero_dmg\":5,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":22,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":4,\"route\":\"min\",\"seed\":73005,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":27,\"visited_count\":2,\"wallet_count\":3}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":48,\"flag_hit_gain\":399,\"flag_miss\":4,\"flag_miss_penalty\":0,\"flag_placed\":57,\"hero_dmg\":18,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":15,\"route\":\"min\",\"seed\":73006,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":34,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":33,\"flag_hit_gain\":52,\"flag_miss\":2,\"flag_miss_penalty\":6,\"flag_placed\":39,\"hero_dmg\":10,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":8,\"route\":\"min\",\"seed\":73007,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":18,\"visited_count\":5,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":6,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":59,\"flag_hit_gain\":155,\"flag_miss\":3,\"flag_miss_penalty\":14,\"flag_placed\":68,\"hero_dmg\":25,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"三干宽门\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"field_dressing\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":3,\"hp_after\":26,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"field_dressing\",\"feat_name\":\"战地包扎\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":23,\"route\":\"min\",\"seed\":73008,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":41,\"visited_count\":8,\"wallet_count\":7}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":27,\"flag_hit_gain\":-267,\"flag_miss\":3,\"flag_miss_penalty\":0,\"flag_placed\":37,\"hero_dmg\":18,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":14,\"mine_hits\":3,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seed\":73009,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":32,\"visited_count\":4,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":3,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":28,\"flag_hit_gain\":-83,\"flag_miss\":5,\"flag_miss_penalty\":4,\"flag_placed\":37,\"hero_dmg\":12,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":9,\"route\":\"min\",\"seed\":73010,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":20,\"visited_count\":4,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":3,\"choices\":2,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":20,\"flag_hit_gain\":9,\"flag_miss\":1,\"flag_miss_penalty\":6,\"flag_placed\":22,\"hero_dmg\":6,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":7,\"route\":\"min\",\"seed\":73011,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":22,\"visited_count\":3,\"wallet_count\":4}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":41,\"flag_hit_gain\":375,\"flag_miss\":5,\"flag_miss_penalty\":6,\"flag_placed\":53,\"hero_dmg\":19,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":3,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":14,\"route\":\"min\",\"seed\":73012,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":27,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":2,\"choices\":1,\"cleared\":false,\"encounter_max_hp_seq\":[26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":5,\"flag_hit_gain\":73,\"flag_miss\":3,\"flag_miss_penalty\":8,\"flag_placed\":10,\"hero_dmg\":2,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[],\"rounds\":2,\"route\":\"min\",\"seed\":73013,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":18,\"visited_count\":2,\"wallet_count\":3}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":5,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":26,\"flag_hit_gain\":184,\"flag_miss\":8,\"flag_miss_penalty\":20,\"flag_placed\":35,\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":0,\"mine_hits\":4,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":10,\"route\":\"min\",\"seed\":73014,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":15,\"visited_count\":6,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":23,\"flag_hit_gain\":15,\"flag_miss\":8,\"flag_miss_penalty\":24,\"flag_placed\":32,\"hero_dmg\":15,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\"],\"levels_cleared\":0,\"mine_dmg\":8,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"rap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"rap\",\"tier\":\"rare\",\"value\":3}}],\"rounds\":11,\"route\":\"min\",\"seed\":73015,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":23,\"visited_count\":5,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":4,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":23,\"flag_hit_gain\":134,\"flag_miss\":5,\"flag_miss_penalty\":8,\"flag_placed\":34,\"hero_dmg\":13,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\",\"地精洞窟·赏战整\"],\"levels_cleared\":0,\"mine_dmg\":16,\"mine_hits\":5,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}}],\"rounds\":8,\"route\":\"min\",\"seed\":73016,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":29,\"visited_count\":5,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":5,\"choices\":6,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":42,\"flag_hit_gain\":129,\"flag_miss\":8,\"flag_miss_penalty\":-2,\"flag_placed\":53,\"hero_dmg\":19,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\"],\"levels_cleared\":0,\"mine_dmg\":36,\"mine_hits\":9,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":17,\"route\":\"min\",\"seed\":73017,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":55,\"visited_count\":7,\"wallet_count\":6}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":4,\"choices\":5,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":30,\"flag_hit_gain\":182,\"flag_miss\":5,\"flag_miss_penalty\":8,\"flag_placed\":37,\"hero_dmg\":16,\"hp_pct\":0,\"keystone_total\":0,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\"],\"levels_cleared\":0,\"mine_dmg\":20,\"mine_hits\":7,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}}],\"rounds\":10,\"route\":\"min\",\"seed\":73018,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":1,\"taken\":36,\"visited_count\":6,\"wallet_count\":5}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":7,\"choices\":7,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":69,\"flag_hit_gain\":275,\"flag_miss\":5,\"flag_miss_penalty\":8,\"flag_placed\":82,\"hero_dmg\":39,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":1,\"mine_dmg\":16,\"mine_hits\":6,\"policy\":\"normal\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"hard_hat\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":49,\"heal_gained\":5,\"hp_after\":17,\"hp_before\":12,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":17,\"hp_before\":17,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":27,\"route\":\"min\",\"seed\":73019,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":55,\"visited_count\":9,\"wallet_count\":8}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,31,31,31,31,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":164,\"flag_hit_gain\":678,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":179,\"hero_dmg\":66,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":19,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":0,\"relics\":[\"flag_burst\",\"stone_skin\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"trap_instinct\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"flag_burst\",\"kind\":\"relic\",\"tier\":\"rare\"}],\"gp\":56,\"heal_gained\":5,\"hp_after\":24,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"flag_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":24,\"hp_before\":24,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":35,\"heal_gained\":6,\"hp_after\":11,\"hp_before\":5,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":11,\"hp_before\":11,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":42,\"route\":\"min\",\"seed\":73020,\"shop_buys\":7,\"shop_gp_seq\":[73,53],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":66,\"visited_count\":19,\"wallet_count\":22}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":8,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":135,\"flag_hit_gain\":474,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":143,\"hero_dmg\":87,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":23,\"feat_ticket\":0,\"heal\":2,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"last_drop\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"heal\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"heal\",\"tier\":\"excellent\",\"value\":6}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":50,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_drop\",\"feat_name\":\"一滴不剩\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":37,\"route\":\"min\",\"seed\":73021,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":87,\"visited_count\":10,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":8,\"choices\":9,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":144,\"flag_hit_gain\":248,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":157,\"hero_dmg\":70,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":2,\"relics\":[\"twin_pick\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":34,\"heal_gained\":0,\"hp_after\":26,\"hp_before\":26,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"twin_pick\",\"feat_name\":\"双管镐\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":38,\"route\":\"min\",\"seed\":73022,\"shop_buys\":0,\"shop_gp_seq\":[],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":70,\"visited_count\":11,\"wallet_count\":9}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":15,\"choices\":17,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,31,31,31,31,46],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":276,\"flag_hit_gain\":1426,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":294,\"hero_dmg\":123,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":1,\"relics\":[\"stone_skin\",\"seal_smith\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":34,\"heal_gained\":5,\"hp_after\":21,\"hp_before\":16,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":21,\"hp_before\":21,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":51,\"heal_gained\":6,\"hp_after\":11,\"hp_before\":5,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":11,\"hp_before\":11,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":82,\"route\":\"min\",\"seed\":73023,\"shop_buys\":8,\"shop_gp_seq\":[94,70],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":123,\"visited_count\":20,\"wallet_count\":24}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":220,\"flag_hit_gain\":378,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":237,\"hero_dmg\":88,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":19,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":3,\"relics\":[\"venom_edge\",\"flag_burst\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"excellent\"},{\"kind\":\"max_hp\",\"tier\":\"excellent\"},{\"kind\":\"perfect_insurance\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"perfect_insurance\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"venom_edge\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":26,\"heal_gained\":3,\"hp_after\":26,\"hp_before\":23,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"venom_edge\",\"feat_name\":\"淬毒刃纹\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"flag_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":7,\"hp_after\":32,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"flag_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":32,\"hp_before\":32,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":60,\"route\":\"min\",\"seed\":73024,\"shop_buys\":6,\"shop_gp_seq\":[45,50],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":88,\"visited_count\":19,\"wallet_count\":20}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,51,51,51,51,56],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":224,\"flag_hit_gain\":643,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":244,\"hero_dmg\":72,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":17},\"refreshes\":1,\"relics\":[\"twin_pick\",\"last_stand\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"attack\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":45,\"heal_gained\":5,\"hp_after\":23,\"hp_before\":18,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"twin_pick\",\"feat_name\":\"双管镐\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"last_stand\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":35,\"heal_gained\":10,\"hp_after\":40,\"hp_before\":30,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"last_stand\",\"feat_name\":\"背水\",\"hp_after\":40,\"hp_before\":40,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":54,\"route\":\"min\",\"seed\":73025,\"shop_buys\":8,\"shop_gp_seq\":[67,61],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":72,\"visited_count\":19,\"wallet_count\":22}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,36,36,36,36,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":255,\"flag_hit_gain\":362,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":267,\"hero_dmg\":142,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":18,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":1,\"relics\":[\"executioner\",\"skip_mining\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"executioner\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":30,\"heal_gained\":5,\"hp_after\":26,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"executioner\",\"feat_name\":\"处刑台\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":31,\"heal_gained\":7,\"hp_after\":29,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":29,\"hp_before\":29,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":87,\"route\":\"min\",\"seed\":73026,\"shop_buys\":8,\"shop_gp_seq\":[74,69],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":142,\"visited_count\":19,\"wallet_count\":23}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,31,31,31,31,31,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":170,\"flag_hit_gain\":123,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":185,\"hero_dmg\":63,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":22,\"attack\":18,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":0,\"relics\":[\"flag_burst\",\"seal_smith\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"flag_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":34,\"heal_gained\":5,\"hp_after\":24,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"flag_burst\",\"feat_name\":\"封印爆裂\",\"hp_after\":24,\"hp_before\":24,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_smith\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":40,\"heal_gained\":6,\"hp_after\":18,\"hp_before\":12,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_smith\",\"feat_name\":\"封印匠\",\"hp_after\":18,\"hp_before\":18,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":41,\"route\":\"min\",\"seed\":73027,\"shop_buys\":7,\"shop_gp_seq\":[53,66],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":63,\"visited_count\":19,\"wallet_count\":22}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":11,\"choices\":12,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,31,31,31],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":202,\"flag_hit_gain\":303,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":220,\"hero_dmg\":104,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":23,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":1,\"relics\":[\"seal_echo\",\"market_close\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"refresh\",\"tier\":\"excellent\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":23,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":43,\"heal_gained\":6,\"hp_after\":14,\"hp_before\":8,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":14,\"hp_before\":14,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":58,\"route\":\"min\",\"seed\":73028,\"shop_buys\":3,\"shop_gp_seq\":[46],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":104,\"visited_count\":15,\"wallet_count\":15}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":11,\"choices\":12,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,41,41,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":197,\"flag_hit_gain\":-852,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":207,\"hero_dmg\":111,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":2,\"relics\":[\"blood_honey\",\"blast_accounting\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"time_sand\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"time_sand\",\"tier\":\"rare\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":45,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blood_honey\",\"feat_name\":\"血蜜\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":31,\"heal_gained\":8,\"hp_after\":27,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"blast_accounting\",\"feat_name\":\"爆破会计\",\"hp_after\":27,\"hp_before\":27,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":63,\"route\":\"min\",\"seed\":73029,\"shop_buys\":4,\"shop_gp_seq\":[68],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":111,\"visited_count\":15,\"wallet_count\":16}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,31,31,31,31,36],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":255,\"flag_hit_gain\":2395,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":271,\"hero_dmg\":90,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\",\"三干中廊\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":17,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":1,\"relics\":[\"warning_beacon\",\"trap_burst\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":58,\"heal_gained\":5,\"hp_after\":20,\"hp_before\":15,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"warning_beacon\",\"feat_name\":\"示警烟火\",\"hp_after\":20,\"hp_before\":20,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"trap_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":56,\"heal_gained\":6,\"hp_after\":25,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_burst\",\"feat_name\":\"陷阱爆发\",\"hp_after\":25,\"hp_before\":25,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":75,\"route\":\"min\",\"seed\":73030,\"shop_buys\":8,\"shop_gp_seq\":[92,80],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":90,\"visited_count\":19,\"wallet_count\":23}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":11,\"choices\":12,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,31,31,31],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":191,\"flag_hit_gain\":862,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":204,\"hero_dmg\":100,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":22,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":2,\"relics\":[\"execution_scholar\",\"market_close\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"perfect_insurance\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"perfect_insurance\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":36,\"heal_gained\":5,\"hp_after\":26,\"hp_before\":21,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"execution_scholar\",\"feat_name\":\"斩首学者\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"geologist\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":28,\"heal_gained\":6,\"hp_after\":12,\"hp_before\":6,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":12,\"hp_before\":12,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":64,\"route\":\"min\",\"seed\":73031,\"shop_buys\":4,\"shop_gp_seq\":[62],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":100,\"visited_count\":15,\"wallet_count\":16}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":17,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,31,31,31,31,31,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":285,\"flag_hit_gain\":2024,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":298,\"hero_dmg\":119,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":18,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":1,\"relics\":[\"seal_echo\",\"seal_stake\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"armor\",\"tier\":\"excellent\",\"value\":2}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_echo\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":36,\"heal_gained\":5,\"hp_after\":24,\"hp_before\":19,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"hp_after\":24,\"hp_before\":24,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":25,\"heal_gained\":6,\"hp_after\":17,\"hp_before\":11,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":17,\"hp_before\":17,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":84,\"route\":\"min\",\"seed\":73032,\"shop_buys\":8,\"shop_gp_seq\":[62,69],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":119,\"visited_count\":20,\"wallet_count\":23}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":9,\"choices\":11,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":133,\"flag_hit_gain\":896,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":146,\"hero_dmg\":69,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":21,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"seal_echo\",\"stone_skin\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"feat_ticket\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"feat_archetype\":\"\",\"feat_id\":\"seal_echo\",\"feat_name\":\"封印回响\",\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":39,\"heal_gained\":5,\"hp_after\":23,\"hp_before\":18,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":23,\"hp_before\":23,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":40,\"route\":\"min\",\"seed\":73033,\"shop_buys\":4,\"shop_gp_seq\":[61],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":69,\"visited_count\":13,\"wallet_count\":14}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,26,26,26,46],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":240,\"flag_hit_gain\":1777,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":255,\"hero_dmg\":84,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\",\"三干宽门\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":19,\"feat_ticket\":0,\"heal\":3,\"max_hp\":20},\"refreshes\":1,\"relics\":[\"seal_stake\",\"twin_pick\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"refresh\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"refresh\",\"tier\":\"excellent\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":50,\"heal_gained\":4,\"hp_after\":26,\"hp_before\":22,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"seal_stake\",\"feat_name\":\"封印桩\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":26,\"heal_gained\":5,\"hp_after\":18,\"hp_before\":13,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"twin_pick\",\"feat_name\":\"双管镐\",\"hp_after\":18,\"hp_before\":18,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}}],\"rounds\":68,\"route\":\"min\",\"seed\":73034,\"shop_buys\":8,\"shop_gp_seq\":[70,72],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":84,\"visited_count\":19,\"wallet_count\":23}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":9,\"choices\":11,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":137,\"flag_hit_gain\":1027,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":149,\"hero_dmg\":80,\"hp_pct\":0,\"keystone_total\":3,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":1,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":2,\"armor\":20,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":1,\"relics\":[\"trap_burst\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"trap_burst\",\"kind\":\"relic\",\"tier\":\"rare\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":48,\"heal_gained\":5,\"hp_after\":22,\"hp_before\":17,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"trap_burst\",\"feat_name\":\"陷阱爆发\",\"hp_after\":22,\"hp_before\":22,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"rare\",\"value\":0}}],\"rounds\":45,\"route\":\"min\",\"seed\":73035,\"shop_buys\":4,\"shop_gp_seq\":[67],\"shop_mode\":\"pool\",\"stopped_at\":2,\"taken\":80,\"visited_count\":13,\"wallet_count\":14}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":15,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,26,26,26],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":211,\"flag_hit_gain\":1222,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":225,\"hero_dmg\":98,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"地精洞窟·主线洞道(现行)\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":2,\"armor\":21,\"attack\":21,\"feat_ticket\":0,\"heal\":3,\"max_hp\":22},\"refreshes\":0,\"relics\":[\"market_close\",\"chain_traps\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"last_drop\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":29,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"market_close\",\"feat_name\":\"收盘\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"common\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":51,\"heal_gained\":5,\"hp_after\":15,\"hp_before\":10,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":15,\"hp_before\":15,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":63,\"route\":\"min\",\"seed\":73036,\"shop_buys\":3,\"shop_gp_seq\":[53],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":98,\"visited_count\":18,\"wallet_count\":17}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":14,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,31,31,31,31,31,41],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":255,\"flag_hit_gain\":1567,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":275,\"hero_dmg\":110,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"地精洞窟·先赏后整\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":2,\"armor\":21,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":19},\"refreshes\":0,\"relics\":[\"chain_traps\",\"hard_hat\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"gp\",\"tier\":\"common\"},{\"kind\":\"armor\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"chain_traps\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_honey\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"twin_pick\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":42,\"heal_gained\":2,\"hp_after\":26,\"hp_before\":24,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"chain_traps\",\"feat_name\":\"连环陷阱\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"hard_hat\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"frontier_miner\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":27,\"heal_gained\":6,\"hp_after\":19,\"hp_before\":13,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"hard_hat\",\"feat_name\":\"安全帽\",\"hp_after\":19,\"hp_before\":19,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":75,\"route\":\"min\",\"seed\":73037,\"shop_buys\":7,\"shop_gp_seq\":[64,54],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":110,\"visited_count\":19,\"wallet_count\":22}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":16,\"cleared\":true,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,41,41,41,41,51],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":228,\"flag_hit_gain\":798,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":242,\"hero_dmg\":97,\"hp_pct\":0,\"keystone_total\":9,\"level_templates\":[\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"地精洞窟·先整后赏\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\",\"宽廊斜岔\"],\"levels_cleared\":3,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":3,\"armor\":20,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":18},\"refreshes\":0,\"relics\":[\"map_exchange\",\"stone_skin\"],\"result\":\"completed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"gp\",\"tier\":\"excellent\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"gp\",\"tier\":\"excellent\",\"value\":50}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"map_exchange\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"blood_insight\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"blast_accounting\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":87,\"heal_gained\":1,\"hp_after\":26,\"hp_before\":25,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"map_exchange\",\"feat_name\":\"弃图兑换\",\"hp_after\":26,\"hp_before\":26,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"field_dressing\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"stone_skin\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"}],\"gp\":31,\"heal_gained\":8,\"hp_after\":15,\"hp_before\":7,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"stone_skin\",\"feat_name\":\"石肤\",\"hp_after\":15,\"hp_before\":15,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":70,\"route\":\"min\",\"seed\":73038,\"shop_buys\":9,\"shop_gp_seq\":[115,73],\"shop_mode\":\"pool\",\"stopped_at\":0,\"taken\":97,\"visited_count\":19,\"wallet_count\":24}", + "RUNSUM {\"atk_pct\":0,\"attune\":\"off\",\"battles\":13,\"choices\":13,\"cleared\":false,\"encounter_max_hp_seq\":[26,26,26,26,26,26,26,26,26,26,31,31,31],\"equipped\":[],\"feat_mode\":\"off\",\"flag_hit\":215,\"flag_hit_gain\":1291,\"flag_miss\":0,\"flag_miss_penalty\":0,\"flag_placed\":228,\"hero_dmg\":91,\"hp_pct\":0,\"keystone_total\":6,\"level_templates\":[\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"地精洞窟·整战赏\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"窄廊短支\",\"宽廊双干(现行)\",\"宽廊双干(现行)\"],\"levels_cleared\":2,\"mine_dmg\":0,\"mine_hits\":0,\"policy\":\"perfect\",\"pool_stock\":{\"ap\":2,\"armor\":22,\"attack\":20,\"feat_ticket\":0,\"heal\":3,\"max_hp\":21},\"refreshes\":2,\"relics\":[\"breaker_strike\",\"skip_mining\"],\"result\":\"failed\",\"reward_events\":[{\"node_curio_pick\":[{\"kind\":\"attack\",\"tier\":\"common\"},{\"kind\":\"max_hp\",\"tier\":\"common\"},{\"kind\":\"ap\",\"tier\":\"rare\"}]},{\"curio_resolved\":{\"context\":\"reward_node\",\"level_index\":1,\"picked\":\"ap\",\"tier\":\"rare\",\"value\":1}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"breaker_strike\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"execution_scholar\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"warning_beacon\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":35,\"heal_gained\":5,\"hp_after\":25,\"hp_before\":20,\"keystone_gained\":3,\"keystone_total\":3,\"level_index\":1}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"breaker_strike\",\"feat_name\":\"破封一击\",\"hp_after\":25,\"hp_before\":25,\"level_index\":1,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}},{\"curio_pick\":{\"aux_cleared\":0,\"candidates\":[{\"feat_id\":\"seal_stake\",\"kind\":\"relic\",\"tier\":\"common\"},{\"feat_id\":\"skip_mining\",\"kind\":\"relic\",\"tier\":\"excellent\"},{\"feat_id\":\"market_close\",\"kind\":\"relic\",\"tier\":\"common\"}],\"gp\":56,\"heal_gained\":6,\"hp_after\":7,\"hp_before\":1,\"keystone_gained\":3,\"keystone_total\":6,\"level_index\":2}},{\"curio_resolved\":{\"feat_archetype\":\"\",\"feat_id\":\"skip_mining\",\"feat_name\":\"跳格采掘\",\"hp_after\":7,\"hp_before\":7,\"level_index\":2,\"picked\":\"relic\",\"tier\":\"excellent\",\"value\":0}}],\"rounds\":66,\"route\":\"min\",\"seed\":73039,\"shop_buys\":4,\"shop_gp_seq\":[90],\"shop_mode\":\"pool\",\"stopped_at\":3,\"taken\":91,\"visited_count\":16,\"wallet_count\":18}" + ], + "jsonl": { + "normal": { + "sha256": "c48a890a39155dc8af29f4a7a98fcec23c7cd3ae077622345e9e171b3b545498", + "rows": 7060 + }, + "perfect": { + "sha256": "118365a0374bdf0d3f462abce46137f98ea76008e28775d59543b4cd5ba734c9", + "rows": 43672 + } + } + } + } +} \ No newline at end of file diff --git a/game/tests/run_all.gd b/game/tests/run_all.gd index 38a09c7e..c773c3f0 100644 --- a/game/tests/run_all.gd +++ b/game/tests/run_all.gd @@ -52,6 +52,8 @@ func _suites() -> Array: {"path": "res://tests/unit/core/tree_requires_level_test.gd", "expect": 0}, # R1 机制位批 2026-10-13——requires_level 等级阈值前置(schema 四拒一过+判定三态+投影两态) {"path": "res://tests/unit/core/pool_case_d_test.gd", "expect": 0}, # 迭代 20 T2——池结构案D 行为(V6 令牌三态/V7 曝光槽 2+1 回退/V8 概率上限解耦提前退役) {"path": "res://tests/unit/core/feat_vouchers_test.gd", "expect": 0}, # 迭代 20 T4——凭证五件+留任二件(V9 路由逐件/F-1 存量账/V10 留任路由+AP 属性轴) + {"path": "res://tests/unit/core/settle_segments_test.gd", "expect": 0}, # 迭代 27 T2——结算五段 Segment(V4:直接 new/编排器接线/真实一轮走段序) + {"path": "res://tests/unit/presentation/modal_flow_test.gd", "expect": 0}, # 迭代 27 T4——模态编排纯逻辑(V6:直接 new/互斥域谓词/三例外锁/演播后排窗) ] diff --git a/game/tests/static_scan.sh b/game/tests/static_scan.sh index f1c7c785..4b649534 100644 --- a/game/tests/static_scan.sh +++ b/game/tests/static_scan.sh @@ -4,7 +4,7 @@ # 正式工程(core/ 存在)时执行;否则优雅跳过(工程在 game/core——2026-09-24 转正结构批后仓库根布局)。 # 纪律:新违规案例必须先补本脚本规则再修码(design/dev/code-conventions.md 缺陷-规范对照表)。 set -u -cd "$(dirname "$0")/.." +cd "$(dirname "$0")/../.." # 迭代 27 T5 修正——2026-09-24 布局迁移后 /.. 落 game/ 致 game/core 守卫恒跳过(静默空转遗产),S 闸规则路径均为仓根相对 if [ ! -d game/core ]; then echo "[static_scan] game/core 不存在(工程未创建)——跳过,退出 0" @@ -13,11 +13,19 @@ fi fail=0 +# S9 — 类型闸(迭代 27 T5——code-conventions §4 机器闸:产品码无类型容器/有值返回缺类型/参数缺类型) +if python tools/audit_type_hints.py; then + echo "[static_scan] S9 ok" +else + echo "[static_scan] S9 FAIL: 类型闸存量未归零(R1/R2/R3 见上清单)" + fail=1 +fi + # S1/S2 — 玩法代码禁 randomize()/自建 RandomNumberGenerator/全局随机函数(绕开 RngService 的口子) # 2026-09-24 首次实跑收敛(转正结构批):违规签名=裸 randomize/rand* 调用(实例方法 .randi() 排除) # + RngService 之外的 RandomNumberGenerator.new();接收服务注入流的类型标注不属违规(core 五文件 15 处实证) hits=$(grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' \ - game/core game/presentation --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | wc -l) + game/core game/presentation --include='*.gd' 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -v 'ADR-0007-exempt' | wc -l) hits2=$(grep -rn 'RandomNumberGenerator\.new()' game/core game/presentation --include='*.gd' 2>/dev/null | grep -vc rng_service) if [ "$hits" -gt 0 ] || [ "$hits2" -gt 0 ]; then echo "[static_scan] S1/S2 FAIL: 全局随机/RNG 自建命中 $hits+$hits2 处"; grep -rnE '\brandomize[[:space:]]*\(|(^|[^.[:alnum:]_])randi[[:space:]]*\(|(^|[^.[:alnum:]_])randf[[:space:]]*\(|(^|[^.[:alnum:]_])randf_range[[:space:]]*\(' game/core game/presentation --include='*.gd' 2>/dev/null; grep -rn 'RandomNumberGenerator\.new()' game/core game/presentation --include='*.gd' 2>/dev/null | grep -v rng_service; fail=1; else echo "[static_scan] S1/S2 ok"; fi diff --git a/game/tests/unit/core/feat_vouchers_test.gd b/game/tests/unit/core/feat_vouchers_test.gd index 05f59316..9c3bf2f8 100644 --- a/game/tests/unit/core/feat_vouchers_test.gd +++ b/game/tests/unit/core/feat_vouchers_test.gd @@ -58,8 +58,8 @@ func _init() -> void: Kit.check_quiet(not a.choose_battery_target("incense_stick"), "电池反例:件不在装配集拒绝(禁自动替选——零副作用)〔专长批 V9·秘宝批换主语〕") a._attunement = ["incense_stick"] # 注入装配集(incense_stick=新目录 active_charged) - a.attune_charges = {"incense_stick": 1} - Kit.check_quiet(a.choose_battery_target("incense_stick") and int(a.attune_charges["incense_stick"]) == 2, + a.run_ledger.attune_charges = {"incense_stick": 1} # 迭代 27 T3——四账住 RunLedger + Kit.check_quiet(a.choose_battery_target("incense_stick") and int(a.run_ledger.attune_charges["incense_stick"]) == 2, "电池正例:玩家点选 incense_stick→充能账 +1(跨层写 attune_charges)〔专长批 V9·秘宝批换主语〕") Kit.check_quiet(a.battery_pending() == 0, "电池:分配后待账清零〔专长批 V9〕") @@ -82,14 +82,14 @@ func _init() -> void: # V10-2 行动点上限归属性轴:ap kind 走加算区第四轴 a._curio_deck = [{"kind": "ap", "tier": "rare"}] a._curio_pending = {"candidates": a._curio_deck.duplicate()} - var ap_bonus_before := int(a.bonus.get("ap", 0)) - Kit.check_quiet(a.pick_curio("ap", "rare") and int(a.bonus.get("ap", 0)) == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")), + var ap_bonus_before := int(a.run_ledger.bonus.get("ap", 0)) + Kit.check_quiet(a.pick_curio("ap", "rare") and int(a.run_ledger.bonus.get("ap", 0)) == ap_bonus_before + int(db.adventure.curio_value("ap", "rare")), "行动点上限归属性轴:ap 兑付写加算区第四轴(bonus.ap 承载)〔专长批 V10〕") # V16——断点扩容往返:新账本随断点快照/续玩回填(breakpoint_resume_test P5 同构) var sm = load("res://core/save_manager.gd").new("user://_feat_vouchers_bp.json") sm.load_from_disk() - a.adventure_counters = {"pain_layers": 3, "twin_digs": 5} + a.run_ledger.adventure_counters = {"pain_layers": 3, "twin_digs": 5} # 迭代 27 T3——四账住 RunLedger a._refresh_stock = 2 a._battery_pending = 1 a.set_save_manager(sm) @@ -100,7 +100,7 @@ func _init() -> void: "V16:新账本三件入断点快照(adventure_counters/refresh_stock/battery_pending)〔专长批 V16〕") var b2 = load("res://core/adventure_runner.gd").new(db, load("res://core/rng_service.gd").new()) Kit.check_quiet(b2.resume_adventure(bp16.duplicate(), {}), "V16 前置:续玩受理") - Kit.check_quiet(int(b2.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2 and b2.battery_pending() == 1, + Kit.check_quiet(int(b2.run_ledger.adventure_counters.get("pain_layers", -1)) == 3 and b2.refresh_stock() == 2 and b2.battery_pending() == 1, "V16:续玩回填逐项一致(计数恢复——跨战账不丢)〔专长批 V16〕") sm.clear_breakpoint() diff --git a/game/tests/unit/core/settle_segments_test.gd b/game/tests/unit/core/settle_segments_test.gd new file mode 100644 index 00000000..52bb3cbc --- /dev/null +++ b/game/tests/unit/core/settle_segments_test.gd @@ -0,0 +1,55 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 结算五段 Segment 单测(迭代 27 T2——需求卡片 V4 验收条目:Segment 类直接 new() + 编排器段序 +# 接线 + 真实一轮结算走段序出裁定/行)。段体逐位一致性由 parity_anchor 三变体对拍承载(V1 核心), +# 本测兜段类可独立实例化与编排器装配面。 +# 运行:godot --headless --path game --script res://tests/unit/core/settle_segments_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") + + +func _init() -> void: + var db = load("res://core/config_db.gd").new() + if not db.load_tables("res://data/tables", false): + print("[settle_segments_test] FAIL — 数值表加载失败") + quit(1) + return + + # V4-1 段类直接 new()(构造收宿主引用——五类同构) + var oc = load("res://core/battle_orchestrator.gd").new(db, load("res://core/rng_service.gd").new()) + Kit.check_quiet(load("res://core/settlement/grant_segment.gd").new(oc) != null, "V4 GrantSegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/aura_segment.gd").new(oc) != null, "V4 AuraSegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/combat_segment.gd").new(oc) != null, "V4 CombatSegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/decay_segment.gd").new(oc) != null, "V4 DecaySegment 直接 new()〔迭代27 T2〕") + Kit.check_quiet(load("res://core/settlement/verdict_segment.gd").new(oc) != null, "V4 VerdictSegment 直接 new()〔迭代27 T2〕") + + # V4-2 编排器 _init 接线(段持宿主引用;段序编排面在位) + Kit.check_quiet(oc._grant_seg != null and oc._grant_seg.oc == oc, "V4 编排器接线·授予段(宿主回指)〔迭代27 T2〕") + Kit.check_quiet(oc._aura_seg != null and oc._aura_seg.oc == oc, "V4 编排器接线·增益/减益段〔迭代27 T2〕") + Kit.check_quiet(oc._combat_seg != null and oc._combat_seg.oc == oc, "V4 编排器接线·战斗段〔迭代27 T2〕") + Kit.check_quiet(oc._decay_seg != null and oc._decay_seg.oc == oc, "V4 编排器接线·衰减段〔迭代27 T2〕") + Kit.check_quiet(oc._verdict_seg != null and oc._verdict_seg.oc == oc, "V4 编排器接线·判定段〔迭代27 T2〕") + Kit.check_quiet(oc._grant_seg._apply_counter_hit({"counter_id": "dig_combo", "value": 1}) == null + and oc._fx.counter("dig_combo") == 1, "V4 counter_add 助手单点(原五位三行组收拢后行为等价)〔迭代27 T2〕") + + # V4-3 真实一轮结算走段序(begin_battle→停手→_maybe_settle→五段→裁定行) + var rng = load("res://core/rng_service.gd").new() + rng.set_master_seed(4242) + var settlement = load("res://core/battle_orchestrator.gd").new(db, rng) + var rows: Array = [] + settlement.step_row.connect(func(row): rows.append(row)) + var result := {"r": ""} + settlement.battle_finished.connect(func(payload): result.r = payload.get("result", "")) + var enc := {"level_index": 1, "node_depth": 1, "hero_hp": int(db.hero.max_hp)} + Kit.check_quiet(settlement.begin_battle(enc), "V4 前置:begin_battle 受理") + # 停手触发轮末结算(stop 经批次提交——_maybe_settle 在 WAITING 相位放行 _settle 段序) + var outcomes: Array = settlement.minesweeper.settle_batch([{"action": "stop"}]) + Kit.check_quiet(not outcomes.is_empty(), "V4 前置:停手受理") + Kit.check_quiet(rows.any(func(row): return String(row.get("type", "")) == "verdict"), + "V4 段序结算出判定行(stop→WAITING→五段→verdict)〔迭代27 T2〕") + Kit.check_quiet(rows.any(func(row): return String(row.get("type", "")) == "decay"), + "V4 衰减段行在流(段序④恒执行)〔迭代27 T2〕") + Kit.check_quiet(String(result.r) == "" or String(result.r) == "victory" or String(result.r) == "defeat", + "V4 裁定面合法(continue 静默/终局经 battle_finished)〔迭代27 T2〕") + + quit(Kit.finish("settle_segments")) diff --git a/game/tests/unit/core/settle_segments_test.gd.uid b/game/tests/unit/core/settle_segments_test.gd.uid new file mode 100644 index 00000000..afc0b06b --- /dev/null +++ b/game/tests/unit/core/settle_segments_test.gd.uid @@ -0,0 +1 @@ +uid://blthtn45ikah6 diff --git a/game/tests/unit/core/treasure_effects_test.gd b/game/tests/unit/core/treasure_effects_test.gd index 4bf8880b..627e1a07 100644 --- a/game/tests/unit/core/treasure_effects_test.gd +++ b/game/tests/unit/core/treasure_effects_test.gd @@ -269,12 +269,13 @@ func _init() -> void: Kit.check_quiet(ob12b.minesweeper.flag_range_free == false, "信标网络反例:未装配旗可达豁免=缺省 false(零装配零扰动)") - # ⑬ 真视使用计数链(受理成功恰计一次——vision_counter 共享 dict) + # ⑬ 真视使用计数链(受理成功恰计一次——RunLedger 单类持账;迭代 27 T3 换代) var rng13 = load("res://core/rng_service.gd").new(); rng13.set_master_seed(321) var ob13 = orch_s.new(db, rng13) - var vc13 := {"used": 0} - Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "vision_counter": vc13}), - "计数链:装配起战(vision_counter 共享 dict 携带)") + var lg13 = load("res://core/run_ledger.gd").new() + lg13.vision_counter = {"used": 0} + Kit.check_quiet(ob13.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.hero.max_hp), "attunement": ["incense_stick"], "ledger": lg13}), + "计数链:装配起战(RunLedger 携带)") var inv13 = load("res://core/treasure_items.gd").new(db) var rec13: Dictionary = inv13.add_item("incense_stick", "attunement", Vector2i(-1, -1), 0) ob13.treasure_inventory = inv13 @@ -282,7 +283,7 @@ func _init() -> void: var tgt13: Vector2i = _first_closed_safe(ob13) if tgt13 != Vector2i(-9, -9): ob13.use_treasure(h13, tgt13) - Kit.check_quiet(int(vc13.get("used", 0)) == 1, + Kit.check_quiet(int(lg13.vision_counter.get("used", 0)) == 1, "计数链正常:受理成功恰 +1(信标网络解锁行为条件数据源)") # ⑭ 装配不可变(迭代 22 秘宝批 T6——V6:冒险全程装配快照三面同源且零写口—— diff --git a/game/tests/unit/core/treasure_effects_test.gd.uid b/game/tests/unit/core/treasure_effects_test.gd.uid new file mode 100644 index 00000000..e0d1e1ed --- /dev/null +++ b/game/tests/unit/core/treasure_effects_test.gd.uid @@ -0,0 +1 @@ +uid://dwsud4v5wh2av diff --git a/game/tests/unit/core/tree_requires_level_test.gd.uid b/game/tests/unit/core/tree_requires_level_test.gd.uid new file mode 100644 index 00000000..597866db --- /dev/null +++ b/game/tests/unit/core/tree_requires_level_test.gd.uid @@ -0,0 +1 @@ +uid://c61pxo55ok5ph diff --git a/game/tests/unit/presentation/modal_flow_test.gd b/game/tests/unit/presentation/modal_flow_test.gd new file mode 100644 index 00000000..0c06dfd3 --- /dev/null +++ b/game/tests/unit/presentation/modal_flow_test.gd @@ -0,0 +1,113 @@ +# VERTICAL SLICE - NOT FOR PRODUCTION +# 模态编排纯逻辑类单测(迭代 27 T4——需求卡片 V6 验收条目:ModalFlow 直接 new() 进 run_all; +# 互斥域谓词/三例外输入锁/演播后排窗挂起/演播完边沿派发/遮挡记账——原组合根注释纪律面的 +# 结构化单测缝首次落地)。载体零场景零树——纯 RefCounted + 哑 Control。 +# 运行:godot --headless --path game --script res://tests/unit/presentation/modal_flow_test.gd(expect 0) +extends SceneTree + +const Kit := preload("res://tests/unit/test_kit.gd") +const ModalFlowScript := preload("res://presentation/modal_flow.gd") # 探针载体禁全局类名裸引用惯例同源 +const ModalStackScript := preload("res://presentation/modal_stack.gd") + + +func _init() -> void: + # 哑层与桩 Callable(纯逻辑类零场景前提的证明面) + var start := Control.new() + var inter := Control.new() + var settlement_holder := {"s": null} # lambda 对局部按值捕获——容器装载 + var rows := {"empty": true} + var calls := {"flush": 0, "open_sync": 0, "close_sync": 0, "replay": 0} + var flow = ModalFlowScript.new() + Kit.check_quiet(flow != null and flow.stack != null, "V6 ModalFlow 直接 new()(stack 内建)〔迭代27 T4〕") + flow.start_visible_get = func() -> bool: return start.visible + flow.inter_visible_get = func() -> bool: return inter.visible + flow.settlement_get = func() -> Variant: return settlement_holder["s"] + flow.rows_empty_get = func() -> bool: return rows["empty"] + flow.flush_rows = func() -> void: calls["flush"] += 1 + flow.on_open_sync = func() -> void: calls["open_sync"] += 1 + flow.on_close_sync = func() -> void: calls["close_sync"] += 1 + flow.replay_click = func() -> void: calls["replay"] += 1 + + # V6-1 互斥域登记+遮挡记账 + var a := Control.new() # 普通层 + var dbg := Control.new() # 调试浮窗(non_locking) + var map := Control.new() # 关卡地图屏(screen_state) + flow.register(a) + flow.register(dbg, [ModalStackScript.TAG_NON_LOCKING]) + flow.register(map, [ModalStackScript.TAG_SCREEN_STATE]) + var tag_dump := "tags a=%s dbg=%s map=%s const=%s" % [str(flow.stack._tags.get(a, "MISSING")), str(flow.stack._tags.get(dbg, "MISSING")), str(flow.stack._tags.get(map, "MISSING")), ModalStackScript.TAG_NON_LOCKING] + print("[DBG] ", tag_dump) + flow.open(a) + Kit.check_quiet(a.visible and calls["open_sync"] == 1 and calls["flush"] == 1, "V6 开侧编排(flush→open→sync 单口序)〔迭代27 T4〕") + # 遮挡记账(登记序=z 序:开低层隐其上方可见层、关闭恢复——低层开启期高层照会不可见的呈现保真) + flow.open(map) + Kit.check_quiet(map.visible, "V6 开最高层(上方无可隐)〔迭代27 T4〕") + flow.open(a) + Kit.check_quiet(a.visible and not map.visible, "V6 遮挡记账(开低层隐其上方可见层)〔迭代27 T4〕") + flow.close(a) + Kit.check_quiet(map.visible and calls["close_sync"] == 1, "V6 关闭恢复(关低层显其上方被隐层+关侧同步)〔迭代27 T4〕") + + # V6-2 输入锁谓词:互斥域随层登记(non_locking 不计)+ 三例外 + start.visible = false + inter.visible = false + settlement_holder["s"] = Object.new() + flow.open(dbg) + Kit.check_quiet(not flow.locks_input(), "V6 输入锁:调试浮窗 non_locking 不计(T2 防线)〔迭代27 T4〕") + flow.close(dbg) + flow.open(a) + Kit.check_quiet(flow.locks_input(), "V6 输入锁:普通层开态计锁〔迭代27 T4〕") + flow.close(a) + flow.close(map) # 登记册全关(close(a) 经遮挡恢复 map——须一并关账才是「全关」态) + settlement_holder["s"] = null + Kit.check_quiet(flow.locks_input(), "V6 输入锁例外三:战斗缺位即锁〔迭代27 T4〕") + settlement_holder["s"] = Object.new() + Kit.check_quiet(not flow.locks_input(), "V6 输入锁:战斗在座+登记册全关=不锁〔迭代27 T4〕") + start.visible = true + Kit.check_quiet(flow.locks_input(), "V6 输入锁例外一:开始层可见即锁〔迭代27 T4〕") + start.visible = false + inter.visible = true + Kit.check_quiet(flow.locks_input(), "V6 输入锁例外二:过场层可见即锁〔迭代27 T4〕") + inter.visible = false + Kit.check_quiet(not flow.locks_input(), "V6 输入锁:全例外解除+登记册全关=不锁〔迭代27 T4〕") + + # V6-3 角标谓词(状态/调试两屏同款守卫——与旧排除式逐位同值) + flow.open(dbg) + Kit.check_quiet(not flow.badge_greyed_battle() and not flow.badge_greyed_map(), + "V6 角标谓词:浮窗开态两形制均不置灰〔迭代27 T4〕") + flow.close(dbg) + flow.open(map) + Kit.check_quiet(not flow.badge_greyed_map() and flow.badge_greyed_battle(), + "V6 角标谓词:地图屏 screen_state 地图形制不计/战斗形制计(旧排除式 [debug] 与 [level,debug] 同值面)〔迭代27 T4〕") + flow.close(map) + flow.open(a) + Kit.check_quiet(flow.badge_greyed_battle() and flow.badge_greyed_map(), + "V6 角标谓词:普通层开态两形制均置灰〔迭代27 T4〕") + flow.close(a) + + # V6-4 演播后排窗挂起+边沿派发(T17 语义) + var end_ov := Control.new() + end_ov.visible = false + rows["empty"] = false + var sync_before1: int = calls["open_sync"] + flow.open_after_playback(end_ov) + Kit.check_quiet(flow._deferred == end_ov and not end_ov.visible and calls["open_sync"] == sync_before1, + "V6 演播后排窗:在播挂起含视觉(不照会上屏——零开侧同步)〔迭代27 T4〕") + rows["empty"] = true + var sync_before2: int = calls["open_sync"] + flow.pump_ended() + Kit.check_quiet(flow._deferred == null and end_ov.visible and calls["open_sync"] == sync_before2 + 1, + "V6 演播完边沿:挂起 modal 先开(队列空判定由 rows_empty_get 承载)〔迭代27 T4〕") + flow.close(end_ov) + flow.pump_ended() + Kit.check_quiet(calls["replay"] == 1, "V6 演播完边沿:无挂起→锁期点击重放派发〔迭代27 T4〕") + # 挂起兜底(他路径开新层先结挂起防永不兑现) + rows["empty"] = false + flow.open_after_playback(end_ov) + rows["empty"] = true + var other := Control.new() + var sync_before: int = calls["open_sync"] + flow.open(other) + Kit.check_quiet(end_ov.visible and flow._deferred == null and calls["open_sync"] == sync_before + 2, + "V6 挂起兜底:开新层先结挂起(T17 防永不兑现——挂起层先开+新层后开两拍)〔迭代27 T4〕") + + quit(Kit.finish("modal_flow")) diff --git a/game/tests/unit/presentation/modal_flow_test.gd.uid b/game/tests/unit/presentation/modal_flow_test.gd.uid new file mode 100644 index 00000000..ec53ac25 --- /dev/null +++ b/game/tests/unit/presentation/modal_flow_test.gd.uid @@ -0,0 +1 @@ +uid://d2pdgrtttf26q diff --git a/game/tests/unit/presentation/pacing_queue_test.gd b/game/tests/unit/presentation/pacing_queue_test.gd index 3287d9a7..ef0f17e8 100644 --- a/game/tests/unit/presentation/pacing_queue_test.gd +++ b/game/tests/unit/presentation/pacing_queue_test.gd @@ -19,19 +19,17 @@ func _init() -> void: var main: Node = load("res://presentation/main.tscn").instantiate() root.add_child(main) await process_frame - var iso = load("res://core/save_manager.gd").new("user://test_pacing_queue_save.json") + # 隔离换注(迭代 27 T1——SaveManager 升格 autoload 后改原地 attach:先删后建防陈旧档泄入, + # 读盘在 attach 内完成;meta 经 save 引用透传零重建) for p in ["user://test_pacing_queue_save.json", "user://test_pacing_queue_save.json.tmp"]: if FileAccess.file_exists(p): DirAccess.remove_absolute(p) - iso = load("res://core/save_manager.gd").new("user://test_pacing_queue_save.json") - iso.load_from_disk() - iso.mark_first_adventure_done() - main.save_manager = iso - main.meta = load("res://core/meta_progression.gd").new(main.db, iso) + var sm = main.get_node("/root/SaveManager") + sm.attach_path("user://test_pacing_queue_save.json") + sm.mark_first_adventure_done() main._refresh_resume_button() # 行信号计数器(真实信号路径旁挂计数——零触碰既有接线) var rows_seen: Array = [] - main.adventure = null main._start_adventure_from_menu() await process_frame # 入口停点选路进首战(真实停点链) @@ -39,11 +37,11 @@ func _init() -> void: _problems.append("入口停点未呈现(前置失守)") _finish() return - main.runner.pick_next_node(int(main.runner.next_candidate_ids()[0])) + main.run_scope.runner.pick_next_node(int(main.run_scope.runner.next_candidate_ids()[0])) await process_frame # 等首战开打(board_changed 同步链——两帧余量) await process_frame - var battle = main.runner.current_battle() + var battle = main.run_scope.runner.current_battle() if battle == null: _problems.append("首战未开打(排播断言无载体)") _finish() diff --git a/roadmap.md b/roadmap.md index 61038f68..e299fe7c 100644 --- a/roadmap.md +++ b/roadmap.md @@ -24,8 +24,8 @@ ## 当前位置(2026-10-14 刷新) - **里程碑语义(2026-10-09 立规换代,详见下方里程碑表的换代注)**:现行里程碑 = **M3「Alpha 版本的完整实现」**——用户未明令宣布完成前停留 M3,迭代只排 M3 需求;M4=Beta 版本的完整实现,一律不排期。下文里程碑表的旧编号(M-3/M-4)保留作历史序列,语义以本节为准。 -- **迭代进度**:迭代 19 五卡全部已入库(2026-10-08);**迭代 20=专长实装批 ✅ 已入库**(2026-10-11,出口闸全绿;残留人闸=实机目检五件+V20/V21/V22 人工核对半,07 在册);**迭代 21=天赋树实装批 ✅ 已入库(2026-10-12 U0 开工单会话交付,T0-T7 全绿;残留人闸=实机目检四柜台)**;**迭代 22=秘宝体系实现重构落地批 ✅ 已入库(2026-10-12 单会话交付,T1-T7 全绿;残留人闸=实机目检秘宝解锁-装配流程)**;**迭代 23=锻造工坊实装批 ✅ 已入库(2026-10-12 单会话交付,T0-T7 全绿;残留人闸=实机目检工作间形态与购买交互)**;**迭代 24=敌人表内容修批(三关)✅ 已入库(2026-10-13 /goal+iter U0 开工单会话交付——N1 (1,2) 降档 hp22/ac2·N2 毒吐盾击两行六池·N3 敌方 attack_type 轴全填 normal+敌方结算克制消费点;冒烟 949/0+run_all 25 套+重闸全绿+MC 同种子配对 40 局×both 位移在带;实现报告=design/qa/iter24-实现报告-2026-10-13.md;残留人闸=实机目检三观测〔07 ㉓〕;代拍板追认单+GDD 反哺点五条待用户;**本迭代位仍预留后续追加卡——用户将把其他需求陆续编入本迭代**);;**迭代 25=UI 全局主题批 ✅ 已入库**(2026-10-13 用户令编入,/goal+iter U0 单会话交付:全局主题资源 main_theme.tres 挂载+两处局部做法退役+冒烟 V1-V6 断言 38 条,冒烟 987/0+重闸全绿;实现报告=design/qa/iter25-实现报告-2026-10-13.md;残留人闸=实机目检全屏巡检〔07 ㉔〕);**迭代 26=UI 交互与观感批已编入待开工**(2026-10-14 用户令;需求卡片 v1.2 零待裁点;开工门=U0——用户新会话 /iter 26 发令;本迭代位预留后续追加卡,同迭代 24 先例);**迭代 27=代码架构技术债修复批 🔄 U0 已开工**(2026-10-14 用户令编入+同日 /goal+iter 27 发令;需求卡片 v1.2 三点全裁零待裁——①第三个 autoload/②单类持四账/③审计五段);**迭代 28+**=M4 数值批(Beta 位·未排期——用户令 2026-10-07:后续新迭代一律不先做 M4;迭代 24/25/26/27 占位后本批顺延)。 -- **近期主战场**:迭代 20/21/22/23/24/25/26 残留人闸(实机目检六批,07 在册);**在飞=迭代 27 代码架构技术债修复批**(需求卡片 v1.2 零待裁,U0=2026-10-14 发令开工;M4 数值批殿后顺延 28+,各批 provisional 终值归其统一标定)。 +- **迭代进度**:迭代 19 五卡全部已入库(2026-10-08);**迭代 20=专长实装批 ✅ 已入库**(2026-10-11,出口闸全绿;残留人闸=实机目检五件+V20/V21/V22 人工核对半,07 在册);**迭代 21=天赋树实装批 ✅ 已入库(2026-10-12 U0 开工单会话交付,T0-T7 全绿;残留人闸=实机目检四柜台)**;**迭代 22=秘宝体系实现重构落地批 ✅ 已入库(2026-10-12 单会话交付,T1-T7 全绿;残留人闸=实机目检秘宝解锁-装配流程)**;**迭代 23=锻造工坊实装批 ✅ 已入库(2026-10-12 单会话交付,T0-T7 全绿;残留人闸=实机目检工作间形态与购买交互)**;**迭代 24=敌人表内容修批(三关)✅ 已入库(2026-10-13 /goal+iter U0 开工单会话交付——N1 (1,2) 降档 hp22/ac2·N2 毒吐盾击两行六池·N3 敌方 attack_type 轴全填 normal+敌方结算克制消费点;冒烟 949/0+run_all 25 套+重闸全绿+MC 同种子配对 40 局×both 位移在带;实现报告=design/qa/iter24-实现报告-2026-10-13.md;残留人闸=实机目检三观测〔07 ㉓〕;代拍板追认单+GDD 反哺点五条待用户;**本迭代位仍预留后续追加卡——用户将把其他需求陆续编入本迭代**);;**迭代 25=UI 全局主题批 ✅ 已入库**(2026-10-13 用户令编入,/goal+iter U0 单会话交付:全局主题资源 main_theme.tres 挂载+两处局部做法退役+冒烟 V1-V6 断言 38 条,冒烟 987/0+重闸全绿;实现报告=design/qa/iter25-实现报告-2026-10-13.md;残留人闸=实机目检全屏巡检〔07 ㉔〕);**迭代 26=UI 交互与观感批已编入待开工**(2026-10-14 用户令;需求卡片 v1.2 零待裁点;开工门=U0——用户新会话 /iter 26 发令;本迭代位预留后续追加卡,同迭代 24 先例);**迭代 27=代码架构技术债修复批 ✅ 已入库**(2026-10-14 /goal+iter U0 开工单会话交付:三屏场景化+会话物化+五段 Segment+RunLedger+ModalFlow+类型闸+卫生件五波全落;逐位一致对拍三变体五跑全同;实现报告=design/qa/iter27-实现报告-2026-10-14.md;残留人闸=实机目检五件〔07 ㉖〕;代拍板追认单十五项+GDD 反哺点两条待用户);**迭代 28+**=M4 数值批(Beta 位·未排期——用户令 2026-10-07:后续新迭代一律不先做 M4;迭代 24/25/26/27 占位后本批顺延)。 +- **近期主战场**:迭代 20/21/22/23/24/25/26 残留人闸(实机目检六批,07 在册);**下一前沿=待用户发令**(迭代 27 已入库收口;M4 数值批殿后顺延 28+,各批 provisional 终值归其统一标定;迭代 20~27 残留人闸七批 07 在册)。 - **实时状态**:以根目录 [active.md](active.md) 顶部「当前状态快照」节为准(`stage.txt` 已定性为旧阶段开关,仅存档)。 ## 主线路线(里程碑 × 迭代序列) @@ -83,7 +83,7 @@ | 迭代 24 | **①敌人表内容修批(三关)(2026-10-13 用户令编入;**本迭代位预留后续追加卡**——用户将把其他需求陆续编入本迭代)**:需求卡片=`design/research/需求卡片-敌人表内容修批-2026-10-13.md`(**v1.1**——grilling 复核九问 Q1-Q9 已全裁、零待裁点、开工门=U0)。三件事合一:**N1** 第一关 (1,2) 降档(`enemy.tres:48` 改两值:`hp` 50→22、`armor_class` 3→2;**敌种 `bugbear` 不动**——`enemy.tres:594-600` 注释载明该行 50 血系 2026-09-24 用户裁决的设计而非填错,且 `hobgoblin` 无立绘,故「改敌种」案已否决;本批定性=阶名化解方案实机未达预期按 2026-10-13 体验反馈重裁)· **N2** 第二/三关普通敌技能投放(`skills.tres` 12→14 行:新增 `venom_spit` 毒吐 damage 1.6/CD2 挂 (2,5)/(2,6)/(2,7) 巨蜘蛛族、`shield_bash` 盾击 damage 1.7/CD3 挂 (3,0)/(3,5)/(3,7) 灰矮人族;**首遇教学位 (2,4) 池不动**,venom_spit 自 (2,5) 起投;两新行均不带 `rider_mult`——低 atk 下恒被 `damage_floor` 兜成 1 与 0.75 档等效)· **N3** 敌方 `attack_type` 加列并全填 `normal`(挂**敌种行**、20 行同值,不做差异化;`enemy_table.gd` 加枚举校验 + `unit_container.gd:119` 加单位快照 + `battle_orchestrator.gd:1406` 第 9 参由 `""` 改传值;全局平移=穿重甲受伤 −1 / 魔法护甲 +1 / 轻甲 0,使锻造甲路线首次具备战斗意义)。**契约要点**:池与敌种键属**契约级**,改必同步 `smoke_all.gd:3211` `expect_pools`(N1 因键值不变零改动、N2 同步 6 键);节点图拓扑零触碰(A1 关卡节点图重构的地盘);(1,1) 地精一伙不在本批范围(「第二个节点」字面偏差已登记 06)。**收口去向**:06「第一关遭遇表数值异常」「第二关技能池与投放」与锻造工坊条迁移面⑥ 三条一并销号;08「二·内容量」敌种池行注摘除 | ✅ 已入库(2026-10-13 单会话交付,出口闸全绿:冒烟 949/0+run_all 25 套〔485 批基线漂移 24→25 在案〕+探针 click/full+render_gate golden 0.00%+doc 三闸/熵增对基线零新增+MC 配对三组读数;实现报告=design/qa/iter24-实现报告-2026-10-13.md;残留人闸=实机目检三观测 07 ㉓) | | 迭代 25 | **①UI 全局主题批(引擎原生控件接管皮肤)(2026-10-13 用户令编入;UI 美化三层诊断 · 第一层第一条)**:**✅ 已全批入库(2026-10-13 /goal+iter U0 开工单会话交付,worktree g-plan2-iter25-theme 分支 iter25-theme-20261013)**——**T0 主题资源**:新建 `game/assets/theme/main_theme.tres` 挂 `project.godot` `gui/theme/custom`(15 类型全覆盖:V/HScrollBar·ScrollContainer·H/VSlider·TooltipPanel/Label·LineEdit·SpinBox·H/VSeparator + 预盖 CheckBox/CheckButton/OptionButton/PopupMenu 弹层;`default_font` 显式指向现役同字体文件=V6 中文字形防御;位图由 `tools/gen_theme_icons.py` 一次性生成落 `game/assets/art/ui/gen/`,色值溯源表落 `game/assets/theme/README.md`)· **T1 换代退役**:`ui_kit.gd` 的 `tooltip_theme()`/`_tip_theme` 与 `styled_slider()` 样式段、`ui_assembler.gd` 挂载行同批删除(禁新旧并存),settings_overlay 几何内联+debug_overlay `_slider_row` 双轨消隐· **T2 断言与文档同步闸**:冒烟新增 V1-V6 主题词条断言 38 条。**顺手销号**=06 第三层「UI 元素细节修」之①(音量滑轨两段对比度重配=已填充段 BG_ROOT #3A2E22 对暖石 #B8A582 实测 5.49:1)。**契约遵守**:🔴 Button 类型词条零接触(冒烟断言在闸)· 🔴 focus 族词条零定义(全主题扫描断言)· 🔴 零新增美术素材(位图=脚本生成纯色/几何)· 🔴 全局纹理过滤未动。**引擎事实订正(呈报追认)**:Godot 4.7.2 源码+默认主题链双查实证——①CheckButton 图标词条名与 CheckBox 同套 `checked`/`unchecked`(卡片 v1.2 注③「CheckButton 用 on/off」不成立,按引擎事实落位)②Slider 无 `grabber_pressed` 图标槽(冻结表「柄·按」值 GOLD_DEEP 留档未落位)③ScrollBar 柄是 StyleBox 非 Icon(无需位图)④4.7 导入参数已无逐贴图 filter 标志(位图原生尺寸消费无滤波伪影)。**闸**:冒烟 987/0(基线 949+本批 38 断言)+run_all 25 套+probe click/full+render_gate golden 0.00%+doc 三闸/熵增对基线零新增;MC 零跑(零数值零行为=纯呈现)。**收口去向**:06「UI 全局主题资源缺失」条移除+「UI 元素细节修」①销号;08 升 v1.3(三·界面主题行→在役、五·表现细节修行注①销号);07 ㉔ 全屏巡检登记;实现报告=design/qa/iter25-实现报告-2026-10-13.md。**待用户**:实机目检全屏巡检+专项观察点二(07 ㉔,登记不阻塞)+代拍板追认单(位图不入 git 政策口径/CheckButton 词条名订正等)+GDD 反哺点(组件规格是否立条款入 art-bible §7 等)。↓ 原任务登记:三件事合一——**T0** 新建 `game/assets/theme/main_theme.tres` 并挂 `project.godot` 的 `gui/theme/custom`,覆盖滚动条 / 滑轨 / 悬停气泡 / 输入件 / 分隔线等引擎原生控件,并预盖 CheckBox / CheckButton / OptionButton 及其弹层 PopupMenu(须在新主题内显式设 `default_font`=现役同一字体文件;新增小位图落 `game/assets/art/ui/gen/`,色值溯源表落 `game/assets/theme/README.md`)· **T1** 退役 `ui_kit.gd` 的 `tooltip_theme()` 与 `styled_slider()` 两处局部做法、消 `debug_overlay._slider_row` 双轨(🔴 旧实现同批删除)· **T2** 断言与文档同步闸。契约要点:🔴 Button 类型词条零接触 · 🔴 focus 族词条零定义 · 🔴 不新增美术素材、不改美术圣经 §7 · 🔴 全局纹理过滤不动 | ✅ 已入库(出口闸全绿+实机目检归用户 07 ㉔ 在册) | T0-T2≈0.5–1d(实际单会话) | | 迭代 26 | **①UI 交互与观感批(2026-10-14 用户令编入;UI 美化三层诊断 · 第一层第二/三条+第三层四件;本迭代位预留后续追加卡,同迭代 24 先例)**:需求卡片=`design/research/需求卡片-UI交互与观感批-2026-10-13.md`(**v1.2**——grilling 两轮八裁全落、零待裁点、卡片成立)。六项:**T1 按钮五态矩阵**(悬停=乙案 modulate 提亮〔与 primary_button 现行 `CANDLE.lightened(0.06)` 同纪律〕/禁用=本族底+α0.45 幽灵化/焦点=INK 2px 内缩 1px/危险钮补本族 pressed;走逐实例 override,迭代 25 两条主题红线断言维持不换代)· **T2 全局纹理过滤**(`project.godot` `default_texture_filter=0`+vignette/gradient_base/beams 显式 LINEAR 白名单+翻转面全枚举)· **T3 <4.5:1 换墨**(站桩顺位/护甲徽标换同族亮档+RAP 行 STONE_DEEP,机检断言锚定)· **T4 光柱连续 alpha 渐变**(0→0.05→0 横向+纵向衰减)· **T5 元素细节余②③**(BCard 图标槽明度差降档/商店卡片区垂直居中/营地四栏等高)· **T5b 诊断转储钮移调试浮窗**(`dump_diagnosis` 功能保留)。🔴 契约 5 条(主题层零新增词条/零新美术素材/棋盘零触碰/对比度 ≥4.5:1/旧实现同批清理);验证闸 V1-V7;代拍板白名单八项;反哺点四条;**渲染闸 golden 面处置**=T6 第 4 步(徽标换墨落 board golden 裁剪区:闸红→核热区限预期面→显式重制基线+呈报→复闸,超预期=L3)。**范围订正**(grilling 实查,iter25 滑轨条同款先例):②垫块仅三选一 BCard(商店卡实为纯文字行卡)/③仅商店+营地(三选一已居中)/危险钮「无按下态」订正为「在但皮肤错配」。估时 T0-T6≈2–2.5d | **✅ 已全批入库(2026-10-14 /goal+iter U0 开工单会话交付,worktree g-plan2-iter26-ui 分支 iter26-ui-20261014 七提交)**——T0 基线+枚举(含卡片 §2.2 枚举漏项勘误)/T1 按钮五态(悬停 modulate 提亮/禁用 α0.45 幽灵化/焦点 INK 2px 内缩 1px/危险钮本族按压态)/T2 全局 Nearest+平滑渐变件 LINEAR 白名单/T3 三处换墨(GOLD_BRIGHT/HERO_BLUE_BRIGHT/STONE_DEEP,6.47/5.88/5.98:1)/T4 光柱连续 alpha/T5 图标槽柔和档+商店居中+营地四栏等高/T5b 诊断钮迁调试浮窗;冒烟 987→1055/0(+68 断言)+run_all 25 套+probe click/full+render_gate golden 0.00%(登记即结未重制)+doc 三闸/熵增对基线零新增;MC 零跑(零数值零行为=纯呈现);实现报告=design/qa/iter26-实现报告-2026-10-14.md;残留人闸=实机目检五项(07 ㉕);待用户=代拍板追认单十三项+GDD 反哺点四条 | T0-T6≈2–2.5d(实际单会话) | -| 迭代 27 | **①代码架构技术债修复批(2026-10-14 用户令编入;来源=[代码架构与实现质量审计报告-2026-10-14](design/research/代码架构与实现质量审计报告-2026-10-14.md),双席审计基线 97de02c,已逐条复核)**:需求卡片=`design/research/需求卡片-代码架构技术债修复批-2026-10-14.md`(**v1.2——三点全裁零待裁〔2026-10-14 用户三裁全按推荐案:①第三个 autoload/②单类持四账/③审计五段〕,卡片成立**)。五波:· **波一 场景拆分+局生命周期物化(≈4-5d)**〔P0-1/P0-2:屏容器 .tscn 切分=开始菜单根场景+营地场景+冒险场景〔内含 level_map/battle 两容器〕,视图代码原样搬、**40+ views 不转 .tscn**;SaveManager/MetaProgression 升格跨场景持有者;「一局」物化为可整体 `queue_free()` 的节点,`main.gd:647-657`/`:555-559` 手工清单消亡;13 个 smoke 探针挂载假设同批迁移〕· **波二 `_settle` 拆段(≈1.5d)**〔P1-1:五段 Segment 类 grant/aura/combat/decay/verdict,24 处 kind 分派随段归位,姊妹病灶 `damage_controller.gd:272` 148 行同批拆〕· **波三 账本收口(≈1.5d)**〔P1-4 共享四账 Ledger 类化+P1-2 刷新双账删 main 副本+P1-3 summary 增 `gp_final` 键、删 `gp_shadow_ledger.gd`〕· **波四 组合根可测化抽取+模态协议内聚(≈1.5-2d)**〔P1-5/P2-7;**须在波一落地后重估抽取面再动**〕· **波五 类型闸收尾+卫生件包(≈3d)**〔P1-6 拉闸+存量 73 处收口,**须在其他重构全部落地后**;P2 七组顺手带〕。🔴 契约 6 条(**零行为变更·重构前后同种子完整局输出逐位一致**/只拆不重写/views 不转 .tscn/探针契约零破坏/信号纪律不变/执行顺序锁+渲染零漂移);**核心验收=逐位一致对拍**(RngService 单盐流在役);验证闸 V1-V9;代拍板白名单九项;反哺点两条(architecture+ADR 系列/code-conventions)。 | **🔄 U0 已开工**(2026-10-14 用户令编入+同日 /goal+iter 27 发令,单会话交付进行中;卡片 v1.2 三点已裁;本迭代位预留后续追加卡,同迭代 24/26 先例) | T0-T7≈12-14d | +| 迭代 27 | **①代码架构技术债修复批(2026-10-14 用户令编入;来源=[代码架构与实现质量审计报告-2026-10-14](design/research/代码架构与实现质量审计报告-2026-10-14.md),双席审计基线 97de02c,已逐条复核)**:需求卡片=`design/research/需求卡片-代码架构技术债修复批-2026-10-14.md`(**v1.2——三点全裁零待裁〔2026-10-14 用户三裁全按推荐案:①第三个 autoload/②单类持四账/③审计五段〕,卡片成立**)。五波:· **波一 场景拆分+局生命周期物化(≈4-5d)**〔P0-1/P0-2:屏容器 .tscn 切分=开始菜单根场景+营地场景+冒险场景〔内含 level_map/battle 两容器〕,视图代码原样搬、**40+ views 不转 .tscn**;SaveManager/MetaProgression 升格跨场景持有者;「一局」物化为可整体 `queue_free()` 的节点,`main.gd:647-657`/`:555-559` 手工清单消亡;13 个 smoke 探针挂载假设同批迁移〕· **波二 `_settle` 拆段(≈1.5d)**〔P1-1:五段 Segment 类 grant/aura/combat/decay/verdict,24 处 kind 分派随段归位,姊妹病灶 `damage_controller.gd:272` 148 行同批拆〕· **波三 账本收口(≈1.5d)**〔P1-4 共享四账 Ledger 类化+P1-2 刷新双账删 main 副本+P1-3 summary 增 `gp_final` 键、删 `gp_shadow_ledger.gd`〕· **波四 组合根可测化抽取+模态协议内聚(≈1.5-2d)**〔P1-5/P2-7;**须在波一落地后重估抽取面再动**〕· **波五 类型闸收尾+卫生件包(≈3d)**〔P1-6 拉闸+存量 73 处收口,**须在其他重构全部落地后**;P2 七组顺手带〕。🔴 契约 6 条(**零行为变更·重构前后同种子完整局输出逐位一致**/只拆不重写/views 不转 .tscn/探针契约零破坏/信号纪律不变/执行顺序锁+渲染零漂移);**核心验收=逐位一致对拍**(RngService 单盐流在役);验证闸 V1-V9;代拍板白名单九项;反哺点两条(architecture+ADR 系列/code-conventions)。 | **✅ 已全批入库(2026-10-14 /goal+iter U0 开工单会话交付,worktree g-plan2-iter27-techdebt 分支 iter27-techdebt-20261014 七提交)**——波一 三屏场景化+run_scope 会话物化+双 autoload(7a0d57d)/波二 _settle 五段 Segment+英雄攻击拆步(26301ba)/波三 RunLedger 单类四账+刷新双账删副本+gp_final 删影子账(6fd7d78)/波四 ModalFlow+互斥域 tags+双 context(8b9e556)/波五a 类型闸 368 参数化+审计器+warning 配置(b605022)/波五b 卫生件 13 死代码+FeatsEngine 退役+Callable 换装+状态机 enum 化+ConfigDB 归根+resolver 解耦(T6 笔)/T7 文档同步(本笔)。**闸**:逐位一致对拍三变体五跑全同(V1 核心验收)+冒烟 1055→1081/0(+V2 屏壳 11+V3 两连局 11+V4 段类 16+V6 模态 19+V5 快照 3+V7 2 断言)+run_all 25→27 套+probe click+render golden 0.00%+static_scan S1/S2/S4/S9 全绿(历史 cd 静默空转顺修);MC 对拍=parity_anchor 三变体×40 局×RUNSUM+JSONL 双面(工具入库 tools/parity_anchor.py);实现报告=design/qa/iter27-实现报告-2026-10-14.md(代拍板追认单十五项+GDD 反哺点两条);残留人闸=实机目检五件(07 ㉖);审计报告本体按退役规则 git rm | T0-T7≈12-14d(实际单会话) | | 迭代 28+ | **M4 数值批(**原 22+;迭代 21 已编入天赋树实装批、迭代 22 已编入秘宝实装批(均 2026-10-11 用户令)、迭代 23 已编入锻造工坊实装批(2026-10-12 用户令)**;迭代 24 已由用户令编入敌人表内容修批、迭代 25 已由用户令编入 UI 全局主题批(2026-10-13)、迭代 26 已由用户令编入 UI 交互与观感批(2026-10-14)、**迭代 27 已由用户令编入代码架构技术债修复批(2026-10-14)**,本批顺延 28+**——**Alpha 收官位·未排期**〔2026-10-09 语义换代:M4=Beta 阶段、非 Alpha 收官位,M3 未由用户明令关闭前不排期维持〕——2026-10-05 用户令编入迭代 17 卡②;2026-10-06 用户令移出独立成批并定序「Alpha 阶段最后——内容补全先行、统一数值调整殿后」〔当日曾被编入迭代 18 为收官卡,2026-10-07 用户令撤回〕;**2026-10-07 用户令二次后移:后续新迭代(迭代 20 起)一律不先做 M4,除非用户明确要求启动 M4 批次**)**:四件数值欠账合一批——①削弱档位终值(weaken 现值 −65/duration 2/refresh 考证在册;叠层改续时语义已定案,差 duration 终值+当量锚复算)②调参批数值点评(06 条目现行登记五条——任务口径「八条」与条目五条不符,需求卡片 §2.2 已呈报差异以条目为准)③迭代 10 奖励重构联动欠账①②(钥石产出重标 boss_keystone 首杀 3/3/3·复杀 2/2/3 provisional + 通关率带重扫〔通关回血 20%+AP 件首轮 +1〕)④元进度定价剩余面(forge base 20/比 150 考证在册——06 条目文本「base 10」滞后于 2026-09-22 对齿批已呈报;Boss 两档终值)。读数义务随批=迭代 14 两件后置复算(T6 MC 四带复算+T7 技能池节奏重测,均挂 07);顺路修复 damage_probe A1 改前既有红灯(判式换代批出口报告 §四.7 在册)。**硬约束:一切 MC/verifier 跑批发起前须用户批准;调参联动禁独立调参网络(β 带 [1.2,2.5] 逐 u 检算)**。需求卡片=`design/research/需求卡片-M4数值批-2026-10-05.md`(**v1.2 2026-10-06 案C 增补+联审修订**:v1.1 三事实与第五推算件照旧;联审=P3 转正顺延至重扫读数后/检算工具 WEAKEN_PCT 入同步面/F11 两句死文本全刷/T3·T5 四带复算并账一次跑批双认账/R5 改前锚=本批新立案C 锚/估时段一勘误 4.25d;案C 增补内容照旧=§2.6 三事实〔行为换代/锚全作废/难度预判〕+T0 推算稿第五件=对拍锚按新序重推导+T6 四带复算按案C 新行为首采;T0-T8≈6.25d,天然切分点 T4 末;🔴 契约 5 条含跑批批准制;**✅ 裁点全裁(2026-10-06 用户令:P1甲=维持 2 场·magnitude [60,85] 带内当量锚复算、具体值随出口 provisional 追认/P2甲 不对称有意/P3甲 3-3-3·2-2-3 维持、转正顺延至重扫读数到账且带内(Q4 二裁)/P4甲 不立直购价);开工=U0**) | 迭代 18 全部内容卡(③④⑤⑦)出口+全部内容批行为变更落定+**用户发令(未排期)**——重锚窗口须收尽全部内容批行为变更 | T0-T8≈6.25d(超软盒预挂,段一 T0-T4≈4.25d〔v1.2 勘误,原 4.5 系加总错误〕/段二 T5-T8≈2.0d) | ## 执行纪律(2026-09-27 用户立规) diff --git a/tools/audit_type_hints.py b/tools/audit_type_hints.py new file mode 100644 index 00000000..fe012c24 --- /dev/null +++ b/tools/audit_type_hints.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""audit_type_hints.py — 迭代27 T5 类型闸机器审计(code-conventions §4 语言纪律的机器闸)。 + +产品码 = game/core + game/data + game/presentation(smoke/tests 豁免——测量面容器直构是惯例)。 +三条规则: + R1 无类型容器声明:显式 Dictionary/Array 非参数化类型注解(审计 §2.2「73 处」同口径—— + `:= {}` 推断形态属类型推断合法面不算债)。 + R2 有值返回缺返回类型:func 无 `->` 且函数体内存在 `return <非空值>`(纯副作用 -> void + 不属本闸危害域,归静态写法面)。 + R3 参数缺类型:func 声明参数无 `: Type`(_init 豁免——构造注入面形态自由)。 +白名单:行内 `# type-hint: ignore` 豁免该行(V7 显式白名单通道)。 +退出码:0=归零,1=有存量(逐条列 file:line)。用法:python tools/audit_type_hints.py +""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SCAN_DIRS = ["game/core", "game/data", "game/presentation"] + +R1A_UNPARAM = re.compile(r"\bvar\s+\w+\s*:\s*(Dictionary|Array)\b(?!\[)") +R1B_LITERAL = re.compile(r"^\s*var\s+\w+\s*(:?=)\s*[\{\[]") +R1B_TYPED = re.compile(r"^\s*var\s+\w+\s*:\s*[\w\[\]]+") +FUNC_DECL = re.compile(r"^(\s*)func\s+(_?\w+)\s*\((.*)\)\s*(->\s*.+)?\s*(#.*)?$") +PARAM_SPLIT = re.compile(r",(?![^(\[]*[)\]])") +RETURN_VALUE = re.compile(r"^\s*return\s+\S") + + +def strip_strings(line): + line = re.sub(r'"[^"]*"', '""', line) + line = re.sub(r"'[^']*'", "''", line) + if line.lstrip().startswith("#"): + return "" + return line.split("#")[0] + + +def audit_file(path): + hits = [] + lines = path.read_text(encoding="utf-8").split("\n") + for idx, raw in enumerate(lines): + if "# type-hint: ignore" in raw: + continue + code = strip_strings(raw) + # R1a 显式非参数化容器类型 + if R1A_UNPARAM.search(code): + hits.append((idx + 1, "R1", raw.strip()[:90])) + continue + fm = FUNC_DECL.match(code) + if not fm: + continue + indent, fname, params, ret = fm.group(1), fm.group(2), fm.group(3), fm.group(4) + # R3 参数缺类型(_init 豁免) + if fname != "_init" and params.strip(): + for prm in PARAM_SPLIT.split(params): + prm = prm.strip() + if prm and ":" not in prm: + hits.append((idx + 1, "R3", "%s(参数 %s)" % (raw.strip()[:80], prm.split("=")[0].strip()))) + # R2 有值返回缺返回类型(函数体 = 缩进更深的后续连续块) + if not ret and fname != "_init": + body = [] + j = idx + 1 + while j < len(lines): + l = lines[j] + if l.strip() == "": + j += 1 + continue + if len(l) - len(l.lstrip("\t")) > len(indent): + body.append(strip_strings(l)) + j += 1 + continue + break + if any(RETURN_VALUE.match(b) for b in body): + hits.append((idx + 1, "R2", raw.strip()[:90])) + return hits + + +def main(): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + total = 0 + for d in SCAN_DIRS: + for p in sorted((ROOT / d).rglob("*.gd")): + for line, rule, text in audit_file(p): + print("[%s] %s:%d: %s" % (rule, p.relative_to(ROOT).as_posix(), line, text)) + total += 1 + if total: + print("[audit_type_hints] FAIL——存量 %d 处(R1 无类型容器/R2 有值返回缺类型/R3 参数缺类型)" % total) + return 1 + print("[audit_type_hints] PASS——产品码类型闸归零(R1/R2/R3)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/parity_anchor.py b/tools/parity_anchor.py new file mode 100644 index 00000000..1b68d8f5 --- /dev/null +++ b/tools/parity_anchor.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""parity_anchor.py — 迭代27重构「逐位一致对拍」锚工具(需求卡片-代码架构技术债修复批 §7 V1 核心验收) + +载体 = adventure_monte_carlo.gd 同种子批量完整局(SceneTree headless,自持 ConfigDB,零渲染)。 +三变体钉死(种子面错开,覆盖 base / 同调账 / 商店-GP 账三类 T2/T3 重构面): + base : -- 20 71000 both min off off off + attune : -- 20 72000 both min on off off + shop : -- 20 73000 both min off on off + +锚双面: + A. RUNSUM 逐行块(stdout 局级摘要面,字符串列表原样入库 anchor.json——红灯时可打印首条差异行); + B. playtest JSONL 全事件流(剔除带墙钟的 session/diagnosis 两类行;文件名本身带时间戳不入比对面)。 +比对口径 = 字节级逐位一致(同引擎二进制+同种子确定性,冻结前已实测两跑全同)。 +原始面不入库(体量 MB 级):红灯取证用 dump 模式对重构前后各落一份原始数据再 diff—— +旧提交随时可重生成同字节原始面(确定性保证),锚只存 sha256 清单。 + +用法: + python tools/parity_anchor.py freeze # 冻结锚(重构前基线,写入 game/tests/evidence/parity/anchor.json) + python tools/parity_anchor.py check # 对拍:重跑同参批,与锚逐位比对(全同退出 0,差异退出 1 并打印首差异) + python tools/parity_anchor.py dump --out DIR # 只跑批落原始面到 DIR(红灯取证) + 可选 --variants base,attune,shop / --quick(只跑 base) +""" +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +GODOT = r"D:\code\godot\Godot_v4.7.2-stable_win64_console.exe" +GAME_DIR = ROOT / "game" +ANCHOR_PATH = ROOT / "game" / "tests" / "evidence" / "parity" / "anchor.json" +USER_LOGS = Path(os.environ.get("APPDATA", "")) / "Godot" / "app_userdata" / "DungeonSweeperVerticalSlice" / "logs" + +# 变体表:名字 → (seed0, attune, shop)。(N=20/策略 both/route min/feat off 全变体钉死。) +VARIANTS = { + "base": (71000, "off", "off"), + "attune": (72000, "on", "off"), + "shop": (73000, "off", "on"), +} +N_RUNS = 20 +BATCH_RE = re.compile(r"^BATCH (\w+) N=(\d+) 终局计数=(.*) 日志=user://logs/(playtest_\d+_\S+\.jsonl)\s*$") +TS_ROW_KINDS = ("session", "diagnosis") # 带墙钟 ts 的两类行——剔出比对面 + + +def run_variant(name: str, seed0: int, attune: str, shop: str, timeout: int = 560): + """跑一个变体批,返回 (runsum_lines, {policy: jsonl绝对路径})。""" + before = set(p.name for p in USER_LOGS.glob("playtest_*.jsonl")) if USER_LOGS.is_dir() else set() + cmd = [GODOT, "--headless", "--path", str(GAME_DIR), "--script", + "res://smoke/adventure_monte_carlo.gd", "--", + str(N_RUNS), str(seed0), "both", "min", attune, shop, "off"] + proc = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", + errors="replace", timeout=timeout) + out = (proc.stdout or "") + (proc.stderr or "") + runsum = [ln for ln in out.splitlines() if ln.startswith("RUNSUM ")] + logs = {} + for ln in out.splitlines(): + m = BATCH_RE.match(ln) + if m: + logs[m.group(1)] = m.group(4) + after = set(p.name for p in USER_LOGS.glob("playtest_*.jsonl")) if USER_LOGS.is_dir() else set() + fresh = after - before + resolved = {} + for policy, fname in logs.items(): + p = USER_LOGS / fname + if p.is_file(): + resolved[policy] = p + else: + cand = [f for f in fresh if f.startswith("playtest_%d_" % seed0)] + if len(cand) == 1: + resolved[policy] = USER_LOGS / cand[0] + else: + print("[parity] FAIL:变体 %s 的 %s 日志文件定位失败(fresh=%s)" % (name, policy, cand)) + sys.exit(2) + expected_policies = 2 + if len(runsum) != N_RUNS * expected_policies or len(resolved) != expected_policies: + print("[parity] FAIL:变体 %s 产出不完整(RUNSUM %d 行 / 日志 %d 份)" % (name, len(runsum), len(resolved))) + print(out[-3000:]) + sys.exit(2) + return runsum, resolved + + +def filter_jsonl(path: Path) -> bytes: + """剔除带 ts 行,保留原始字节行序(bit-exact;不做任何再序列化)。""" + kept = [] + with open(path, "rb") as f: + for raw in f.read().split(b"\n"): + if not raw.strip(): + continue + head = raw[:48] + if any(('{"kind":"%s"' % k).encode() in head for k in TS_ROW_KINDS): + continue + kept.append(raw) + return b"\n".join(kept) + b"\n" + + +def collect(variants, quick=False): + if quick: + variants = [v for v in variants if v == "base"] or ["base"] + data = {} + for v in variants: + seed0, attune, shop = VARIANTS[v] + t0 = time.time() + print("[parity] 跑批 %s(N=%d×2 策略,seed0=%d,attune=%s,shop=%s)…" % (v, N_RUNS, seed0, attune, shop)) + runsum, logs = run_variant(v, seed0, attune, shop) + faces = {"runsum": runsum, "jsonl": {}} + for policy in sorted(logs): + body = filter_jsonl(logs[policy]) + faces["jsonl"][policy] = { + "sha256": hashlib.sha256(body).hexdigest(), + "rows": body.count(b"\n"), + } + data[v] = faces + print("[parity] %s 完成(%.0fs,RUNSUM %d 行,JSONL %s)" % + (v, time.time() - t0, len(runsum), + ", ".join("%s=%s" % (p, faces["jsonl"][p]["sha256"][:12]) for p in sorted(faces["jsonl"])))) + return data + + +def dump_variant_raw(variants, outdir: Path): + outdir.mkdir(parents=True, exist_ok=True) + for v in variants: + seed0, attune, shop = VARIANTS[v] + runsum, logs = run_variant(v, seed0, attune, shop) + (outdir / ("%s_runsum.txt" % v)).write_text("\n".join(runsum) + "\n", encoding="utf-8", newline="\n") + for policy, p in logs.items(): + (outdir / ("%s_%s.jsonl" % (v, policy))).write_bytes(filter_jsonl(p)) + print("[parity] dump %s → %s" % (v, outdir)) + + +def main(): + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + ap = argparse.ArgumentParser(description="迭代27重构逐位一致对拍锚") + ap.add_argument("mode", choices=["freeze", "check", "dump"]) + ap.add_argument("--variants", default="base,attune,shop") + ap.add_argument("--quick", action="store_true", help="只跑 base 变体") + ap.add_argument("--out", default="", help="dump 模式输出目录") + args = ap.parse_args() + variants = [v.strip() for v in args.variants.split(",") if v.strip() in VARIANTS] + if not variants: + print("[parity] FAIL:无有效变体(可选 %s)" % list(VARIANTS)) + return 2 + + if args.mode == "dump": + dump_variant_raw(variants, Path(args.out if args.out else str(ROOT / "tools" / "parity_dump"))) + return 0 + + data = collect(variants, quick=args.quick) + + if args.mode == "freeze": + ANCHOR_PATH.parent.mkdir(parents=True, exist_ok=True) + anchor = {"variants": {v: data[v] for v in variants}} + ANCHOR_PATH.write_text(json.dumps(anchor, ensure_ascii=False, indent=1), encoding="utf-8", newline="\n") + print("[parity] 锚已冻结 → %s(变体 %s)" % (ANCHOR_PATH, variants)) + return 0 + + # check + if not ANCHOR_PATH.is_file(): + print("[parity] FAIL:锚不存在(先 freeze)→ %s" % ANCHOR_PATH) + return 2 + anchor = json.loads(ANCHOR_PATH.read_text(encoding="utf-8")) + bad = 0 + for v in variants: + if v not in anchor["variants"]: + print("[parity] FAIL:锚无变体 %s(锚含 %s)" % (v, list(anchor["variants"]))) + bad += 1 + continue + old = anchor["variants"][v] + new = data[v] + if old["runsum"] != new["runsum"]: + bad += 1 + print("[parity] ✗ %s RUNSUM 漂移(%d 行 vs %d 行)" % (v, len(old["runsum"]), len(new["runsum"]))) + for i, (a, b) in enumerate(zip(old["runsum"], new["runsum"])): + if a != b: + print("[parity] 首差异第 %d 行:" % i) + print("[parity] 锚: %s" % a[:600]) + print("[parity] 今: %s" % b[:600]) + break + for policy in sorted(old["jsonl"]): + o, n = old["jsonl"][policy], new["jsonl"].get(policy) + if n is None: + bad += 1 + print("[parity] ✗ %s/%s 本次缺日志" % (v, policy)) + elif o["sha256"] != n["sha256"]: + bad += 1 + print("[parity] ✗ %s/%s JSONL 逐位漂移(锚 %s… 行数 %d vs 今 %s… 行数 %d)" % + (v, policy, o["sha256"][:12], o["rows"], n["sha256"][:12], n["rows"])) + if bad: + print("[parity] 逐位一致对拍 ✗ 失败 %d 项——该波回炉(V1 红线)" % bad) + return 1 + print("[parity] 逐位一致对拍 ✓ 全同(变体 %s,RUNSUM+JSONL 双面逐位一致)" % variants) + return 0 + + +if __name__ == "__main__": + sys.exit(main())