merge: 迭代27 代码架构技术债修复批全量入库(七提交, worktree g-plan2-iter27-techdebt)
五波: T1 场景拆分+会话物化/T2 _settle 五段/T3 RunLedger 账本收口/T4 ModalFlow+模态互斥域/T5+T6 类型闸+卫生件 核心验收: 同种子完整局输出逐位一致——parity_anchor 三变体五跑全同; 冒烟 1055→1081/0; run_all 25→27 套; static_scan 修复后全绿
This commit is contained in:
commit
dd418e5d7d
118 files changed
+2925
-1728
No files matched your search
@@ -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 ㉖ 五件登记不阻塞)
|
||||
@@ -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
|
||||
@@ -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 反哺点②附带)。
|
||||
@@ -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 配套)。
|
||||
@@ -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` 段边界切法。
|
||||
@@ -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+)
|
||||
|
||||
|
||||
@@ -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 := "" # 调试观测量(同板续轮 / 换新图)
|
||||
|
||||
@@ -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——电池待分配充能随断点
|
||||
|
||||
@@ -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——快照消费方直改不渗入状态容器)
|
||||
|
||||
@@ -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 用户拍板删——与 排雷玩法 展开实现口径归一;变异定稿批按正式语义重接)
|
||||
|
||||
@@ -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():
|
||||
|
||||
+16
-21
@@ -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 计数域保留——非槽位硬消费在册)──
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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":
|
||||
|
||||
+14
-31
@@ -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——发射点逐条执行。
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://6d12lkx5w2x8
|
||||
+41
-51
@@ -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
|
||||
|
||||
@@ -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))):
|
||||
|
||||
@@ -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", "")})
|
||||
|
||||
|
||||
|
||||
+14
-14
@@ -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), "图级偏斜守恒破(偏斜后合计须等于原池)")
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://7afr7e1gtrj5
|
||||
+33
-25
@@ -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"] = {}
|
||||
|
||||
@@ -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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://buwfoafhp2wvk
|
||||
@@ -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 模式地图单补提交时即结)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cmr602bpitrxd
|
||||
@@ -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, {}, "")
|
||||
@@ -0,0 +1 @@
|
||||
uid://c4oht2fyigigk
|
||||
@@ -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", "")))
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0f3h2g13d04g
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://i0dhvekibaam
|
||||
@@ -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:
|
||||
|
||||
+10
-26
@@ -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", "")),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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 := {}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dxj1wy8n1yp6k
|
||||
@@ -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
|
||||
|
||||
|
||||
# 打开营地(主菜单入口——开合与开始层互斥仿设置面先例:开始覆盖层非模态且树序在后,
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://lrba2xy83jwv
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
uid://uk77ei6hjhca
|
||||
@@ -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("回复与护甲资源减少")
|
||||
|
||||
+329
-326
File diff suppressed because it is too large.
Load diff
@@ -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])
|
||||
@@ -0,0 +1 @@
|
||||
uid://nqnk82quv1lb
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 载荷)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d05dkfqnbqdlx
|
||||
@@ -0,0 +1,37 @@
|
||||
class_name AdventureScreen
|
||||
extends Control
|
||||
# 冒险屏壳(迭代 27 T1 场景拆分——审计 P0-1 第一步;内含关卡地图屏与战斗屏两容器)。
|
||||
# 两容器视图原样构建(build_battle_ui 归 ui_assembler 纪律不变、LevelMapScreen 类视图继续
|
||||
# 代码构建、零 .tscn 化改造——契约 2);战斗信号接线归组合根 main.gd(ADR-0001——信号连接
|
||||
# 只在组合根)。树序语义:本屏壳挂 main 首子位(全模态之下)——屏内 level_screen 与
|
||||
# battle_root 无同时可见态(屏级互斥+模态栈遮挡),层内先后序零视觉差;
|
||||
# 模态登记序不变(level_screen 实例仍按节点层与三选一层之间登记)。
|
||||
# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。
|
||||
|
||||
var battle_root: Control # 战斗屏宿主(B/C/D 三区容器——原 main._battle_root 句柄指向成员)
|
||||
var stage: BattleStageView # B 区(含 log_view 明细带)
|
||||
var log_view: BattleLogView
|
||||
var info_panel: HeroInfoPanelView # C 区
|
||||
var board_zone: BoardZoneView # D 区(含 ap_bar/board_view/toast/fx)
|
||||
var board_view: BoardView # 探针兼容别名(= board_zone.board_view)
|
||||
var ap_bar: ApBarView # 探针兼容别名(= board_zone.ap_bar)
|
||||
var settlement_fx: SettlementFxView # 结算演出覆盖层
|
||||
var level_screen: LevelMapScreen # 关卡画面(独立屏)
|
||||
|
||||
const UiAssembler := preload("res://presentation/ui_assembler.gd") # 构建归装配器(规范 1.1——非全局类,preload 引用)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入)
|
||||
battle_root = VBoxContainer.new()
|
||||
add_child(battle_root)
|
||||
var battle_ui: Dictionary[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)
|
||||
@@ -0,0 +1 @@
|
||||
uid://csw5cl1xa7qu7
|
||||
@@ -0,0 +1,12 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://slice0advn0screen"]
|
||||
|
||||
[ext_resource type="Script" path="res://presentation/scenes/adventure_screen.gd" id="1_advn"]
|
||||
|
||||
[node name="AdventureScreen" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_advn")
|
||||
@@ -0,0 +1,14 @@
|
||||
class_name CampScreen
|
||||
extends Control
|
||||
# 营地屏壳(迭代 27 T1 场景拆分——审计 P0-1 第一步)。
|
||||
# 营地覆盖层视图原样构建(CampOverlay 类视图继续代码构建、零 .tscn 化改造——契约 2);
|
||||
# 购买/开合接线归组合根经 CampShopPresenter(ADR-0001——信号连接只在组合根)。
|
||||
# 可独立实例化(F6/headless 挂载)——V2 探针断言载体。
|
||||
|
||||
var camp_overlay: CampOverlay # 营地覆盖层(原 build_overlays 营地段迁入——main 组合根句柄指向本屏成员)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = Control.MOUSE_FILTER_IGNORE # 屏壳=纯容器(ui_input 实证:全屏 STOP 壳会吃掉下层模态鼠标输入)
|
||||
camp_overlay = CampOverlay.new()
|
||||
add_child(camp_overlay)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dw1k6rn5w8gso
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
uid://dqijp10rs1kaj
|
||||
@@ -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")
|
||||
@@ -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", []))
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
# 实际光栅观感归批末目检清单。
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://dd071t8g8cjb3
|
||||
@@ -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", ""))))
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -90,8 +90,11 @@ static func _text_match(text: String, needle: String, mode: String) -> bool:
|
||||
# tag = 隔离档名(不带扩展名,如 "smoke_input_probe_save"——各探针后缀保持不变);先删后建防陈旧档泄入。
|
||||
# mark_first_done = 对齐真实档游玩史口径(模板抽签路径不变);with_settings = 连设置面换注
|
||||
#(隔离 cfg 名 = <tag>_settings.cfg,与被收口探针原文件名逐一同名);refresh_resume = 组合根
|
||||
# 「继续上局」按钮重刷;swap_save = 是否换注 main.save_manager(营地先行段只换 meta 不换 save_manager)。
|
||||
# 「继续上局」按钮重刷;swap_save = 是否换注全局 SaveManager(营地先行段只换 meta 侧不换 SaveManager)。
|
||||
# 返回隔离 SaveManager 实例(调用方常需后续直读余额/重读档)。
|
||||
# 迭代 27 T1——SaveManager/MetaProgression 升格 autoload 后隔离面改单例原地重置
|
||||
#(attach_path 沿 RngService.set_master_seed 先例;MetaProgression 持 save 引用透传,
|
||||
# swap_save=false 分支换 meta 侧引用=独立 iso 实例,语义同旧「只换 meta 不换 save_manager」)。
|
||||
static func isolate_save(main, tag: String, mark_first_done: bool = false, with_settings: bool = false,
|
||||
refresh_resume: bool = false, swap_save: bool = true):
|
||||
var save_path := "user://%s.json" % tag
|
||||
@@ -102,9 +105,15 @@ static func isolate_save(main, tag: String, mark_first_done: bool = false, with_
|
||||
iso.load_from_disk()
|
||||
if mark_first_done:
|
||||
iso.mark_first_adventure_done()
|
||||
# --script 模式无裸 autoload 标识符(run_all.gd 头注)——经 main 节点路径解析
|
||||
var sm = main.get_node("/root/SaveManager")
|
||||
if swap_save:
|
||||
main.save_manager = iso
|
||||
main.meta = MetaProgressionScript.new(main.db, iso)
|
||||
sm.attach_path(save_path)
|
||||
if mark_first_done:
|
||||
sm.mark_first_adventure_done()
|
||||
iso = sm
|
||||
else:
|
||||
main.get_node("/root/MetaProgression").save = iso # 只换 meta 侧(营地段语义)——SaveManager 全局不动
|
||||
if with_settings:
|
||||
main.settings_store = SettingsStoreScript.new("user://%s_settings.cfg" % tag)
|
||||
if refresh_resume:
|
||||
|
||||
+151
-18
@@ -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))
|
||||
|
||||
@@ -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 耗尽/停手→五行段同步跑完→行全在队)
|
||||
|
||||
@@ -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" % [
|
||||
|
||||
@@ -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——契约复原语义显式重算)")
|
||||
|
||||
@@ -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])
|
||||
|
||||
Loaded 100 of 118 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user