Merge branch 'master' into archp2-card
This commit is contained in:
commit
4dd24ac6ed
112 files changed
+2222
-1520
No files matched your search
@@ -44,4 +44,10 @@ jobs:
|
||||
cd "$GITHUB_WORKSPACE/repo"
|
||||
GODOT_BIN=/usr/local/bin/godot bash tools/golden_baseline_refresh.sh "$GITHUB_WORKSPACE/repo/game" --menu
|
||||
ls -la game/tests/golden/mac/ || true
|
||||
echo "WORKSPACE_REMOINDER: 基线文件在 runner 侧 $GITHUB_WORKSPACE/repo/game/tests/golden/mac/,人工取回目检后入库"
|
||||
|
||||
- name: 基线导出(/Users/Shared 不随作业清理,供人工取回目检)
|
||||
run: |
|
||||
mkdir -p /Users/Shared/baseline_out
|
||||
cp -f "$GITHUB_WORKSPACE/repo/game/tests/golden/mac/"*.png /Users/Shared/baseline_out/
|
||||
chmod 644 /Users/Shared/baseline_out/*
|
||||
ls -la /Users/Shared/baseline_out/
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
## 文档状态
|
||||
|
||||
- 版本:1.31
|
||||
- 最近更新:2026-11-02(迭代 49 批后收尾随动——轴戊两件与功能层四行换代已建件现状、ui_registry 行三域句柄分域换代;沿革查 git)
|
||||
- 版本:1.34
|
||||
- 最近更新:2026-11-03(架构审查 P1 余项·工程改动批随动——S8 行登记清单单点 `B1_SET` 与 settlement/ 目录域反向对账、S9/S10 空面自检查偏差清偿;沿革查 git)
|
||||
- 引擎:Godot 4.7.2-stable(GDScript 单语言;2026-09-07 锁定)
|
||||
- 评审模式:solo(`review-mode.txt`,2026-09-12 用户拍板——TD/LP 门禁按 solo 跳过,用户授权架构产出自主推进、迭代期回调)
|
||||
- 覆盖 GDD:现行全集(根索引=`design/gdd/02-系统清单.md`,文档树构成以根索引为准)
|
||||
@@ -101,7 +101,7 @@
|
||||
| 模块 | 独占职责 | 对外暴露 | 消费依赖 | 引擎 API |
|
||||
|---|---|---|---|---|
|
||||
| **board_generator**(A1) | 生成算法(五阶段管线)、BoardGenTable 回填、生成期指标集、合法性谓词(clearable / R₀≠S / min_safe)、可推理率度量(metric_version);生成姿态 = MVP 同步生成 + 加载幕(切片实证 10×10/12×12 同步可接受,WorkerThread 留扩展) | `generate(L, D, rng) -> Board`;度量独立入口×3(推理器 / F3 度量 / 合法性谓词——独立纯函数、同一实现) | ConfigDB(BoardGenTable/VariationTable);board_gen 流(一次 open_stream = 一张图);dig_ap_cost 只读 | 无 |
|
||||
| **minesweeper_rules**(A2) | 棋盘状态(已开集 O / 封印集 G / 小人位)、三事件面、批次仲裁、install 四不变式 | `install_board(board)`;`settle_batch(intents)`(唯一提交入口);查询只读面(current_board / hero_pos / is_revealed / has_seal 等);`compute_move_path(cell)` 只读;`retire_board()`;三源拒绝(board_face / resource_face / phase)+ sub_reason 封闭枚举 | A1 产物(Board class_name 数据契约);A3 try_spend / last_round_end(迭代 41 由属性改方法)/ 相位 | 无(零表读取、零 UI——切片实证) |
|
||||
| **minesweeper_rules**(A2) | 棋盘状态(已开集 O / 封印集 G / 小人位)、三事件面、批次仲裁、install 四不变式 | `install_board(board)`;`settle_batch(intents)`(唯一提交入口);查询只读面(current_board / hero_pos / is_revealed / has_seal 等);`compute_move_path(cell)` 只读;`preview_reveal(cell)` 只读预览口(零写入零事件——0 格连锁传播闭包;迭代 50 己案建口);`retire_board()`;三源拒绝(board_face / resource_face / phase)+ sub_reason 封闭枚举 | A1 产物(Board class_name 数据契约);A3 try_spend / last_round_end(迭代 41 由属性改方法)/ 相位 | 无(零表读取、零 UI——切片实证) |
|
||||
| **unit_container**(B2) | hero/enemy 单位实例(hp/atk/实例池:池快照+冷却+摇定动作+保底计数)、行值快照 | spawn / query_alive / rollable_skills / special_skills / check_trigger_unlocks / set_pending_action / pending_action / basic_streak / commit_action / tick_cooldowns / apply_damage / heal / default_target / check_skill_unlocks / despawn_all(2026-09-16 敌人技能池批:query_beat/query_attack/advance 随行为环退役;rollable_skills/special_skills/check_trigger_unlocks 带 facts 参——2026-10-27 情境条件触发器实装,事实缺键=条件假拦下,百分比事实走阈值式折算) | ConfigDB(EnemyTable/SkillTable/HeroTable) | 无 |
|
||||
| **reward_economy**(B4) | 格面资源身份映射(install 期预分配、Ready 只读、换图映射消亡)、极性路由字典面((类型, 极性)→ 字典行查询;路由执行归 B1 授予段)、F6 当量供给投影;装配期摇号面已拆出掷格件 RewardRoller(2026-11-02 轴戊设计闭合、迭代 49 卡① 建件——roll_board 装配序=守卫→复位→委托→收纳→ready;受理账面留守) | 封闭 API:roll_board / query_identity / query_full_snapshot / grant / grant_clear / retire_mapping / polarity_row / phase(weighted_pick 退役——返行选行算子迁掷格件);另有 is_mapped 静默布尔投影(批量工具用);实测类变量 11 | ConfigDB(RewardTable);BattleFxHost 经注入 Callable `luck_bias_query` 取偏运合计(reward_roll 发射与 bias_rewards 求和移至效果层完成);reward_roll 流(scope = 每次 install 批,取号经掷格件) | 无 |
|
||||
| **helper_items**(A5,切片已实装秘宝域) | HelperItemTable、探测显影语义 | (契约规范源:`../../gdd/systems/跨局循环/秘宝.md`——桥接位:A2 查询接口 + A4 counter_item_ids) | A2 / A4 / B4 | 无 |
|
||||
@@ -128,7 +128,7 @@
|
||||
| **level_runner**(C1-lite,切片已建) | 关卡域生命周期(节点链推进、共享 B4 + HP 携带、关卡末 level_clear) | 节点推进接口;LevelTable(`level.tres`——节点 enum {battle,reward,rest,boss}、末节点必须 Boss、ConfigDB 跨表校验⑧) | B1(直构 BattleOrchestrator 接线战斗)、ConfigDB LevelTable | 无 |
|
||||
| **run_structure**(C2) | 冒险/关卡序列、失败重开、**master_seed 唯一接线方** | (契约规范源:`../../gdd/systems/冒险循环/冒险结构.md`) | RngService.set_master_seed、C3/D2 | 无 |
|
||||
| **difficulty**(C3) | DifficultyTable 回填、全部曲线数值调参所有权、战斗期望表(切片 monte_carlo 为工具原型) | 调参输出(经 D2 R1 解析/编排器传递,禁反向调用生成) | ConfigDB、B1 对账输出(调参约束清单)、A1 报告 C | 无 |
|
||||
| **meta_progression**(C4,Alpha) | MetaTable、钥石账本、天赋树域(MetaTreeTable 12 节点 v0.8——Fork 一节点双态/row 字段退役/RULE_KEYS 六键/彩蛋特化觉醒,迭代 21 T1 换代)+ 同调位槽位判定(树点亮数单源——T4 槽位收编)。**存档内元进度数据读写原语归 MetaStateStore——已落地(迭代 41):本类纯转发读已退役(消费方直连 meta_state),业务规则与写口编排留本类;hero_override 内联锻造加值与树淬炼 flat;并组三组(forge_select(kind,route_id)/hero_type_axes()→{attack,armor}/tree_camp_status()→{rows,fork_groups});补 meta_state 成员装配** | 公开面 20 定稿:buy_treasure_unlock / refresh_treasure_unlocks / treasure_shop_status / forge_buy_level / forge_buy_unlock / forge_select / hero_type_axes / forge_shop_status / settle_adventure / hero_override / tree_can_buy / tree_buy / tree_toggle_fork / tree_buy_fork / tree_rule_keys / tree_camp_status / toggle_attunement / attune_camp_status / attune_refresh_charges / buy_attune_refresh | D3、C2、ConfigDB | 无 |
|
||||
| **meta_progression**(C4,Alpha) | MetaTable、钥石账本、天赋树域(MetaTreeTable 12 节点 v0.8——Fork 一节点双态/row 字段退役/RULE_KEY_PARAMS 六键参数字典(迭代 50 键值字典化)/彩蛋特化觉醒,迭代 21 T1 换代)+ 同调位槽位判定(树点亮数单源——T4 槽位收编)。**存档内元进度数据读写原语归 MetaStateStore——已落地(迭代 41):本类纯转发读已退役(消费方直连 meta_state),业务规则与写口编排留本类;hero_override 内联锻造加值与树淬炼 flat;并组三组(forge_select(kind,route_id)/hero_type_axes()→{attack,armor}/tree_camp_status()→{rows,fork_groups});补 meta_state 成员装配** | 公开面 20 定稿:buy_treasure_unlock / refresh_treasure_unlocks / treasure_shop_status / forge_buy_level / forge_buy_unlock / forge_select / hero_type_axes / forge_shop_status / settle_adventure / hero_override / tree_can_buy / tree_buy / tree_toggle_fork / tree_buy_fork / tree_rules(返参数字典——迭代 50 R1 换代)/ tree_camp_status / toggle_attunement / attune_camp_status / attune_refresh_charges / buy_attune_refresh | D3、C2、ConfigDB | 无 |
|
||||
|
||||
### 战斗域装配现状(迭代 27——2026-10-17 用户裁写入;RunLedger 段已随迭代 39 拆件批退役,四账去向见下「冒险域拆分(现状登记)」节)
|
||||
|
||||
@@ -279,22 +279,24 @@
|
||||
|
||||
背景事实(run_smoke 头注在案):GDScript 运行时脚本错误不改进程退出码、不计入冒烟失败数、且静默中断所在函数剩余断言——2026-09-19 状态记录 83 实证「309 项 0 失败」实为断言未跑全。这是 SCRIPT ERROR 扫描必须存在于判定层的原因。
|
||||
|
||||
### 静态扫描域 S1–S8 承接清单
|
||||
### 静态扫描域 S1–S13 承接清单
|
||||
|
||||
静态扫描域规则(2026-09-12 立,沿革见 git commit log;下表为现行事实登记):
|
||||
|
||||
| # | 规则 | 状态 |
|
||||
|---|---|---|
|
||||
| S1 | 玩法代码禁 `randomize()`、自建 `RandomNumberGenerator`(D2 Rule 5) | **在位**(硬失败;扫描面 game/core + game/presentation) |
|
||||
| S1 | 玩法代码禁 `randomize()`、自建 `RandomNumberGenerator`(D2 Rule 5) | **在位**(硬失败;扫描面 game/core + game/presentation)。非产品码三目录(`game/tests`/`game/probe`/`game/sim`)不进本闸与 S2——它们自建且**显式定种**的随机源是合法测量口径(同种子对拍改前改后读数),禁的是不定种形态;适用域分档与无机器闸的理由见 code-conventions §4 第 13 条(2026-11-03 架构审查 P1 余项裁:不扩扫描面,人工评审承载) |
|
||||
| S2 | 禁全局随机函数 `randi()` / `randf()` / `randf_range()`——全局 RNG 绕开 RngService(specialist 补登) | **在位**(与 S1 同段硬失败;实例方法 `.randi()` 排除、`rng-seed-exempt` 豁免注记在案) |
|
||||
| S3 | core/ 禁 ConfigDB 外 data 表加载(`load(` / `preload(` / `ResourceLoader.load`;schema class_name 直引豁免)(D2 Rule 2) | **在位**(warning 级——class_name 直引误报需人工复核;路径已由裸 `core` 修正为 `game/core`,此前恒零命中静默空转;豁免 config_db.gd 自身=数值单一事实源的正主) |
|
||||
| S3 | core/ 禁 ConfigDB 外 data 表加载(`load(` / `preload(` / `ResourceLoader.load`;schema class_name 直引豁免)(D2 Rule 2) | **在位**(warning 级——class_name 直引误报需人工复核;路径已由裸 `core` 修正为 `game/core`,此前恒零命中静默空转;豁免 config_db.gd 自身=数值单一事实源的正主)。2026-11-03 架构审查 P1 余项裁**维持 warning 级不升硬**:升硬的前提是先有可靠的直引误报排除判据,该判据未立;本行状态即事实源,禁在别处另列 |
|
||||
| S4 | core/ 禁 `extends Control/Node2D`、禁 `.tscn` preload(D2 Rule 7) | **在位**(硬失败) |
|
||||
| S5 | core/ 禁裸引用 autoload 全局标识符(`ConfigDB` / `RngService` 直接写 = --script 模式 parse error)——构造注入 only(specialist 升格) | **在位**(硬失败;形态=标识符后紧跟 `.` 的成员访问,排除注释行与字符串字面量) |
|
||||
| S6 | core↔core 信号连接只许方法 Callable(禁 lambda 捕获 self);发射者禁持有订阅者反向引用 | **在位**(2026-10-25 落闸:内联 lambda 禁令机器闸——同行与跨行两形态、豁免注记 `s6-signal-exempt`、盘面零存量;「发射者禁反向引用」句静态判据不成熟,归人工评审面承接、不造弱判据) |
|
||||
| S7 | B1 编排唯一性:B2/B3/B4 API 符号命中文件 ⊆ B1 文件集;A2/B3/B4 零相互直调(B1 AC-43) | **在位**(2026-10-25 落闸:符号清单×B1 域文件集,族内自调用豁免、豁免注记 `s7-b1-exempt`、缺位自检;全符号形态盘面零存量) |
|
||||
| S8 | 结算路径 `await` 零命中(B1 结算/编排文件集);字面量扫描 = warning 级 + 人工三要素复核(D2 AC-14 口径) | **在位**(2026-10-25 落闸:B1 域文件集 await 零命中,文件集缺位自检、排除注释与字符串;盘面零存量。本行后半句「字面量扫描=warning 级+人工三要素复核」为独立子项、仍归人工面) |
|
||||
| S8 | 结算路径 `await` 零命中(B1 结算/编排文件集);字面量扫描 = warning 级 + 人工三要素复核(D2 AC-14 口径) | **在位**(2026-10-25 落闸:B1 域文件集 await 零命中,排除注释与字符串;盘面零存量。清单单点=`static_scan.sh` 的 `B1_SET`,S7 的豁免名单由它派生(原两份 14 文件抄本已并点,2026-11-03 架构审查 P1 裁)。自检两件:①清单内文件缺位即红;②**目录域反向对账**——`game/core/settlement/` 实有 `.gd` 未登记进 `B1_SET` 即红(新段类忘登记=await 面静默失效,2026-11-03 补)。反向对账只覆盖 settlement/:装板/授予/效果端口/掷格四件结算码住 `game/core` 根下,纯目录域覆盖不住,根下新增仍由本行「模块归属」B1 登记面+写卡段申报承接。本行后半句「字面量扫描=warning 级+人工三要素复核」为独立子项、仍归人工面 |
|
||||
|
||||
域外扩充两件(均硬失败在位):**S9** 类型闸(`tools/audit_type_hints.py`,迭代 27 T5——产品码无类型容器/有值返回缺类型/参数缺类型);**S10** 文件头闸(`tools/audit_file_headers.py`——头注 ≤8 行 + 日期/变更叙事黑名单)。
|
||||
域外扩充五件(S11/S12/S13 自 2026-09-29 起陆续落闸,本表原先只登记到 S10,2026-11-03 补齐登记面):**S9** 类型闸(`tools/audit_type_hints.py`,迭代 27 T5——产品码无类型容器/有值返回缺类型/参数缺类型;扫描面=产品码三目录);**S10** 文件头闸(`tools/audit_file_headers.py`——头注 `##` 载体+日期/变更叙事黑名单;扫描面=产品码三目录);**S11** 代码注释禁用词闸(扫描面=`game/` 全域 `.gd`/`.tres`/`.md`/`.json`/`.sh`,词表单点取 `design/gdd/术语表.md`);**S12** 效果引擎无状态化旧件零残留闸(扫描面=`game/` 全域 `.gd`/`.tres`,旧符号与旧 id 字面清单为脚本字面值);**S13** 第三方质量规则闸(`tools/gdstyle_gate.py`,钉 gdstyle v0.4.0;扫描面=产品码三目录,存量以 `tools/gdstyle_baseline.txt` 欠账簿登记,判定=基线外新键红、同键数值恶化红)。
|
||||
|
||||
S11/S12 扫描面覆盖全域,故 sim/probe/tests 的术语与旧件面**在册**;其余十一闸(S1–S10、S13)扫描面止于产品码,非产品码三目录不受约束——分档理由与各目录的运行期承接通道见 code-conventions §4 第 13 条。**防空转自检现行状态**(本文「三层承载与判定原则」要求扫描面为空即判红):S1–S8 与 S11/S12/S13 各自带空面或缺位自检;**S9/S10 两件已补齐同款自检**(`tools/audit_type_hints.py` 与 `tools/audit_file_headers.py` 缺目录或扫描面零个 `.gd` 一律判红,并绿时打印扫描面件数供核;2026-11-03 架构审查 P1 余项的工程改动批落地,本表此前登记的「静默零命中即 PASS」偏差已清偿)。
|
||||
|
||||
### 三层承载与判定原则
|
||||
|
||||
|
||||
@@ -66,12 +66,13 @@
|
||||
4. **lambda 捕获循环变量按创建时值**——循环内先落地局部变量再捕获,或用 `bind`。
|
||||
5. **重命名脚本必须同步 `.uid` 文件**;新增 `class_name` 后必须跑一次编辑器扫描刷新全局类缓存,否则 `--headless --script` 模式解析失败。
|
||||
6. **`print` 格式占位符与实参类型必须匹配**(`%d` 配 String = 静默输出字面量)。
|
||||
7. **语言纪律已机器化为 S9 类型闸**——`tools/audit_type_hints.py`,执行载体=静态扫描域 S9(`game/tests/static_scan.sh`),退出码 0=归零。扫描面=产品码(`game/core`+`game/data`+`game/presentation`;probe/sim/tests 不在扫描面——测量面容器直构是既有惯例)。三规则:R1 无类型容器(显式非参数化 `Dictionary`/`Array` 注解)/R2 有值返回缺返回类型(纯副作用缺 `-> void` 属写法面不入闸)/R3 参数缺类型(`_init` 豁免——构造注入面形态自由)。
|
||||
7. **语言纪律已机器化为 S9 类型闸**——`tools/audit_type_hints.py`,执行载体=静态扫描域 S9(`game/tests/static_scan.sh`),退出码 0=归零。扫描面=产品码三目录(`game/core`+`game/data`+`game/presentation`);非产品码三目录的适用域分档见本节第 13 条。三规则:R1 无类型容器(显式非参数化 `Dictionary`/`Array` 注解)/R2 有值返回缺返回类型(纯副作用缺 `-> void` 属写法面不入闸)/R3 参数缺类型(`_init` 豁免——构造注入面形态自由)。
|
||||
8. **`:= {}` 推断形态不算债**——`var x := {}` 类赋值推断属类型推断合法面,不入 R1 扫描;只有显式写的非参数化容器注解才算无类型容器。
|
||||
9. **行级白名单通道 `# type-hint: ignore`**——豁免该行不入 S9;豁免理由必须随行注明(现行例:`board_generator` 四函数的 db 参鸭子型——typed 参拒冒烟夹具 `_BoardRowStub` 注入)。
|
||||
10. **`# rng-seed-exempt` 标记豁免通道**——S1/S2 随机函数扫描对该行豁免;限定用途=非玩法 RNG 消费面的引擎熵(现行例:组合根种子编辑框空态取 `randi()`——建种子不进玩法封闭流)。
|
||||
10. **`# rng-seed-exempt` 标记豁免通道**——S1/S2 随机函数扫描对该行豁免;限定用途=非玩法 RNG 消费面的引擎熵(现行例:组合根种子编辑框空态取 `randi()`——建种子不进玩法封闭流);非产品码三目录自建随机源的定种要求见第 13 条。
|
||||
11. **`--script` 模式禁裸 autoload 标识符**——`ConfigDB`/`RngService` 等 autoload 名在 `--headless --script` 模式编译器不解析(parse error);产品码与测试夹具一律经节点路径取用(`get_node("/root/…")`/`AUTOLOAD_PATH_*` 常量);静态扫描域 S5 硬失败承载在位。
|
||||
12. **Variant 参数化现行口径**——账本类字典/数组标注 `Dictionary[Variant, Variant]`/`Array[Variant]` 即合格(满足 R1);元素类型精化列后续事项、不入硬纪律,口径变更须用户裁决后续批次另行写入。
|
||||
13. **静态扫描适用域按来源分档(2026-11-03 架构审查 P1 余项裁)**——S 系静态扫描(S1–S13)的扫描面一律止于产品码三目录,`game/tests`/`game/probe`/`game/sim` 不进任何 S 系扫描面;这三目录的可靠性由运行期通道承接,各通道是:`game/tests`=闸 00/01/02(冒烟·单元与集成·系统测试)、`game/probe`=闸 03/04/05(场景探针与渲染 golden;同目录内 `type_glow_shot_probe` 是人工取证件、`probe_ui_util` 是探针共用面,两者不入闸)、`game/sim`=不入 CI,属用户手动发起的调参与数值测量批量。三目录**允许**自建 `RandomNumberGenerator`,条件=**必须显式定种**(改前改后读数要用同种子对拍,这是全项目测量口径);三目录真正禁止的形态是不定种随机——`randomize()` 与全局 `randi()`/`randf()`/`randf_range()`,一旦命中那次运行结果不可复现,读数与取证一并失效。这条对三目录的部分目前**无机器闸**(S1/S2 扫描面不含它们):扩面需给每处合法定种挂 `rng-seed-exempt` 注记、只拦得住新增,2026-11-03 用户裁不扩,改由本条文字+批末独立验收审计的人工评审承载。术语口径:`tools/run_all_gates.sh` 里的「测量面」指 `game/tests`+`game/sim`(影响面选闸用语,`game/probe` 不在其中——改探针必须跑 03/05),与本条分档无关;本文不再用「测量面」统称这三个目录。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
5. **涉数值改动先采改前读数**——与改后同随机种子配对比较并写明逐项比对口径;无数值改动禁跑模拟
|
||||
|
||||
**实施**
|
||||
6. **新功能必配新测试**——新增玩法/机制必须配套新测试套件进 `tests/run_all`;没有对应测试的变更不许合并;并按 test-conventions §8 挂进改动—验收对照表,没有对照表行的行为改动同样不许合并
|
||||
6. **新功能必配新测试**——新增玩法/机制必须配套新测试套件进 `tests/run_all`;没有对应测试的变更不许合并;并按 test-conventions §8 挂进改动—验收对照表,没有对照表行的行为改动同样不许合并。拆件/重构产出的新 core 件不属新增玩法,由 test-conventions §8.3「新增 core 件的覆盖登记义务」承接(被 tests 面点名,或在能力行申报「经宿主链覆盖」),核对动作归本清单第 10 条的收尾提交
|
||||
7. **验收改动先行声明**——验收变化(基线换代/探针增改/旧断言改判)预先写进需求卡片,禁事后补
|
||||
|
||||
**完成**
|
||||
|
||||
@@ -99,6 +99,8 @@
|
||||
|
||||
**能力清单维护锚(2026-10-29 用户裁改版定稿)**:`game/tests/README.md`「能力清单」是**实现指导+阅读导航+义务声明**——只宣布「要验证什么」,不记「是否已验证/是否跑过」;落地账归现行验收闭合链(改动—验收对照表逐条挂断言+合回前独立验收审计),本清单不承载验证状态。表为四列:能力|系统|真实入口|代表用例(`文件:函数`)。**维护归写卡段,当场入表**:写需求卡片时,本批新增能力当场写入能力行——真实入口取自设计文档已定的入口,代表用例=卡里定死的计划测试(`文件:函数`);受影响存量行(入口/用例名变更)同批更新;能力退役删行。**实现照单落名**:测试函数名以清单为唯一依据,名实不符=卡片没写清=停卡退回。**收尾核对**:批末收尾只核对表上名字与实际落地一致(改名则回表),不刷任何批次字段;实现批禁写该文件的现行红线不变(process-conventions §3 第 11 条),核对义务归收尾提交。三层模型见 §8.9。改动—验收对照表纪律不变:新增或改写的每条断言仍须挂一行(上文「测试侧」);断言改判仍受 §8.4 真实入口口径约束。
|
||||
|
||||
**新增 core 件的覆盖登记义务(2026-11-03 架构审查 P1 余项裁)**:拆件或重构产出的新 core 文件不触发 process-conventions §3 第 6 条「新功能必配新测试」(那条只管新增玩法/机制),因此单由本条承接——新 core 件要么被 `game/tests/` 某套件按文件名或 `class_name` 点名,要么在承接它的能力行「代表用例」里申报「经宿主链覆盖(宿主=<件>)」;两者都无=批末收尾核对时补登。本条**不是覆盖率要求**,与 §7「覆盖率不是目标」同轨:登记的是能力义务的归属,不做文件计数——「每个 core 件必须被点名一次」的机器闸已裁否(它逼出为点名而点名的空套件,且与 §7 正面冲突)。落笔面沿用现行分工:申报行由写卡段入能力清单(实现批禁写该文件),核对归批末收尾提交。
|
||||
|
||||
### 8.4 真实入口断言(验收闭合的主判据)
|
||||
|
||||
- **唯一口径(2026-10-24/2026-10-28 用户两裁)**:每个需求点与每个行为改动的验收=一条从真实入口进入的断言,可 grep 定位、在当轮真实跑批中执行并通过。**禁变异类测试**(把生产码或数据表改成错值看断言抓不抓得到);**禁撤销类取证**(回退本批改动或该 hunk 看断言红不红)——两者都是人为制造红,属伪造证据,一律禁止。
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
>
|
||||
> **兑付频率(先看这条,它是三条渠道的共同前提)**:奇物**不是打一场战斗给一份**。关内的普通战斗节点打完了什么也不给;一关之内奇物**只兑付一次**,触发点是**关底 Boss 被击败**——而按 [冒险结构](冒险结构.md) 的定义,打掉关底 Boss 恰好就等于"通过这一关",这两件事是同一个时刻,不是一条链上的两步。
|
||||
>
|
||||
> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.4。本文件是奇物的唯一定义文档。
|
||||
> 状态:**已实现**(2026-10-04 双层 BD 实施批落地);现行为 v1.5。本文件是奇物的唯一定义文档。
|
||||
>
|
||||
> 数值纪律:本文件只写概念、取值域与指路;每件给多少、效果是什么一律不进正文,事实源是 `data/tables/feats.tres` 与 `data/tables/adventure.tres`。
|
||||
|
||||
@@ -164,7 +164,7 @@
|
||||
> 令牌牌库(奖励节点与商店共用)的组织与三选一抽取规则(2026-10-07 用户裁案D——归档打底做翼 + 曝光槽)。
|
||||
|
||||
- **令牌=出现次数上限**:每个成员在牌库里有 N 张令牌——拿了消耗、**没拿回池**(刷新重抽同样不消耗),所以「最多被拿 N 次」= 池里放 N 张,天然成立,不另设计数器;「只刷出、没拿」不计次数(现行 TP0 语义沿用)。
|
||||
- **概率与上限解耦**(设计约束,新内容必检):每个成员标注**两个独立属性**——概率位(档+权重/令牌数)与已拿上限(cap;拿了才计、刷出不计)。概率不决定上限、上限不影响概率。
|
||||
- **概率与上限解耦**(设计约束,新内容必检):每个成员标注**两个独立属性**——概率位(档+权重/令牌数)与已拿上限(cap;拿了才计、刷出不计。cap 必填——成员行缺 cap 声明=数据错误、拒绝加载,取值域 [1,99];2026-11-02 用户裁「不要任何缺省值」总判据)。概率不决定上限、上限不影响概率。
|
||||
- **已知耦合问题(实装批选型注意)**:现行令牌模型里「N 张令牌」同时承担概率与上限两个职责,无法独立设定(例:档概率需 3 张 vs 单件限拿 2 次——3 张压不住上限、2 张保不住概率)。实现路径候选三案:①抽象权重表+独立已拿计数器(解耦彻底;代价=推翻 TP0 实体牌库模型、「池耗即上限」语义改写)②令牌池+提前退役计数器(保留模型;代价=名义概率与有效概率两层、对账复杂化)③维持耦合(设计自我设限——用户已否)。选型归实装批;影响面=MC 对账模型与断点存档(已拿计数器须进断点附随件)。
|
||||
- **内容归档(打底+做翼)**:普通档=属性件+小额钱(每拿都有底的面粉);优秀/稀有档=功能凭证与改手感件(期待位);传说档=惊喜位。归档按决策影响力、不按数值大小;面值数值归调参批。
|
||||
- **曝光槽**:每次三选一呈现 = **2 格按全池抽 + 1 格按「优秀档以上」子池抽**;高档子池空 → 该格回退按全池抽(池空直通同族兜底)。格数与档配比归调参批。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 增益状态
|
||||
|
||||
> 一句话:增益/减益的状态池、叠层与衰减
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.5
|
||||
> 状态:已定稿(自旧体系迁移);现行为 v1.6
|
||||
|
||||
## 设计目的
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
| 合并匹配认个体 | 同名状态只在同一个体名下按合并策略结算;不同个体同名实例互不相干、互不可见 |
|
||||
| 个体死亡即清 | 个体死亡,其名下实例全部随体移除,不转移、不并回别处 |
|
||||
| 呈现各读各的 | 每只立绘的状态图标行只读它自己名下的实例;结算明细行归因照旧,单体授予写明目标是哪一只 |
|
||||
| 实装随该批 | 本节为定稿口径;实装随敌方状态个体化实装批进游戏——合入前行为仍为旧共享态(对敌效果全体一条实例) |
|
||||
|
||||
举证:本节为结构规则与语义裁定,无计算。
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://sim/adv
|
||||
**试玩画像口径(2026-09-19 用户拍板固化——全项目试玩/扫描/面板模拟一律只用以下两档,随机策略口径退役)**:`policy="normal"` = **普通画像·基础定式**(单数字满足规则——满足即标/满足即开,无处可推盲挖边界格、不刷新;教学后正常玩家的下限近似);`policy="perfect"` = **完美画像**(single+subset 全推理零盲挖/卡死刷新——技能上界仪)。实现单一出口 = `sim/auto_player.gd`(novice_action / perfect_action)经 `sim/level_autoplay.gd`,调试浮窗批量模拟与各扫描工具自动同口径;战斗层冒烟夹具与调试转储的 `choose_dig` 直调是机制测试夹具、不承载画像语义。分级依据与新旧读数(乱点 0%/5% → 基础定式 65%/90%)见 auto_player.gd 普通策略档节与台账 77。
|
||||
|
||||
单关诊断工具(level_debug.gd)随单关直入口口径退役(2026-09-23)——败退归因走 balance_verifier 冒险段(层/节点/敌人人读归因)。
|
||||
战斗级工具(v1 遗产):`dump_battle.gd`(结算流水)/ `monte_carlo.gd`(战斗期望)/ `experiment_density.gd`(密度调参)。
|
||||
战斗级工具(v1 遗产):`dump_battle.gd`(结算流水)/ `monte_carlo.gd`(战斗期望)。
|
||||
|
||||
## 范围(v1 范围 + v2 追加,用户 2026-09-10/11 确认)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ var _refresh_bank: RefreshBank = null # 刷新账件(_init 建——三源
|
||||
var _loadout: AttuneLoadout = null # 同调配装件(_init 建;充能单源化——物化行唯一事实源)
|
||||
var _bp_store: BreakpointStore # 断点组装与回放件(_init 建;六成员+27 键登记单源+读写侧机组随件——宿主读写经件)
|
||||
var adventure_inventory: TreasureItems # 冒险级秘宝库存(单源化——同调未用行跨关存活的承载面;关卡域语义归 level 使用面)
|
||||
var _tree_rules: Array[Variant] = [] # 树 rule 键集(Fork 激活侧 effects 透传;下传 enc.tree_rules 消费面)
|
||||
var _tree_rules: Dictionary[Variant, Variant] = {} # 树规则参数字典(Fork 激活侧 effects_* 透传;下传 enc.tree_rules 消费面)
|
||||
var _shop_mode := "legacy" # 商店模式("legacy"=固定三件加定价〔off 对照承载,缺省零扰动〕/"pool"=共享池抽 3 买 1 重抽;真实入口 main.gd 显式投 pool)
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ func start_adventure(seed_val: int, params: Dictionary = {}) -> bool:
|
||||
# 特性开局注入(MC 成型画像载体:params.relics_seed 非空=开局持有集〔七流派
|
||||
# 各 2 件档一成型——测量面专用;真实局规则改写型特性只走局内随机渠道,红线②层级纪律不破〕)
|
||||
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 [] # A/B 面 rule 键集
|
||||
_tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典
|
||||
if not feat_seed.is_empty():
|
||||
for r in feat_seed:
|
||||
_stats.feats.append(String(r))
|
||||
@@ -163,7 +163,7 @@ func resume_adventure(bp: Dictionary, params: Dictionary = {}) -> bool:
|
||||
_attunement = params.get("attunement", []) if params.get("attunement", []) is Array else [] # additive
|
||||
_refresh_bank.set_refresh_x_cap(_attune_refresh_x_total()) # 刷新账 X 基数预算注入(续玩参数面同 start)
|
||||
_bp_store.restore_stop_bank_fragments(bp, params) # 停点件/刷新账片段回填(件侧——凭证计数;刷新存量/Y 池缺键回退参数面)
|
||||
_tree_rules = params.get("tree_rules", []) if params.get("tree_rules", []) is Array else [] # A/B 面 rule 键集(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
_tree_rules = params.get("tree_rules", {}) if params.get("tree_rules", {}) is Dictionary else {} # A/B 面规则键参数字典(续玩参数面同 start_adventure 同式,消费面 set_tree_rules 下传)
|
||||
# 续玩计数器回填(adventure 域计数——缺键 = 旧版断点防御空集;_reset_curio_state 已清零在先——桶归宿主字典)
|
||||
var bp_counters = _bp_store._bp_read("adventure_counters", bp)
|
||||
if bp_counters is Dictionary:
|
||||
|
||||
@@ -179,7 +179,7 @@ func flush_pending_reveals(ri: int) -> void:
|
||||
|
||||
# 效果引擎订阅重建(每战从持有集/装备集重建,中途获得下一战生效;树规则合成订阅同面装载。
|
||||
# 装载序=feats 表序→treasure 表序→树规则,写死保种子复放。计数器四桶经 counters_ctx 引用宿主容器,无回灌时点)
|
||||
func rebuild_subscriptions(feats: Array, attunement: Array, tree_rules: Array) -> void:
|
||||
func rebuild_subscriptions(feats: Array, attunement: Array, tree_rules: Dictionary[Variant, Variant]) -> void:
|
||||
engine.rebuild(feats, attunement, tree_rules)
|
||||
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ func _fx_host_fact(fact: String) -> Variant:
|
||||
match fact:
|
||||
"nest_count":
|
||||
var n := 0
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
for eff in _status_effects.query_active_side("enemy"):
|
||||
if String(eff.effect_id) == "weaken":
|
||||
n += 1
|
||||
return n
|
||||
@@ -348,7 +348,7 @@ func _begin_battle_fx_state(enc: Dictionary) -> void:
|
||||
# 树 rule 键消费(Fork 激活侧效果=效果引擎合成
|
||||
# 订阅(rebuild 注入——round_start 首轮 AP+1 / damage_taken 减伤+1)。其余 rule 键
|
||||
# =stat 面投影不入战斗(T8 表内容 provisional 口径披露))
|
||||
var tree_rules: Array[Variant] = enc.get("tree_rules", []) as Array
|
||||
var tree_rules: Dictionary[Variant, Variant] = enc.get("tree_rules", {}) as Dictionary
|
||||
# 效果引擎订阅重建(装载序=feats 表序→treasure 表序→树规则,写死保种子复放——细则见 battle_fx_host.rebuild_subscriptions)
|
||||
_fx_host.rebuild_subscriptions(feat_ids, _facts.attunement, tree_rules)
|
||||
|
||||
@@ -510,19 +510,15 @@ func _defender_ac(unit: Dictionary) -> int:
|
||||
return _damage.defender_ac(unit)
|
||||
|
||||
|
||||
func _pool_effective(effect_id: String) -> int:
|
||||
var side := "hero"
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id)
|
||||
if not row.is_empty():
|
||||
side = String(row.target_side)
|
||||
for eff in _status_effects.query_active(side):
|
||||
func _pool_effective(effect_id: String, owner_key: String) -> int:
|
||||
for eff in _status_effects.query_active(owner_key):
|
||||
if eff.effect_id == effect_id:
|
||||
return eff.effective_value
|
||||
return 0
|
||||
|
||||
|
||||
func _emit_composition_effects(side: String, ri: int) -> void:
|
||||
for eff in _status_effects.query_active(side):
|
||||
for eff in _status_effects.query_active_side(side):
|
||||
if eff.round_index < ri:
|
||||
# 续存实例(前轮授予)——每结算兑付一次的可见面(合并实例只持首授归因——增益状态 义务⑦)
|
||||
_emit_row({"type": "effect", "source_class": eff.source_class, "origin_cell": eff.origin_cell}, ri,
|
||||
@@ -536,10 +532,10 @@ func _emit_settlement_snapshot(verdict: String) -> void:
|
||||
for u in _enemy_system.query_alive("enemy"):
|
||||
enemies.append({"id": String(u.enemy_id), "hp": int(u.hp), "max_hp": int(u.max_hp), "atk": int(u.atk), "pending_skill": _enemy_system.pending_action(u), "armor": String(u.get("armor_type", "light")), "armor_class": int(u.get("armor_class", 0))})
|
||||
var buffs: Array[Dictionary] = []
|
||||
for eff in _status_effects.query_active("hero"):
|
||||
buffs.append({"id": eff.effect_id, "side": "hero", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
buffs.append({"id": eff.effect_id, "side": "enemy", "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
for eff in _status_effects.query_active_side("hero"):
|
||||
buffs.append({"id": eff.effect_id, "side": "hero", "owner": String(eff.owner_key), "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
for eff in _status_effects.query_active_side("enemy"):
|
||||
buffs.append({"id": eff.effect_id, "side": "enemy", "owner": String(eff.owner_key), "layers": eff.layers, "left": eff.remaining_settlements})
|
||||
var mines_hit := 0
|
||||
var safe_n := 0
|
||||
for evt in _facts.reveals:
|
||||
@@ -651,8 +647,9 @@ func _roll_gp_drop() -> int:
|
||||
# 财富之道·聚(战斗掉落 GP 乘算·入账前:只乘战斗掉落两成分〔掷定+关卡加成〕,
|
||||
# 店内 escrow 不乘〔范围=仅战斗掉落渠道〕;本函数返回值=结算框「GP 增量」行
|
||||
# 显示源,落在入账前=显示即实得;乘数 provisional 150%)
|
||||
if (_facts.encounter.get("tree_rules", []) as Array).has("economy_hoard"):
|
||||
drop = int(round(float(drop) * 1.5))
|
||||
var hoard: Dictionary[Variant, Variant] = (_facts.encounter.get("tree_rules", {}) as Dictionary).get("economy_hoard", {})
|
||||
if not hoard.is_empty():
|
||||
drop = int(round(float(drop) * float(hoard["mult_pct"]) / 100.0)) # 乘数读规则键参数 mult_pct
|
||||
return drop + _reward_grant.escrow
|
||||
|
||||
|
||||
@@ -666,12 +663,6 @@ func _roll_gp_drop() -> int:
|
||||
func presentation_snapshot() -> Dictionary:
|
||||
var enemies: Array[Dictionary] = []
|
||||
var all: Array[Variant] = _enemy_system.all_enemies()
|
||||
# 敌方增益投影(side-wide 状态——蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行)
|
||||
var enemy_effects: Array[Dictionary] = []
|
||||
for eff in _status_effects.query_active("enemy"):
|
||||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(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[Variant, Variant] = all[i]
|
||||
var alive: bool = bool(u.get("alive", false))
|
||||
@@ -689,7 +680,7 @@ func presentation_snapshot() -> Dictionary:
|
||||
"pending_skill": pending,
|
||||
"pending_type": String(pending_row.get("type", "")) if not pending_row.is_empty() else "",
|
||||
"pending_name": String(pending_row.get("display_name", "")) if not pending_row.is_empty() else "",
|
||||
"effects": enemy_effects,
|
||||
"effects": _enemy_own_effects(u),
|
||||
"alive": alive,
|
||||
})
|
||||
var hero := {}
|
||||
@@ -705,7 +696,7 @@ func presentation_snapshot() -> Dictionary:
|
||||
"refresh_used": _facts.refresh_used, # 受理计数入呈现成文面(HUD 余量单源读此处)
|
||||
"hero": hero,
|
||||
"enemies": enemies,
|
||||
"hero_effects": _status_effects.query_active("hero").duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器)
|
||||
"hero_effects": _status_effects.query_active(StatusContainer.OWNER_HERO).duplicate(true), # 值拷贝出户(快照消费方直改不渗入状态容器)
|
||||
# 整图变异投影(呈现横幅/常驻角标唯一读取面;
|
||||
# 空数组 = 无全局型合法态:普通图与全局池空回落图恒空)
|
||||
"board_variants": (minesweeper.current_board().board_variant_ids().duplicate()
|
||||
@@ -713,6 +704,18 @@ func presentation_snapshot() -> Dictionary:
|
||||
}
|
||||
|
||||
|
||||
# 敌方增益逐卡投影(呈现各读各的——增益状态 v1.5):每卡只读它自己名下实例
|
||||
# (蓄力/硬化/削弱/暴走 percent 行 + 警觉 ac 行;死亡个体桶已清,投影恒空)
|
||||
func _enemy_own_effects(u: Dictionary) -> Array[Dictionary]:
|
||||
var own: Array[Dictionary] = []
|
||||
if bool(u.get("alive", false)):
|
||||
for eff in _status_effects.query_active(StatusContainer.owner_key_for(u)):
|
||||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
|
||||
if not erow.is_empty() and String(erow.unit) in ["percent", "ac"]:
|
||||
own.append({"effect_id": eff.effect_id, "display_name": String(eff.display_name), "layers": int(eff.layers)})
|
||||
return own
|
||||
|
||||
|
||||
# 战斗进行中谓词(hero 在场且存活)——组合根高频相位判断的低成本面
|
||||
func is_battle_live() -> bool:
|
||||
return battle_result == "" and not _enemy_system.hero_unit().is_empty()
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
class_name BoardData
|
||||
extends RefCounted
|
||||
|
||||
# 出生 3×3 区格数(W×H−9 布雷域基的双落点收口单点:
|
||||
# 棋盘生成 _attempt/_finalize 与 BoardGenTable 布雷数下限校验共用;与 spawn_cells() 的 3×3 几何同源)
|
||||
const SPAWN_AREA_CELLS := 9
|
||||
var spawn_edge: int = 0 # 出生区边长(奇数——表头 board_gen.spawn_edge 经生成器注入;spawn_cells() 几何与 W×H−edge² 布雷域基同源导出)
|
||||
|
||||
var width: int = 0
|
||||
var height: int = 0
|
||||
@@ -69,9 +67,10 @@ func neighbors8(c: Vector2i) -> Array:
|
||||
|
||||
|
||||
func spawn_cells() -> Array:
|
||||
var half := spawn_edge / 2
|
||||
var out: Array[Vector2i] = []
|
||||
for dy in range(-1, 2):
|
||||
for dx in range(-1, 2):
|
||||
for dy in range(-half, half + 1):
|
||||
for dx in range(-half, half + 1):
|
||||
var c := Vector2i(spawn_center.x + dx, spawn_center.y + dy)
|
||||
if in_bounds(c):
|
||||
out.append(c)
|
||||
|
||||
@@ -14,26 +14,26 @@ var fatal_handler: Callable = Callable()
|
||||
|
||||
# ==================== 生成入口 ====================
|
||||
|
||||
# 目标雷数 N = round(d×(W×H−9))(params 快照键:density/w/h)——布雷定数与产物断言两处的单点公式
|
||||
# 目标雷数 N = round(d×(W×H−edge²))(params 快照键:density/w/h/spawn_edge)——布雷定数与产物断言两处的单点公式
|
||||
static func target_mine_count(params: Dictionary) -> int:
|
||||
return int(round(float(params.density) * float(int(params.w) * int(params.h) - BoardData.SPAWN_AREA_CELLS)))
|
||||
return int(round(float(params.density) * float(int(params.w) * int(params.h) - int(params.spawn_edge) * int(params.spawn_edge))))
|
||||
|
||||
|
||||
# rng_stream = 一次 open_stream("board_gen") 实例(一张图的全部取数;重试共用——attempt 边界非 scope 事件)。
|
||||
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(experiment_density.gd 等)——typed 形参会拒收桩表
|
||||
# db 参数不标具体类型原因:冒烟工具注入 FakeDB 桩(calibrate_boards.gd 等)——typed 形参会拒收桩表
|
||||
func generate(L: int, D: int, rng_stream: RandomNumberGenerator, db) -> BoardData: # type-hint: ignore(测量面 _BoardRowStub 冒烟夹具鸭子型注入——typed 参拒 stub)
|
||||
var row: Dictionary[Variant, Variant] = db.combat.board_gen.resolve_row(L, D)
|
||||
if row.is_empty():
|
||||
_fatal("BoardGen 行缺失 (%d,%d)" % [L, D])
|
||||
return null
|
||||
var params := _board_params(row, L, D)
|
||||
var params := _board_params(row, L, D, int(db.combat.board_gen.spawn_edge)) # 表头直索引——缺成员即崩=fail-fast
|
||||
var outcome := _attempt_loop(params, rng_stream, db)
|
||||
_finalize(outcome["board"], params, outcome["attempts"], outcome["mode"], outcome["r0_rej"])
|
||||
return outcome["board"]
|
||||
|
||||
|
||||
## 生成期表值单次读取快照(防热重载撕裂 attempt 语义;零 RNG)
|
||||
func _board_params(row: Dictionary, L: int, D: int) -> Dictionary:
|
||||
func _board_params(row: Dictionary, L: int, D: int, spawn_edge: int) -> Dictionary:
|
||||
return {
|
||||
"L": L, "D": D,
|
||||
"w": int(row.board_width), "h": int(row.board_height),
|
||||
@@ -47,6 +47,7 @@ func _board_params(row: Dictionary, L: int, D: int) -> Dictionary:
|
||||
# r0_cap=0 闸关。正式表行全行投值,加载期校验在 BoardGenTable.validate)
|
||||
"shave_r": int(row.get("shave_ring_radius", 1)),
|
||||
"r0_cap": int(row.get("r0_max_cap", 0)),
|
||||
"spawn_edge": spawn_edge, # 出生区边长(表头单点——经 generate 直索引注入)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,11 +101,12 @@ func _attempt(params: Dictionary, rng_stream: RandomNumberGenerator, db) -> Boar
|
||||
var b := BoardData.new()
|
||||
b.width = params.w
|
||||
b.height = params.h
|
||||
b.spawn_edge = int(params.spawn_edge)
|
||||
b.spawn_center = Vector2i(int((params.w - 1) / 2.0), int((params.h - 1) / 2.0))
|
||||
b.board_type = str(params.board_type)
|
||||
b.level_index = params.L
|
||||
b.node_depth = params.D
|
||||
# ② 布雷:N = round(d×(W×H−9)) 先定数;域 = 全格 − 出生 3×3;均匀布雷后"先布后删"(_populate_mines 承载——Fisher-Yates 取号段整体随迁)
|
||||
# ② 布雷:N = round(d×(W×H−edge²)) 先定数;域 = 全格 − 出生区(edge²);均匀布雷后"先布后删"(_populate_mines 承载——Fisher-Yates 取号段整体随迁)
|
||||
var removed := _populate_mines(b, params, rng_stream)
|
||||
# ③ 变异布置(地形变异 框架面):布置约束与算法随首批变异类型定稿(生成期布置接口结构位),
|
||||
# 定稿前零布置——清单为空是合法态(P0),静默产出无变异图。
|
||||
@@ -148,7 +150,7 @@ func _populate_mines(b: BoardData, params: Dictionary, rng_stream: RandomNumberG
|
||||
func _shave_ring(b: BoardData, shave_r: int) -> int:
|
||||
var ring := {}
|
||||
for c in b.all_cells():
|
||||
if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= 1 + shave_r:
|
||||
if maxi(absi(c.x - b.spawn_center.x), absi(c.y - b.spawn_center.y)) <= (int(b.spawn_edge) - 1) / 2 + shave_r:
|
||||
ring[c] = true
|
||||
var removed := 0
|
||||
for c in ring.keys():
|
||||
|
||||
@@ -122,7 +122,7 @@ func _place_variants(board: BoardData) -> void:
|
||||
func _place_variants_apply(board: BoardData, candidates: Array[Vector2i], stream: RandomNumberGenerator, vrow: Dictionary, total: int) -> void:
|
||||
var placed := {}
|
||||
while placed.size() < total:
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(2, int(db.combat.variation.pack_max)))
|
||||
var pack_size: int = mini(total - placed.size(), stream.randi_range(int(db.combat.variation.pack_min), int(db.combat.variation.pack_max)))
|
||||
var free: Array[Vector2i] = []
|
||||
for c in candidates:
|
||||
# 新片种子/生长格禁与其他已布片 8 邻贴接——否则两片粘连成一个超 pack_max 的连通分量
|
||||
|
||||
@@ -107,7 +107,8 @@ func _warn_resolve(table_id: String, category: String, message: String, l_key: i
|
||||
_diag.call("warning", category, message)
|
||||
|
||||
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,越域值 clamp(GDD Edge Case——组合事故非配置事故)
|
||||
# 难度曲线与Boss DifficultyTable 消费面(敌人系统.spawn 调用):(L,D) 精确匹配修饰乘数,
|
||||
# 越域值 clamp(域常量读 DifficultyTable.MULT_MIN/MULT_MAX 单点——使用时 clamp 口径,warning 预登记同族)
|
||||
func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
var dt = _difficulty_get.call()
|
||||
if dt == null:
|
||||
@@ -115,8 +116,8 @@ func resolve_difficulty_modifiers(L: int, D: int) -> Dictionary:
|
||||
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),
|
||||
"atk_mult": clampf(float(row.get("atk_mult", 1.0)), 0.1, 10.0),
|
||||
"hp_mult": clampf(float(row.get("hp_mult", 1.0)), DifficultyTable.MULT_MIN, DifficultyTable.MULT_MAX),
|
||||
"atk_mult": clampf(float(row.get("atk_mult", 1.0)), DifficultyTable.MULT_MIN, DifficultyTable.MULT_MAX),
|
||||
}
|
||||
return {}
|
||||
|
||||
|
||||
@@ -95,14 +95,15 @@ func consume_deck_token(kind: String, tier: String, feat_id: String = "") -> boo
|
||||
# 已拿上限判定(身份维度:kind 走 curio_cap、relic 走 feats 行 cap 字段;
|
||||
# taken 只在兑付时 +1〔拿了才计〕、刷出不计)。缺省放行=白名单外 kind 兜底。
|
||||
func cap_ok(kind: String, feat_id: String = "") -> bool:
|
||||
var cap := 99
|
||||
if kind == "relic":
|
||||
var row: Dictionary[Variant, Variant] = db.crossrun.feats.row_by_id(feat_id)
|
||||
cap = int(row.get("cap", 1)) if not row.is_empty() else 1
|
||||
return taken.get("relic:" + feat_id, 0) < cap
|
||||
if row.is_empty():
|
||||
push_error("[奇物牌库] relic 令牌引用悬空 feat_id=%s(feats 表无此行——配置违约,禁兜底)" % feat_id)
|
||||
return false
|
||||
return taken.get("relic:" + feat_id, 0) < int(row["cap"]) # 直索引——schema 必填保证在键
|
||||
if not db.run.adventure.CURIO_KINDS.has(kind):
|
||||
return true
|
||||
cap = int(db.run.adventure.curio_cap(kind))
|
||||
var cap: int = int(db.run.adventure.curio_cap(kind))
|
||||
return int(taken.get(kind, 0)) < cap
|
||||
|
||||
|
||||
|
||||
@@ -89,10 +89,11 @@ func _per_value(name: String, payload: Dictionary) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# 巢穴控制数(fact nest_count 取数面——敌方 weaken 实例数,猎巢人订阅面)
|
||||
# 巢穴控制数(fact nest_count 取数面——敌方名下 weaken 实例总数,猎巢人订阅面;
|
||||
# 个体化后削弱为累加行、每体各持一条——总数 = 被削弱个体数)
|
||||
func _nest_count() -> int:
|
||||
var n := 0
|
||||
for eff in effects.query_active("enemy"):
|
||||
for eff in effects.query_active_side("enemy"):
|
||||
if String(eff.effect_id) == "weaken":
|
||||
n += 1
|
||||
return n
|
||||
@@ -116,8 +117,8 @@ func submit(t: Dictionary) -> int:
|
||||
# board_flat_hero 的键并入行载荷,board_blast/board_typed 带 batch_key 供未生效行归因)。
|
||||
# attack_commit 发射点(我方伤害单据开立入口;
|
||||
# 命中挂单据 context 随行——施加面在 _resolve_hero_attack 管线算术前逐族施加,开单时点=主角攻击
|
||||
# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实〔连珠矢/
|
||||
# 执拗订阅面〕+巢穴控制数〔猎巢人〕)
|
||||
# 单恒为段内首单(结账现场与开单现场一致)。载荷 atk/target/matchup + 挖掘 streak 事实
|
||||
# (执拗订阅面)+巢穴控制数〔猎巢人〕)
|
||||
# spec 束必载键(缺载 assert——fail-fast):channel/base_value/target_rule/ri;
|
||||
# 可选键缺省=原形缺省:origin_cell=(-1,-1)/attack_type=""/rider=0/highlight=""/context={}。
|
||||
# 返回单据字典九栏目+requires/source_unit 逐字不变(契约 8)。
|
||||
@@ -196,6 +197,7 @@ func _on_unit_died(u: Dictionary) -> void:
|
||||
for t in _queue:
|
||||
if is_same(t.source_unit, u):
|
||||
t["cancelled"] = true # 死亡即不动手——该敌未结单据作废(静默零行零 commit)
|
||||
effects.purge_owner(StatusContainer.owner_key_for(u)) # 个体死亡即清名下实例(增益状态 v1.5——不转移不并回)
|
||||
|
||||
|
||||
# ---- 兑付管线(单点——所有伤害经过同一道门)----
|
||||
@@ -356,7 +358,7 @@ func _hero_attack_base_value(t: Dictionary, ctx: Dictionary) -> float:
|
||||
|
||||
|
||||
# 掷点兑现(rolled 分量/payload 归因/池乘区/下限/英雄侧 rider 族/落地扣血/击杀标记;
|
||||
# 击杀发射面拆 _kill_overflow)。返回 {landed, payload, hero_rider}(交付步消费)。
|
||||
# 击杀发射面拆 _kill_effects)。返回 {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"]
|
||||
@@ -370,7 +372,7 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
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 pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_offense", StatusContainer.OWNER_HERO), clamped_pool("enemy_defense", StatusContainer.owner_key_for(target)))
|
||||
var landed: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled)
|
||||
# 加法族:英雄侧 rider(enemy_skill rider 对称位补齐口径)——后置必中附加,独立于掷点与乘区
|
||||
# (奥术余烬「普攻命中后恒定结算」语义);猎巢人巢控光环(per nest_count)/矿工的执拗开格加成
|
||||
@@ -391,31 +393,16 @@ func _hero_attack_redeem(t: Dictionary, ctx: Dictionary, base_value: float) -> D
|
||||
units.apply_damage(target, landed)
|
||||
payload["killed"] = not target.alive # 击杀标记在扣血后取
|
||||
if bool(payload.get("killed", false)):
|
||||
_kill_overflow(t, target, landed)
|
||||
_kill_effects(t, target)
|
||||
return {"landed": landed, "payload": payload, "hero_rider": hero_rider}
|
||||
|
||||
|
||||
# kill 发射+施加(敌人死亡确认;贯穿射击溢出弹射订阅面:对下一存活敌
|
||||
# 以穿刺类型弹射本体 value%〔无次敌=弹射落空〕。B 模式入队段尾 flush 兑现;A 模式递归 resolve)
|
||||
func _kill_overflow(t: Dictionary, target: Dictionary, landed: int) -> void:
|
||||
for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", "")), "overflow": landed}):
|
||||
# kill 发射+施加(敌人死亡确认;订阅面=reveal_tiles 击杀揭示——斩首学者通道。
|
||||
# B 模式入队段尾 flush 兑现;A 模式端口路由宿主入队)
|
||||
func _kill_effects(t: Dictionary, target: Dictionary) -> void:
|
||||
for h in _fx_hits("kill", {"target": String(target.display_name), "target_id": String(target.get("enemy_id", ""))}):
|
||||
if String(h.get("kind", "")) == "reveal_tiles":
|
||||
_port.reveal_effect() # 斩首学者——击杀揭示 1 格(端口路由宿主入队,授予段末兑付)
|
||||
continue
|
||||
if String(h.get("kind", "")) != "overflow":
|
||||
continue
|
||||
var next_target = units.default_target()
|
||||
if next_target != null:
|
||||
# source_class 用中性名(fx_overflow 泛用通道,不挂件名)
|
||||
var of_t := open_ticket(DamageController.KIND_BOARD_TYPED, "fx_overflow", "贯穿弹射",
|
||||
{"channel": "board",
|
||||
"base_value": maxi(int(db.combat.settlement.damage_roll.damage_floor), int(landed * int(h.get("value", 50)) / 100)),
|
||||
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": int(t.round_index),
|
||||
"origin_cell": t.origin_cell, "attack_type": "pierce"})
|
||||
if policy == POLICY_SEGMENT:
|
||||
_queue.append(of_t)
|
||||
else:
|
||||
_resolve(of_t)
|
||||
|
||||
|
||||
# 交付:即时执行族(碎甲震击打重甲附加「护甲裂解」——复用 off_guard,零新状态行;
|
||||
@@ -424,8 +411,8 @@ func _hero_attack_deliver(t: Dictionary, ctx: Dictionary, redemption: Dictionary
|
||||
var payload: Dictionary[Variant, Variant] = redemption["payload"]
|
||||
for h in (ctx["ac_hits"] as Array):
|
||||
if String(h.get("kind", "")) == "apply_status":
|
||||
# source_class 用中性名(fx_apply_status 泛用通道,不挂件名)
|
||||
effects.apply_effect(String(h.get("status_id", "")), {"source_class": "fx_apply_status"}, -1)
|
||||
# source_class 用中性名(fx_apply_status 泛用通道,不挂件名);落点 = 被命中者个体(增益状态 v1.5)
|
||||
effects.apply_effect(String(h.get("status_id", "")), StatusContainer.owner_key_for(ctx["target"]), {"source_class": "fx_apply_status"}, -1)
|
||||
payload["cracked"] = true
|
||||
var label := _attack_label(String(t.display), bool(ctx["crit"]), int(ctx["discount"]))
|
||||
if int(redemption["hero_rider"]) > 0:
|
||||
@@ -473,7 +460,7 @@ func _enemy_roll(t: Dictionary) -> Dictionary:
|
||||
var discount: int = _roll_discount(int(roll_read.roll), threshold)
|
||||
var cdelta: int = _counter_delta(String(t.attack_type), String(hero.get("armor_type", "light")))
|
||||
var rolled: int = DamageMath.rolled_component(float(t.base_value), discount, crit, int(db.combat.settlement.damage_roll.crit_multiplier), cdelta)
|
||||
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense"), clamped_pool("enemy_output"))
|
||||
var pooled: int = DamageMath.pool_damage(rolled, clamped_pool("hero_defense", StatusContainer.OWNER_HERO), clamped_pool("enemy_output", StatusContainer.owner_key_for(t.source_unit)))
|
||||
var total: int = maxi(int(db.combat.settlement.damage_roll.damage_floor), pooled) + int(t.rider) # rider 后置于伤害下限——独立必中加项,不进乘区
|
||||
return {"roll": int(roll_read.roll), "threshold": threshold, "crit": crit, "discount": discount, "total": total}
|
||||
|
||||
@@ -593,13 +580,13 @@ func defender_ac(unit: Dictionary) -> int:
|
||||
var total: int = _hero_ac_override
|
||||
if total < 0:
|
||||
total = int(db.combat.hero.armor_class)
|
||||
for eff in effects.query_active("hero"):
|
||||
for eff in effects.query_active(StatusContainer.OWNER_HERO):
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(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"):
|
||||
for eff_e in effects.query_active(StatusContainer.owner_key_for(unit)):
|
||||
var erow_ac: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff_e.effect_id)
|
||||
if not erow_ac.is_empty() and String(erow_ac.unit) == "ac":
|
||||
total_e += int(eff_e.effective_value)
|
||||
@@ -625,10 +612,12 @@ func _discount_shift_up(discount: int, shift: int) -> int:
|
||||
return int(tbl[mini(tbl.size() - 1, idx + shift)])
|
||||
|
||||
|
||||
# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧)
|
||||
func clamped_pool(pool: String) -> int:
|
||||
# 池内加算 → clamp [-100,+100](池间乘算归 DamageMath.pool_damage 调用侧);
|
||||
# 池按个体读(增益状态 v1.5):主角侧池传 OWNER_HERO,敌方侧池传被打者/施放者个体键——
|
||||
# 乘区只合计该个体名下实例(B 敌的削弱不进 A 敌的账)
|
||||
func clamped_pool(pool: String, owner_key: String) -> int:
|
||||
var total := 0
|
||||
for eff in effects.query_active("hero") + effects.query_active("enemy"):
|
||||
for eff in effects.query_active(owner_key):
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(eff.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
|
||||
+16
-16
@@ -26,7 +26,7 @@ const MINE_EVENTS := ["dig_reveal", "dig_paid", "seal_hit", "mine_hit", "seal_se
|
||||
## grant_gp/bias_wallet、counter_add/counter_spend、reveal_tiles/remove_mines/refresh_pick、
|
||||
## bias_rewards/preview。需求卡片标题计 23 系笔误,落码按行内容全收——commit 留痕)
|
||||
const EFFECT_KINDS := ["add_ticket_rider", "modify_ticket", "damage_mult", "open_ticket",
|
||||
"attack_bonus", "attack_segments", "yield_mult", "spawn_spell", "overflow", "grant_material",
|
||||
"attack_bonus", "yield_mult", "spawn_spell", "grant_material",
|
||||
"grant_ap", "grant_gp", "bias_wallet", "shield", "spike", "apply_status", "counter_add",
|
||||
"counter_spend", "reveal_tiles", "remove_mines", "refresh_pick",
|
||||
"bias_rewards", "preview",
|
||||
@@ -100,7 +100,7 @@ func _init(db_ref: ConfigDb = null) -> void:
|
||||
## 集过滤)→树规则合成行。同一数据行的多个 trigger 按行内数组序。计数器四桶由宿主持有、随宿主生灭,
|
||||
## 本方法只重建订阅集(运行时可变状态=零;battle/level 桶的清零由宿主 new 天然承载,round 桶清零
|
||||
## 在 dispatch 的 round_start 特判内整桶 clear,adventure 桶随宿主字典引用跨战持久——无副本搬运)。
|
||||
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Array = []) -> void:
|
||||
func rebuild(feat_ids: Array, item_ids: Array, tree_rules: Dictionary[Variant, Variant] = {}) -> void:
|
||||
_triggers.clear()
|
||||
_load_feat_triggers(feat_ids)
|
||||
_load_attune_triggers(item_ids)
|
||||
@@ -134,42 +134,42 @@ func _load_attune_triggers(item_ids: Array) -> void:
|
||||
## sense 两组=每关限次〔level 桶计数器 lt 上限闸 + 命中即 counter_add 记账双行——同事件双行同
|
||||
## 预评估态见闸同记,序=装载序确定;寿命随 LevelRunner.level_counters 每关 new 天然按关清零〕;
|
||||
## economy 两组零订阅行——消费点=GP 掉落乘算与商品计价折价的 tree_rules 成员判定。数值全 provisional 归 M4。
|
||||
func _push_tree_rules(tree_rules: Array) -> void:
|
||||
for key_v in tree_rules:
|
||||
_push_tree_rule(String(key_v))
|
||||
func _push_tree_rules(tree_rules: Dictionary[Variant, Variant]) -> void:
|
||||
for rk in tree_rules:
|
||||
_push_tree_rule(String(rk), tree_rules[rk] as Dictionary)
|
||||
|
||||
|
||||
## 单树规则合成行 push(六键 match 分派——词表封闭,未知键零订阅零扰动)
|
||||
func _push_tree_rule(rk: String) -> void:
|
||||
## 单树规则合成行 push(六键 match 分派——词表封闭,未知键零订阅零扰动;参数全读规则键参数字典)
|
||||
func _push_tree_rule(rk: String, params: Dictionary[Variant, Variant]) -> void:
|
||||
match rk:
|
||||
"posture_first_strike":
|
||||
_push_trigger("tree", rk, {"on": "round_start",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": int(params["ap_bonus"])}}, {})
|
||||
"posture_bulwark":
|
||||
_push_trigger("tree", rk, {"on": "damage_taken",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "shield", "value": 1}}, {})
|
||||
"do": {"kind": "shield", "value": int(params["shield"])}}, {})
|
||||
"sense_sharp":
|
||||
# 锋=踩中危险格伤害减半·每关限次——mine_hit 挂点现成(damage_mult 收集→halve_pct,
|
||||
# 与惊巢反制同路径;伤害在结算战斗段末兑付,探索阶段零改动)
|
||||
_push_trigger("tree", rk, {"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "damage_mult", "value": 50}}, {})
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(params["cap_per_level"])}],
|
||||
"do": {"kind": "damage_mult", "value": int(params["halve_pct"])}}, {})
|
||||
_push_trigger("tree", rk, {"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(params["cap_per_level"])}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used",
|
||||
"value": 1}}, {})
|
||||
"sense_guard":
|
||||
# 守=标错返还行动点·每关限次(契约 12 失误补偿——seal_missed 由 _judge_seal 去重闸后
|
||||
# 发射;queue=下一轮开轮兑现,末轮判出无下一轮=合法作废自动成立)
|
||||
_push_trigger("tree", rk, {"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}, {})
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(params["cap_per_level"])}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": int(params["ap_refund"])}}, {})
|
||||
_push_trigger("tree", rk, {"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(params["cap_per_level"])}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used",
|
||||
"value": 1}}, {})
|
||||
|
||||
@@ -461,7 +461,7 @@ static func _vtr_from_refs(rid: String, tr: Dictionary, row: Dictionary, errs: A
|
||||
## 加法族(数值相加,可交换):add_ticket_rider、attack_bonus、grant_material、grant_ap(非 transfer)、
|
||||
## counter_add、shield——发射点逐条累加收集器。
|
||||
## 乘法族(收集后一次施加,正负分桶):yield_mult、damage_mult。
|
||||
## 任意真族(任一命中即生效):spawn_spell、overflow、spike。
|
||||
## 任意真族(任一命中即生效):spawn_spell、spike。
|
||||
## 按序改写族(收集→单点按声明序施加):modify_ticket 及一切改写窗口效果。
|
||||
## 按序消耗族(按订阅序消耗共享资源,耗尽即止;可选 fallback):counter_spend。
|
||||
## 即时执行族(每条命中各自执行一次,执行序=声明序):open_ticket、grant_ap(mode:transfer)、
|
||||
|
||||
@@ -91,7 +91,7 @@ func _init(db_ref: ConfigDb, rng_ref: RngServiceCore, ctx: Dictionary = {}) -> v
|
||||
treasure_inventory = ctx["treasure_inventory"] # 冒险级库存共享(同调未用行跨关存活——单源化承载面)
|
||||
_treasure_unlocked = ctx.get("treasure_unlocked", {}) if ctx.get("treasure_unlocked", {}) is Dictionary else {}
|
||||
_attunement = ctx.get("attunement", []) if ctx.get("attunement", []) is Array else [] # 活引用注入(契约 6——禁快照)
|
||||
_tree_rules = ctx.get("tree_rules", []) if ctx.get("tree_rules", []) is Array else [] # 活引用注入
|
||||
_tree_rules = ctx.get("tree_rules", {}) if ctx.get("tree_rules", {}) is Dictionary else {} # 活引用注入(规则键参数字典)
|
||||
_hero_attack_type = String(ctx.get("hero_attack_type", ""))
|
||||
_hero_armor_type = String(ctx.get("hero_armor_type", ""))
|
||||
_voucher_rap = maxi(0, int(ctx.get("voucher_rap", 0)))
|
||||
@@ -661,8 +661,8 @@ func current_battle() -> Variant:
|
||||
|
||||
# 特性持有集(begin_battle enc.feats 求值面;**共享引用不复制**——关内奖励节点兑付的 relic 须当关生效)
|
||||
var _feats: Array[Variant] = []
|
||||
# 树 rule 键集(begin_battle enc.tree_rules 携带;Fork 激活侧 effects——构造注入活引用)
|
||||
var _tree_rules: Array[Variant] = []
|
||||
# 树规则参数字典(begin_battle enc.tree_rules 携带;Fork 激活侧 effects_*——构造注入活引用)
|
||||
var _tree_rules: Dictionary[Variant, Variant] = {}
|
||||
|
||||
|
||||
# 特性持有集读面(HUD C 区特性段数据源)
|
||||
|
||||
@@ -384,9 +384,10 @@ func tree_buy_fork(node_id: String) -> bool:
|
||||
return save.write_to_disk()
|
||||
|
||||
|
||||
# 树规则键投影(激活 Fork 侧 effects 并入)
|
||||
func tree_rule_keys() -> Array:
|
||||
var out: Array[Variant] = []
|
||||
# 树规则参数字典投影(激活 Fork 侧 effects_* 参数字典并入——键=规则键,值=该键参数字典原样透传;
|
||||
# 首遇优先去重语义保持)
|
||||
func tree_rules() -> Dictionary[Variant, Variant]:
|
||||
var out: Dictionary[Variant, Variant] = {}
|
||||
var sides: Dictionary[Variant, Variant] = meta_state.tree_state().fork_sides
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
@@ -397,7 +398,7 @@ func tree_rule_keys() -> Array:
|
||||
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))
|
||||
out[String(k)] = fx[k]
|
||||
return out
|
||||
|
||||
|
||||
|
||||
@@ -368,6 +368,19 @@ func fx_reveal(c: Vector2i) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
## 挖一格→真实翻开集只读预览(sim 信息面出口——与 _classify 同口径:本格封印视为已隐式退役、
|
||||
## 其余封印照挡;复用 _chain_closure 闭包;零状态写入、零事件发射)
|
||||
func preview_reveal(c: Vector2i) -> Array[Vector2i]:
|
||||
var sim_seals := _seals.duplicate()
|
||||
sim_seals.erase(c)
|
||||
var closure := _chain_closure(c, sim_seals)
|
||||
var out: Array[Vector2i] = []
|
||||
for cc in closure.keys():
|
||||
if cc != c and not _revealed.has(cc):
|
||||
out.append(cc) # F2 口径:新开格集——不含首格与已开格
|
||||
return out
|
||||
|
||||
|
||||
# 连锁闭包:μX.[O∪{c} ⊆ X ∧ ∀x∈X:(x∈S ∧ num(x)=0 ⇒ N₈(x)∩S∖G ⊆ X)]——连锁不越封印
|
||||
func _chain_closure(start: Vector2i, seal_set: Dictionary) -> Dictionary:
|
||||
var chain := {start: true}
|
||||
|
||||
@@ -9,7 +9,6 @@ extends RefCounted
|
||||
|
||||
const NODE_TYPES := ["battle", "boss", "reward", "rest", "shop"]
|
||||
const FILLER_TYPES := ["battle", "reward", "rest"]
|
||||
const MAX_WIDTH := 4 # 产出约束·宽度=分叉生长(同深度节点数由岔口长出 1→2→3→4,禁摆平行层)
|
||||
# 多终点图的支路互斥段必须各有独有类型(契约 5③),而商店与 Boss 是每条支路共有件,
|
||||
# 独有类型只能来自 battle/reward/rest 三件 ⇒ 归属大岔口各区域的填充节点取互斥签名类型。
|
||||
# 入口与主链段(大岔口上游,不属任何支路互斥段)恒为战斗节点——承载战斗保底与岔口下一跳互异基准形。
|
||||
@@ -66,40 +65,11 @@ func _setup_and_build(params: Dictionary, seed_val: int) -> Dictionary:
|
||||
|
||||
## 参数档可行性判定(执行边界③:参数无解=配置错误)——域校验外加深度预算与战斗保底可放性。
|
||||
static func check_params(params: Dictionary) -> Array:
|
||||
var errs: Array[Dictionary] = _cp_domain(params)
|
||||
if not errs.is_empty():
|
||||
return errs
|
||||
# 域校验并单点(R7——参数档域合法性唯一在 LevelGenParams.validate 加载期;
|
||||
# 生成器只留可行性判定 _cp_structural——schema 无此判定的生成器职责)
|
||||
return _cp_structural(params)
|
||||
|
||||
|
||||
## 域校验段:五旋钮值域/岔口去路数锁值/配比四类合计 1
|
||||
static func _cp_domain(params: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
var L := int(params.get("layers", 0))
|
||||
var e := int(params.get("exit_count", 0))
|
||||
var en := int(params.get("entry_count", 0))
|
||||
var bf := int(params.get("battle_floor", -1))
|
||||
var bd := float(params.get("branch_density", -1.0))
|
||||
if L < 6:
|
||||
errs.append({"category": "domain", "message": "层深 %d 低于下限 6(层高已锁)" % L})
|
||||
if e < 1 or e > 3:
|
||||
errs.append({"category": "domain", "message": "出口个数 %d 越域 [1,3]" % e})
|
||||
if en < 1:
|
||||
errs.append({"category": "domain", "message": "入口个数 %d 越域(≥1)" % en})
|
||||
if bd < 0.0 or bd > 1.0:
|
||||
errs.append({"category": "domain", "message": "分支密度 %f 越域 [0,1]" % bd})
|
||||
if bf < 0:
|
||||
errs.append({"category": "domain", "message": "战斗保底 %d 越域(≥0)" % bf})
|
||||
if int(params.get("fork_ways_min", 2)) < 2 or int(params.get("fork_ways_max", 3)) > 3:
|
||||
errs.append({"category": "domain", "message": "岔口去路数域越界(锁 2/3)"})
|
||||
var sum := 0.0
|
||||
for rk in ["battle_ratio", "reward_ratio", "rest_ratio", "shop_ratio"]:
|
||||
sum += float(params.get(rk, -1.0))
|
||||
if not is_equal_approx(sum, 1.0):
|
||||
errs.append({"category": "domain", "message": "配比四类合计 %f ≠ 1" % sum})
|
||||
return errs
|
||||
|
||||
|
||||
## 结构可行性段:主链段容量与战斗保底可放性(多出口按区域签名累加可放槽)
|
||||
static func _cp_structural(params: Dictionary) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
@@ -271,8 +241,8 @@ func _vo_width_rules(nodes: Array, errs: Array) -> void:
|
||||
var d2: int = int(n["depth"])
|
||||
per_depth[d2] = int(per_depth.get(d2, 0)) + 1
|
||||
for d3 in per_depth:
|
||||
if int(per_depth[d3]) > MAX_WIDTH:
|
||||
errs.append({"category": "structural", "message": "深度 %d 节点数 %d 超分叉生长上限 4(平行层摆放)" % [d3, per_depth[d3]]})
|
||||
if int(per_depth[d3]) > int(_params["width_max"]):
|
||||
errs.append({"category": "structural", "message": "深度 %d 节点数 %d 超分叉生长上限(width_max 表值——平行层摆放)" % [d3, per_depth[d3]]})
|
||||
|
||||
|
||||
## 计数规则③·参数档对照:战斗保底/入口数/终点数/层高(params 为空时跳过)
|
||||
@@ -358,7 +328,7 @@ func _build_region(r: int, e: int, L: int, prevs: Array, terminals: Array) -> vo
|
||||
## 侧支节点与主去路同深(保持「深度=自入口最长路径步数」不变量),两支各自汇入主去路的后继,
|
||||
## 于是支路互斥段={主去路节点} 与 {侧支节点}——两支类型互异即同时满足「下一跳互异」与「各有独有类型」。
|
||||
func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void:
|
||||
var wanted := clampi(int(round(float(_params["branch_density"]) * float(L))), 1, 24)
|
||||
var wanted := maxi(int(round(float(_params["branch_density"]) * float(L))), 1) # 死上限 24 删(不可达值——下限 1 保留防零岔口)
|
||||
var anchors := _collect_fork_anchors(L, t, e)
|
||||
anchors = _shuffle(rg, anchors)
|
||||
var selected := {} # 已扩岔节点 id
|
||||
@@ -377,10 +347,10 @@ func _add_forks(rg: RandomNumberGenerator, L: int, t: int, e: int) -> void:
|
||||
continue
|
||||
if (v["next"] as Array).is_empty():
|
||||
continue # 主去路无后继(无可汇入点)——该锚点弃用
|
||||
var extra := 2 if (int(_params["fork_ways_max"]) >= 3 and rg.randf() < 0.35) else 1
|
||||
var extra := 2 if (int(_params["fork_ways_max"]) >= 3 and rg.randf() < float(_params["fork_third_way_pct"]) / 100.0) else 1
|
||||
var made := 0
|
||||
for k in range(extra):
|
||||
if _depth_count(int(v["depth"])) >= MAX_WIDTH:
|
||||
if _depth_count(int(v["depth"])) >= int(_params["width_max"]):
|
||||
break
|
||||
var wid := _mk("battle", int(v["depth"]))
|
||||
(a["next"] as Array).append(wid)
|
||||
@@ -452,7 +422,7 @@ func _fill_ratios(rg: RandomNumberGenerator) -> void:
|
||||
free = _shuffle(rg, free)
|
||||
var want := int(round(float(_pool.size()) * (float(_params["reward_ratio"]) + float(_params["rest_ratio"]))))
|
||||
var floor_n := int(_params["battle_floor"])
|
||||
var rest_first := rg.randf() < 0.5
|
||||
var rest_first := rg.randf() < float(_params["fill_rest_first_pct"]) / 100.0
|
||||
for i in range(want):
|
||||
if i >= free.size():
|
||||
break
|
||||
|
||||
@@ -223,8 +223,9 @@ func _dispatch_row(row: Dictionary, att: Dictionary) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
# percent 行兑付:GP 型行走战斗内缓冲记账;其余 apply_effect 入容器;
|
||||
# 满层拒收 = "奖励未生效"行(零静默)
|
||||
# percent 行兑付:GP 型行走战斗内缓冲记账;其余 apply_effect 入容器(逐个体——enemy 侧行 =
|
||||
# 全体存活敌逐只各发一条实例,hero 侧行 = 主角单桶,增益状态 v1.5 目标规则「全体存活敌」);
|
||||
# 满层拒收 = "奖励未生效"行(零静默——全体皆拒才记一行;部分个体满层拒由该个体容器内原子拒绝)
|
||||
func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool:
|
||||
var eff := String(row.effect_id)
|
||||
if eff == "gp_gain" or eff == "gp_loss":
|
||||
@@ -236,11 +237,20 @@ func _apply_percent_row(row: Dictionary, att: Dictionary) -> bool:
|
||||
var eff_mag: int = int(srow.magnitude)
|
||||
if prem_pct != 100:
|
||||
eff_mag = int(round(eff_mag * prem_pct / 100.0))
|
||||
var ok: bool = status_effects.apply_effect(eff, _grant_src(att, prem_pct, eff_mag))
|
||||
if ok:
|
||||
var owner_keys: Array[Variant] = []
|
||||
if String(srow.target_side) == "enemy":
|
||||
for u in units.query_alive("enemy"):
|
||||
owner_keys.append(StatusContainer.owner_key_for(u))
|
||||
else:
|
||||
owner_keys.append(StatusContainer.OWNER_HERO)
|
||||
var granted := 0
|
||||
for okey in owner_keys:
|
||||
if status_effects.apply_effect(eff, String(okey), _grant_src(att, prem_pct, eff_mag)):
|
||||
granted += 1
|
||||
if granted > 0:
|
||||
emit_cb.call({"type": "effect", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1))},
|
||||
int(att.get("ri", 0)), String(srow.display_name) + String(att.get("label_suffix", "")), eff_mag,
|
||||
{"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff))})
|
||||
{"effect_id": eff, "pool_value": int(pool_effective_cb.call(eff, String(owner_keys[0])))})
|
||||
return true
|
||||
emit_cb.call({"type": "reward_ineffective", "source_class": String(att.get("source_class", "")), "origin_cell": att.get("cell", Vector2i(-1, -1)), "highlight": String(att.get("highlight", ""))},
|
||||
int(att.get("ri", 0)), "奖励未生效(满层)%s" % String(att.get("label_suffix", "")), 0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
class_name CombatSegment
|
||||
extends RefCounted
|
||||
# ③ 战斗段 Segment(P1-1 结算巨函数拆段;段边界=审计五段〔裁点③甲〕)。
|
||||
# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak/连珠矢段数)+ 蓄力层注入 + 主角先手
|
||||
# (含连珠矢分段开单)+ 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。
|
||||
# 职责:案C 闸复位 + 本轮扫雷事实推导(执拗 streak 取数面)+ 蓄力层注入 + 主角先手
|
||||
# + 敌方行动兑现 + 地图伤害账殿后(补提交统一结账)。
|
||||
# 调用序:主角先手 → 逐敌还手 → 地图伤害账殿后〔案C〕;hero hp=0 短路。
|
||||
# 行发射/开单单点归宿主(oc)——案C 暂存闸归 RewardGrant(段持三引用直调,零转发壳)——
|
||||
# 本函数体为原编排器 _combat_segment 逐字搬运(🔴 零行为变更契约)。
|
||||
@@ -21,7 +21,7 @@ func _init(host, fx_host_ref: BattleFxHost, reward_grant_ref: RewardGrant) -> vo
|
||||
|
||||
func run(ri: int) -> void:
|
||||
reward_grant.defer["on"] = false # 案C 闸复位——战斗段起开单即入队(主角/敌方/补提交的地图单按开单序统一结账)
|
||||
# 本轮扫雷事实推导(执拗 streak/连珠矢段数共用取数面):本轮 safe 事件数=
|
||||
# 本轮扫雷事实推导(执拗 streak 取数面):本轮 safe 事件数=
|
||||
# streak;踩雷即清零(mine_hit 监听同口——执拗「踩雷清零」承载)。攻击在战斗段=授予段后
|
||||
# 同一次 _settle 内,facts 已定。
|
||||
var round_safe := 0
|
||||
@@ -56,18 +56,6 @@ func _hero_strike(ri: int) -> void:
|
||||
"ri": ri, "origin_cell": Vector2i(-1, -1),
|
||||
"attack_type": 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)), int(oc.fx_host().facts_inject["dig_streak"]) / maxi(1, int(h.get("divisor", 3))))
|
||||
for s in segments:
|
||||
# source_class 用中性名;attack_segments 兑付通道保留泛用面
|
||||
var ct := oc._damage.open_ticket(DamageController.KIND_BOARD_TYPED, "fx_streak_segments", "连击分段",
|
||||
{"channel": "board", "base_value": int(h.get("value", 0)),
|
||||
"target_rule": DamageController.TARGET_FRONT_ALIVE, "ri": ri,
|
||||
"origin_cell": oc.minesweeper.hero_pos(), "attack_type": "pierce"})
|
||||
oc._damage.submit(ct)
|
||||
|
||||
|
||||
# 敌方行动(敌人技能池批——本拍动作开轮已摇定,此处按技能行兑现;行序 = 生成序)。
|
||||
@@ -83,9 +71,11 @@ func _enemy_actions(ri: int) -> void:
|
||||
continue
|
||||
if String(srow.type) == "buff" or String(srow.type) == "debuff":
|
||||
# 增益/妨害技 = 0 伤害施放行:不掷不折;状态授予即数值兑现,技能冷却与增益持续两计时器分账。
|
||||
# 落点由状态行 target_side 路由(buff → 敌方侧 / debuff → 主角侧——容器零改动,proposal-status-vocabulary §1)。
|
||||
# 落点按目标规则(增益状态 v1.5〈目标规则与个体归属〉):buff → 施放者个体(「自己」——
|
||||
# 敌方增益技只强化施放者自己,不再全体受益);debuff → 主角(off_guard/sapped 同义主角侧)。
|
||||
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)
|
||||
var owner_key: String = StatusContainer.owner_key_for(u) if String(srow.type) == "buff" else StatusContainer.OWNER_HERO
|
||||
oc._status_effects.apply_effect(String(srow.buff_ref), owner_key, {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": ri}, dur)
|
||||
oc._emit_row({"type": "cast", "source_class": "enemy_action", "origin_cell": 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
|
||||
|
||||
@@ -53,12 +53,14 @@ func current_price(prow: Dictionary) -> int:
|
||||
+ int(prow.get("price_increment", 0)) * purchased_tier(String(prow.get("product_id", ""))))
|
||||
|
||||
|
||||
# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×spend_discount〔表 knob,缺省 0.8〕,
|
||||
# 散折价单点(财富之道·散 economy_spend 激活时商品购买价 ×discount_pct/100〔规则键参数字典——
|
||||
# 折率读 economy_spend 参数,零表字段〕,
|
||||
# floor 取整偏向玩家、下限 1 防 1 元商品折成 0;范围=商品标价定义边界〔legacy 商品面 current_price
|
||||
# + 池货架面 pool_price 同折〕——商店刷新价 shop_refresh_cost 与三选一付费刷新不折)
|
||||
func _spend_discounted(price: int) -> int:
|
||||
if _rules().has("economy_spend"):
|
||||
return maxi(1, int(floor(float(price) * float(db.run.shop.spend_discount))))
|
||||
var spend_params: Dictionary[Variant, Variant] = _rules()["economy_spend"] as Dictionary
|
||||
return maxi(1, int(floor(float(price) * float(spend_params["discount_pct"]) / 100.0)))
|
||||
return price
|
||||
|
||||
|
||||
@@ -152,7 +154,7 @@ func _draw_shelf() -> void:
|
||||
continue
|
||||
seen.append(k)
|
||||
shelf.append(tok)
|
||||
if shelf.size() >= 3:
|
||||
if shelf.size() >= int(db.run.shop.shelf_size):
|
||||
break
|
||||
# T5 专长凭证注入(非 Boss 渠道唯一专长入口——货架与节点共享保底计数;判定含 pity 记账,替换末位占一席。
|
||||
# 取数复用本函数 shop_draw scope 的 stream——零新 scope,确定性序列不漂移)
|
||||
@@ -329,8 +331,8 @@ func _level_index() -> int:
|
||||
return int((_refs["level_row"] as Dictionary).get("level_index", 0))
|
||||
|
||||
|
||||
func _rules() -> Array:
|
||||
return _refs["tree_rules"] as Array
|
||||
func _rules() -> Dictionary[Variant, Variant]:
|
||||
return _refs["tree_rules"] as Dictionary
|
||||
|
||||
|
||||
func _attunement() -> Array:
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
## 增益状态统一容器:三方效果同构实例、单一时限量纲(先兑付后衰减)、按行叠层与授予时快照抗热重载(垂直切片验证:增益减益逐行归因)。
|
||||
## 增益状态统一容器:三方效果同构实例、单一时限量纲(先兑付后衰减)、按行叠层与授予时快照抗热重载。
|
||||
## 实例按个体归属(增益状态 v1.5):主角一桶+每只敌各一桶——全体授予=授予路径逐只各发一条实例,
|
||||
## 合并只在同体内(同名状态不同个体互不相干),个体死亡即清名下实例(purge_owner 单点)。
|
||||
## 零随机零信号——纯被动服务;授予唯一入口 apply_effect,满层拒绝零写入(拒收记未生效行归调用方)。
|
||||
class_name StatusContainer
|
||||
extends RefCounted
|
||||
|
||||
const OWNER_HERO := "hero" # 主角个体键(单桶——主角全体状态唯一的归属键)
|
||||
|
||||
var db: ConfigDb
|
||||
var _open := false
|
||||
var _instances: Array[Dictionary] = [] # 插入序;元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display}
|
||||
var _buckets: Dictionary[Variant, Variant] = {} # owner_key -> Array[Dictionary](桶内插入序);元素 = {effect_id, layers, remaining, source_class, origin_cell, round_index, snap_m, snap_unit, snap_display}
|
||||
|
||||
|
||||
# 个体键约定单点(拼写禁散落第二处——授予/死亡清/消费面一律经此导出):
|
||||
# 主角 = OWNER_HERO;敌人 = "e<生成序>"(生成序 = 开战逐只编号 handle_id 的 1 基序,沿顺位轴)。
|
||||
static func owner_key_for(u: Dictionary) -> String:
|
||||
if bool(u.get("is_hero", false)):
|
||||
return OWNER_HERO
|
||||
assert(u.has("handle_id"), "[增益状态] 敌单位缺 handle_id(个体键导出沿生成序轴——单位违约)")
|
||||
return "e%d" % (int(u.get("handle_id", -1)) + 1)
|
||||
|
||||
|
||||
func _init(db_ref: ConfigDb) -> void:
|
||||
@@ -18,35 +31,62 @@ func notify_battle_start() -> bool:
|
||||
push_warning("[增益状态] Active 下重复 notify_battle_start(EC11)")
|
||||
return false
|
||||
_open = true
|
||||
_instances.clear()
|
||||
_buckets.clear()
|
||||
return true
|
||||
|
||||
|
||||
# 唯一授予入口(挖掘期零直写——战斗结算 结算编排期统一调用)。
|
||||
# owner_key(个体归属——增益状态 v1.5):实例挂到该个体桶,合并/衰减/查询全在桶内;
|
||||
# duration_override(敌人技能池批——增益技施放):技能行 buff_duration 覆盖状态行时长(-1 = 状态表值)。
|
||||
# 施放是持续量的 owner(proposal §2.2 字段词汇表"buff_duration 增益持续回合数");其他授予路径传缺省。
|
||||
func apply_effect(effect_id: String, source: Dictionary, duration_override: int = -1) -> bool:
|
||||
func apply_effect(effect_id: String, owner_key: String, source: Dictionary = {}, duration_override: int = -1) -> bool:
|
||||
if not _open:
|
||||
push_warning("[增益状态] 域闭授予(EC1)")
|
||||
return false
|
||||
if owner_key == "":
|
||||
push_warning("[增益状态] 空 owner_key 授予(个体归属违约)")
|
||||
return false
|
||||
return _apply_open(effect_id, owner_key, source, duration_override)
|
||||
|
||||
|
||||
# 开域授予主路径(守卫已过——未知行警告与三臂受理;单出口布尔受理)
|
||||
func _apply_open(effect_id: String, owner_key: String, source: Dictionary, duration_override: int) -> bool:
|
||||
var accepted := false
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(effect_id)
|
||||
if row.is_empty():
|
||||
push_warning("[增益状态] 未知 effect_id(EC2): %s" % effect_id)
|
||||
return false
|
||||
var policy: String = row.stack_policy
|
||||
var dur: int = int(row.duration_settlements) if duration_override < 1 else duration_override
|
||||
var max_stack: int = int(row.max_stack)
|
||||
var snap := _grant_snapshot(row, source)
|
||||
var bucket: Array[Dictionary] = _bucket_of(owner_key)
|
||||
var hit: Dictionary[Variant, Variant] = {}
|
||||
for inst in bucket:
|
||||
if inst.effect_id == effect_id:
|
||||
hit = inst
|
||||
break
|
||||
match policy:
|
||||
"independent":
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
bucket.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
accepted = true
|
||||
"accumulate":
|
||||
return _apply_accumulate(effect_id, dur, snap, source)
|
||||
# 命中加时长未命中追加(R += D 取本次授予生效时长;L 不变;快照沿首授)
|
||||
if hit.is_empty():
|
||||
bucket.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
else:
|
||||
hit.remaining = int(hit.remaining) + dur
|
||||
accepted = true
|
||||
"stack":
|
||||
return _apply_stack(effect_id, dur, snap, source, max_stack)
|
||||
push_warning("[增益状态] 未知 stack_policy")
|
||||
return false
|
||||
# 满层拒零写入·未满加层·未命中追加(stack 授予不触碰 R;拒收由战斗结算记"奖励未生效"行)
|
||||
if hit.is_empty():
|
||||
bucket.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
accepted = true
|
||||
elif int(hit.layers) < int(row.max_stack):
|
||||
hit.layers = int(hit.layers) + 1
|
||||
accepted = true
|
||||
_:
|
||||
push_warning("[增益状态] 未知 stack_policy")
|
||||
return accepted
|
||||
|
||||
|
||||
## 授予时快照域(抗表重载——EC9;封印溢价贯穿——迭代14② T4 percent 形态同乘快照、缺省路径零扰动)
|
||||
@@ -57,27 +97,13 @@ func _grant_snapshot(row: Dictionary, source: Dictionary) -> Dictionary:
|
||||
return {"m": snap_m, "unit": String(row.unit), "display": String(row.display_name)}
|
||||
|
||||
|
||||
## 累加臂:命中加时长未命中追加(R += D 取本次授予生效时长——覆盖值 ≥1 按覆盖值;L 不变;快照沿首授;每次授予必有剩余次数变化)
|
||||
func _apply_accumulate(effect_id: String, dur: int, snap: Dictionary, source: Dictionary) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
inst.remaining = int(inst.remaining) + dur
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
|
||||
|
||||
## stack 臂:满层拒零写入·未满加层·未命中追加(stack 授予不触碰 R——spec 决议;
|
||||
## 满层拒绝原子——容器零写入,拒收由战斗结算记"奖励未生效"行)
|
||||
func _apply_stack(effect_id: String, dur: int, snap: Dictionary, source: Dictionary, max_stack: int) -> bool:
|
||||
for inst in _instances:
|
||||
if inst.effect_id == effect_id:
|
||||
if int(inst.layers) >= max_stack:
|
||||
return false
|
||||
inst.layers = int(inst.layers) + 1
|
||||
return true
|
||||
_instances.append(_new_instance(effect_id, 1, dur, source, snap))
|
||||
return true
|
||||
## 个体桶取用(缺桶新建——实例只在桶内合并,不同体永不互见)
|
||||
func _bucket_of(owner_key: String) -> Array[Dictionary]:
|
||||
if not _buckets.has(owner_key):
|
||||
var fresh: Array[Dictionary] = []
|
||||
_buckets[owner_key] = fresh
|
||||
return fresh
|
||||
return _buckets[owner_key]
|
||||
|
||||
|
||||
func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary, snap: Dictionary) -> Dictionary:
|
||||
@@ -94,16 +120,15 @@ func _new_instance(effect_id: String, layers: int, dur: int, source: Dictionary,
|
||||
}
|
||||
|
||||
|
||||
# 只读投影(插入序稳定枚举;六归因字段 + display_name + 授予时快照)
|
||||
func query_active(side: String) -> Array:
|
||||
# 只读投影(个体桶插入序稳定枚举;owner_key 随行出户 + 六归因字段 + display_name + 授予时快照)
|
||||
func query_active(owner_key: String) -> Array:
|
||||
var out: Array[Dictionary] = []
|
||||
for inst in _instances:
|
||||
for inst in _buckets.get(owner_key, [] as Array[Dictionary]):
|
||||
var row: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id(inst.effect_id)
|
||||
if row.is_empty():
|
||||
continue
|
||||
if String(row.target_side) != side:
|
||||
continue
|
||||
out.append({
|
||||
"owner_key": owner_key,
|
||||
"effect_id": inst.effect_id,
|
||||
"layers": inst.layers,
|
||||
"remaining_settlements": inst.remaining,
|
||||
@@ -118,26 +143,47 @@ func query_active(side: String) -> Array:
|
||||
return out
|
||||
|
||||
|
||||
# 轮末衰减:先兑付后衰减;每实例恰 −1;归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载)
|
||||
# 侧汇总投影(主角侧 = hero 桶;敌方侧 = 各存活个体桶并集——死亡个体桶已随 purge_owner 删除,
|
||||
# 并集即全部在册实例;数值与旧阵营共享读法等价,服务计数类汇总消费面)
|
||||
func query_active_side(side: String) -> Array:
|
||||
if side == OWNER_HERO:
|
||||
return query_active(OWNER_HERO)
|
||||
var out: Array[Dictionary] = []
|
||||
for key in _buckets.keys():
|
||||
if String(key) != OWNER_HERO:
|
||||
out.append_array(query_active(String(key)))
|
||||
return out
|
||||
|
||||
|
||||
# 轮末衰减:先兑付后衰减;逐桶各 −1(每实例每结算恰一次);归 0 移除;返回移除数 k(恰一次执行由 战斗结算 编排信任承载)
|
||||
func decay_round() -> int:
|
||||
if not _open:
|
||||
return 0 # EC13 域闭兜底读法——容器恒空,无独立分支不诊断
|
||||
var k := 0
|
||||
var survivors: Array[Dictionary] = []
|
||||
for inst in _instances:
|
||||
inst.remaining = int(inst.remaining) - 1
|
||||
if inst.remaining <= 0:
|
||||
k += 1
|
||||
else:
|
||||
survivors.append(inst)
|
||||
_instances = survivors
|
||||
for key in _buckets.keys():
|
||||
var bucket: Array[Dictionary] = _buckets[key]
|
||||
var survivors: Array[Dictionary] = []
|
||||
for inst in bucket:
|
||||
inst.remaining = int(inst.remaining) - 1
|
||||
if inst.remaining <= 0:
|
||||
k += 1
|
||||
else:
|
||||
survivors.append(inst)
|
||||
_buckets[key] = survivors
|
||||
return k
|
||||
|
||||
|
||||
# 个体死亡即清(死亡清唯一通道——死亡个体名下实例全部移除,不转移、不并回别处;返回移除数)
|
||||
func purge_owner(owner_key: String) -> int:
|
||||
var removed := (_buckets.get(owner_key, [] as Array[Dictionary]) as Array).size()
|
||||
_buckets.erase(owner_key)
|
||||
return removed
|
||||
|
||||
|
||||
func notify_battle_end() -> bool:
|
||||
if not _open:
|
||||
push_warning("[增益状态] BattleClosed 下重复 notify_battle_end(EC11)")
|
||||
return false
|
||||
_open = false
|
||||
_instances.clear() # 全清不跨战斗
|
||||
_buckets.clear() # 全清不跨战斗
|
||||
return true
|
||||
@@ -4,7 +4,7 @@
|
||||
class_name ActionEconomyTable
|
||||
extends Resource
|
||||
|
||||
@export var ap_per_round: int = 6 # [1,32] knob(默认值 = 现行表值基线,随 AP 压速调整)
|
||||
@export var ap_per_round: int # [1,32] knob(零哨兵——表行缺字段越域即加载致命)
|
||||
@export var dig_ap_cost: int = 1 # [1,8] knob(域下限 1 = 零预算系统失效拒绝)
|
||||
@export var seal_place_ap_cost: int = 1 # [0,8] knob(无 seal_pull_ap_cost 字段——拔印不占 AP)
|
||||
@export var rap_refresh_cost: int = 6 # [1,64] knob(默认值随 AP 压速调整)
|
||||
|
||||
@@ -5,6 +5,7 @@ class_name BoardGenTable
|
||||
extends Resource
|
||||
|
||||
@export var rows: Array[Dictionary] = [] # Array[Dictionary]
|
||||
@export var spawn_edge: int = 0 # 出生区边长(奇数 [3,5]——零哨兵:表头缺字段=加载期域校验拒)
|
||||
const SOLVER_RULES := ["single", "single_subset"]
|
||||
const COST_BUDGET := 16384 # F2c provisional 量级护栏(禁墙钟逃生)
|
||||
const TARGET_ABS_FLOOR := 0.85 # 战斗结算 修订——target 域下限(P1 绝对锚)
|
||||
@@ -13,6 +14,8 @@ const BOARD_TYPE_VOCABULARY := ["dungeon", "underdark", "deep_underdark"] #
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array[Dictionary] = []
|
||||
if spawn_edge < 3 or spawn_edge > 5 or spawn_edge % 2 == 0:
|
||||
errs.append({"category": "domain", "message": "spawn_edge 越域(奇数 [3,5]——零缺省): %d" % spawn_edge})
|
||||
if rows.is_empty():
|
||||
errs.append({"category": "empty_table", "message": "BoardGenTable 为空"})
|
||||
return errs
|
||||
@@ -73,8 +76,8 @@ func _validate_row_constraints(row: Dictionary) -> Array[Dictionary]:
|
||||
var att: int = int(row.get("max_gen_attempts", 0))
|
||||
if att < 1 or att > 32:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) max_gen_attempts 越域 [1,32]" % [L, D]})
|
||||
# 布雷数下限(R3)——零雷板拒绝(−9 = 出生区格数,常量单点 BoardData.SPAWN_AREA_CELLS)
|
||||
var n_mines := int(round(d * float(w * h - BoardData.SPAWN_AREA_CELLS)))
|
||||
# 布雷数下限(R3)——零雷板拒绝(−spawn_edge² = 出生区格数,表头单点导出)
|
||||
var n_mines := int(round(d * float(w * h - spawn_edge * spawn_edge)))
|
||||
if n_mines < 1:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) 布雷数下限违例 round(d×(W×H−9)) ≥ 1" % [L, D]})
|
||||
# F2b min_safe 可行性
|
||||
@@ -102,9 +105,9 @@ func _validate_shave_params(row: Dictionary) -> Array[Dictionary]:
|
||||
errs.append({"category": "domain", "message": "(%d,%d) shave_ring_radius 越域 [0,2]: %d" % [L, D, sr]})
|
||||
var r0_cap: int = int(row.get("r0_max_cap", 0))
|
||||
if r0_cap > 0:
|
||||
# 护栏(需求卡片 §二):cap ≥ 出生格数 9(BoardData.SPAWN_AREA_CELLS)、cap ≤ 板格数一半
|
||||
if r0_cap < BoardData.SPAWN_AREA_CELLS:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, BoardData.SPAWN_AREA_CELLS, r0_cap]})
|
||||
# 护栏(需求卡片 §二):cap ≥ 出生格数(spawn_edge² 表头导出)、cap ≤ 板格数一半
|
||||
if r0_cap < spawn_edge * spawn_edge:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 低于出生保障面 %d: %d" % [L, D, spawn_edge * spawn_edge, r0_cap]})
|
||||
if r0_cap > (w * h) / 2:
|
||||
errs.append({"category": "cross_field", "message": "(%d,%d) r0_max_cap 超板格数一半 %d: %d" % [L, D, (w * h) / 2, r0_cap]})
|
||||
return errs
|
||||
|
||||
@@ -22,7 +22,7 @@ func validate() -> Array:
|
||||
seen[key] = true
|
||||
if L < 1 or D < 0:
|
||||
errs.append({"category": "domain", "message": "难度键越域 (%d,%d)" % [L, D]})
|
||||
# 乘数越域 = warning 级(使用时 clamp——组合事故非配置事故,难度曲线与Boss GDD Edge Case 预登记)
|
||||
# 乘数越域 = warning 级(使用时 clamp 到 MULT_MIN/MULT_MAX——组合事故非配置事故,warning 预登记)
|
||||
for k in ["hp_mult", "atk_mult"]:
|
||||
var v: float = float(row.get(k, 1.0))
|
||||
if v < MULT_MIN or v > MULT_MAX:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
## VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
|
||||
## 专长(奇物·Feat)定义表 schema:逐 feat_id 行订阅式效果声明的类型化读取(空表合法态;垂直切片验证:订阅行经效果引擎统一解释执行)。
|
||||
## 🔴 层级纪律:专长只走局内随机渠道(Boss 三选一/凭证开启)——本表行永不入同调位表/局外树。
|
||||
## triggers 订阅行四词表封闭校验单点在 EffectEngine.validate_trigger_row——本表不自建第二套校验。
|
||||
@@ -36,8 +37,7 @@ func validate() -> Array:
|
||||
errs.append({"category": "domain", "message": "%s display_name 空" % rid})
|
||||
if not RARITIES.has(String(row.get("rarity", ""))):
|
||||
errs.append({"category": "enum", "message": "%s rarity 出域 %s" % [rid, str(RARITIES)]})
|
||||
if row.has("cap") and int(row.get("cap", 1)) < 1:
|
||||
errs.append({"category": "domain", "message": "%s cap 出域 [1,∞): %d" % [rid, int(row.get("cap", 1))]})
|
||||
errs.append_array(_validate_cap(row, rid))
|
||||
if not row.has("triggers"):
|
||||
errs.append({"category": "domain", "message": "%s 缺 triggers(订阅形态唯一合法行形)" % rid})
|
||||
else:
|
||||
@@ -48,6 +48,16 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
## cap 域(必填零兜底——缺行拒、越域 [1,99] 拒;域=config_db_cross_validator 在册口径)
|
||||
func _validate_cap(row: Dictionary, rid: String) -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if not row.has("cap"):
|
||||
errs.append({"category": "domain", "message": "%s 缺 cap(必填——零兜底)" % rid})
|
||||
elif int(row.get("cap", 0)) < 1 or int(row.get("cap", 0)) > 99:
|
||||
errs.append({"category": "domain", "message": "%s cap 越域 [1,99]: %d" % [rid, int(row.get("cap", 0))]})
|
||||
return errs
|
||||
|
||||
|
||||
func row_by_id(feat_id: String) -> Dictionary:
|
||||
for row in rows:
|
||||
if String(row.get("feat_id", "")) == feat_id:
|
||||
|
||||
@@ -7,7 +7,8 @@ extends Resource
|
||||
|
||||
const RATIO_KEYS := ["battle_ratio", "reward_ratio", "rest_ratio", "shop_ratio"]
|
||||
const ROW_KEYS := ["level_index", "layers", "branch_density", "entry_count", "exit_count",
|
||||
"battle_floor", "fork_ways_min", "fork_ways_max"] + RATIO_KEYS
|
||||
"battle_floor", "fork_ways_min", "fork_ways_max",
|
||||
"fork_third_way_pct", "fill_rest_first_pct", "width_max"] + RATIO_KEYS
|
||||
|
||||
@export var rows: Array[Dictionary] = [] # 每行 = ROW_KEYS 键集(level_index 唯一正整数)
|
||||
|
||||
@@ -65,6 +66,16 @@ func _validate_row_shape(row: Dictionary, L: int, key: String, layers_by_level:
|
||||
var bd: float = float(row.get("branch_density", -1.0))
|
||||
if bd < 0.0 or bd > 1.0:
|
||||
errs.append({"category": "domain", "message": "%s 分支密度 %f 越域 [0,1]" % [key, bd]})
|
||||
# 域并单点(关卡节点图生成.md v1.4 调参表——生成器侧域校验退役,唯此一处)
|
||||
var ftw: float = float(row.get("fork_third_way_pct", -1.0))
|
||||
if ftw < 0.0 or ftw > 100.0:
|
||||
errs.append({"category": "domain", "message": "%s fork_third_way_pct %f 越域 [0,100]" % [key, ftw]})
|
||||
var frf: float = float(row.get("fill_rest_first_pct", -1.0))
|
||||
if frf < 0.0 or frf > 100.0:
|
||||
errs.append({"category": "domain", "message": "%s fill_rest_first_pct %f 越域 [0,100]" % [key, frf]})
|
||||
var wmax: int = int(row.get("width_max", 0))
|
||||
if wmax < 2 or wmax > 6:
|
||||
errs.append({"category": "domain", "message": "%s width_max %d 越域 [2,6]" % [key, wmax]})
|
||||
var ratio_sum := 0.0
|
||||
for rk in RATIO_KEYS:
|
||||
var v: float = float(row.get(rk, -1.0))
|
||||
|
||||
@@ -23,6 +23,16 @@ const SPINE_ROW_GATE := 4
|
||||
## 无 fg_mastery 克制专精组——类型表达归锻造工坊)
|
||||
const FORK_GROUPS := ["fg_posture", "fg_sense", "fg_economy"]
|
||||
|
||||
## 规则键参数域表(键集与取值域逐字=天赋树.md v0.12 规则键参数表——文档定域、schema 执行)
|
||||
const RULE_KEY_PARAMS := {
|
||||
"posture_first_strike": {"ap_bonus": [1, 4]},
|
||||
"posture_bulwark": {"shield": [1, 9]},
|
||||
"sense_sharp": {"halve_pct": [1, 99], "cap_per_level": [1, 9]},
|
||||
"sense_guard": {"ap_refund": [1, 4], "cap_per_level": [1, 9]},
|
||||
"economy_hoard": {"mult_pct": [101, 300]},
|
||||
"economy_spend": {"discount_pct": [1, 99]},
|
||||
}
|
||||
|
||||
|
||||
func validate() -> Array:
|
||||
var errs: Array[Variant] = []
|
||||
@@ -42,6 +52,7 @@ func validate() -> Array:
|
||||
errs.append_array(_validate_row_head(row, nid))
|
||||
errs.append_array(_validate_row_numbers(row, nid))
|
||||
errs.append_array(_validate_row_effects(row, nid))
|
||||
errs.append_array(_validate_rule_key_params(row, nid))
|
||||
errs.append_array(_validate_row_fork(row, nid, fork_seen))
|
||||
errs.append_array(_validate_requires_dangling(rows, seen))
|
||||
errs.append_array(_validate_requires_level(rows, seen))
|
||||
@@ -101,6 +112,43 @@ func _validate_row_effects(row: Dictionary, nid: String) -> Array[Variant]:
|
||||
return errs
|
||||
|
||||
|
||||
## 规则键参数域校验:effects/effects_a/effects_b 中凡 RULE_KEYS 成员的值必须是 Dictionary、
|
||||
## 键集恰等 RULE_KEY_PARAMS[键] 的键集(缺参数/多参数即拒)、每值 int 且在闭域内(零缺省——缺字段=加载期报错)
|
||||
func _validate_rule_key_params(row: Dictionary, nid: String) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
for fx_key in ["effects", "effects_a", "effects_b"]:
|
||||
var fx: Dictionary[Variant, Variant] = row.get(fx_key, {}) as Dictionary
|
||||
for rk in fx.keys():
|
||||
if not RULE_KEYS.has(String(rk)):
|
||||
continue
|
||||
errs.append_array(_validate_one_rule_key_params(nid, fx_key, String(rk), fx[rk]))
|
||||
return errs
|
||||
|
||||
|
||||
## 单规则键参数字典校验(值须为 Dictionary、键集恰等参数域表、每值 int 且在闭域内)
|
||||
func _validate_one_rule_key_params(nid: String, fx_key: String, rk: String, params_v: Variant) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
if not (params_v is Dictionary):
|
||||
errs.append({"category": "domain", "message": "%s %s.%s 缺参数(规则键值须为参数字典——零缺省)" % [nid, fx_key, rk]})
|
||||
return errs
|
||||
var params: Dictionary[Variant, Variant] = params_v
|
||||
var expect: Dictionary[Variant, Variant] = RULE_KEY_PARAMS[rk]
|
||||
for pk in params.keys():
|
||||
if not expect.has(String(pk)):
|
||||
errs.append({"category": "domain", "message": "%s %s.%s 多参数: %s(参数域表封闭)" % [nid, fx_key, rk, String(pk)]})
|
||||
elif not (params[pk] is int):
|
||||
errs.append({"category": "domain", "message": "%s %s.%s.%s 越域(须 int)" % [nid, fx_key, rk, String(pk)]})
|
||||
else:
|
||||
var dom: Array[Variant] = expect[String(pk)]
|
||||
var v := int(params[pk])
|
||||
if v < int(dom[0]) or v > int(dom[1]):
|
||||
errs.append({"category": "domain", "message": "%s %s.%s.%s 越域 [%d,%d]: %d" % [nid, fx_key, rk, String(pk), int(dom[0]), int(dom[1]), v]})
|
||||
for pk in expect.keys():
|
||||
if not params.has(String(pk)):
|
||||
errs.append({"category": "domain", "message": "%s %s.%s 缺参数: %s(参数域表封闭)" % [nid, fx_key, rk, String(pk)]})
|
||||
return errs
|
||||
|
||||
|
||||
## Fork 结构域:一节点双态结构判据(fork_group 词表/组唯一/主 effects 须空/a/b 双键规则面)
|
||||
func _validate_row_fork(row: Dictionary, nid: String, fork_seen: Dictionary) -> Array[Variant]:
|
||||
var errs: Array[Variant] = []
|
||||
|
||||
@@ -13,7 +13,7 @@ extends Resource
|
||||
@export var price_a: float = 0.0 # 进店已购次数爬价因子(provisional——0 = 未投值即致命域)
|
||||
@export var price_b: float = 0.0 # 跨关递增因子(provisional)
|
||||
@export var relic_base_price: float = 15.0 # relic 保底卡基价(provisional——池令牌非货架种,词表外专枝)
|
||||
@export var spend_discount: float = 0.8 # 财富之道·散 economy_spend 激活时的购买折扣(provisional)
|
||||
@export var shelf_size: int = 0 # 货架位数(零哨兵——缺字段越域 [1,6] 即加载致命)
|
||||
|
||||
const GP_TIERS := ["normal", "elite", "boss"]
|
||||
const PRODUCT_ATTRS := ["atk", "max_hp", "armor_class"]
|
||||
@@ -131,8 +131,8 @@ func _validate_price_factors(errs: Array[Dictionary]) -> void:
|
||||
errs.append({"category": "domain", "message": "price_b 越域 [1.0,2.0]: %f" % price_b})
|
||||
if relic_base_price < 1.0 or relic_base_price > 99.0:
|
||||
errs.append({"category": "domain", "message": "relic_base_price 越域 [1.0,99.0]: %f" % relic_base_price})
|
||||
if spend_discount <= 0.0 or spend_discount > 1.0:
|
||||
errs.append({"category": "domain", "message": "spend_discount 越域 (0.0,1.0]: %f" % spend_discount})
|
||||
if shelf_size < 1 or shelf_size > 6:
|
||||
errs.append({"category": "domain", "message": "shelf_size 越域 [1,6]: %d" % shelf_size})
|
||||
|
||||
|
||||
# ---- 行族访问器(自 ConfigDB 归位——行集解析单点;消费面 db.run.shop.<m> 直连)----
|
||||
|
||||
@@ -9,9 +9,10 @@ extends Resource
|
||||
# 布置参数(表头字段;数量公式 = round(候选池格数 × percent_of_cells / 100) + (关卡序号 − 1) × per_level_bonus,
|
||||
# 连通片生长、单片 ≤ pack_max——图规模百分比 × 关卡递增,全连通、单片不超 4;
|
||||
# 百分比基数 = 候选池(安全格 − 初始展开区,即实际可挖格)而非总格数)
|
||||
@export var percent_of_cells: int = 5
|
||||
@export var per_level_bonus: int = 2
|
||||
@export var pack_max: int = 4
|
||||
@export var percent_of_cells: int = 0
|
||||
@export var per_level_bonus: int = 0
|
||||
@export var pack_max: int = 0
|
||||
@export var pack_min: int = 0 # 成包下界(零哨兵——缺字段越域即加载致命)
|
||||
|
||||
const SCOPES := ["region", "global"]
|
||||
# resource_face = 资源面偏斜(全局作用域变异——整图修摇号分布:生存族按 effect_value%
|
||||
@@ -24,6 +25,7 @@ const PLACEMENTS := ["closed_only", "any"] # 布置约束
|
||||
func validate() -> Array:
|
||||
# 空清单是合法状态(地形变异 P0)——零行即零变体格,配置加载零错误、零警告
|
||||
var errs: Array[Dictionary] = []
|
||||
errs.append_array(_validate_header_domains())
|
||||
var seen := {}
|
||||
for row in rows:
|
||||
var head := _validate_row(row, seen)
|
||||
@@ -36,6 +38,20 @@ func validate() -> Array:
|
||||
return errs
|
||||
|
||||
|
||||
## 表头域校验单点(地形变异.md v1.2 调参表 owner 域——零缺省,缺字段/越域即拒)
|
||||
func _validate_header_domains() -> Array[Dictionary]:
|
||||
var errs: Array[Dictionary] = []
|
||||
if percent_of_cells < 0 or percent_of_cells > 50:
|
||||
errs.append({"category": "domain", "message": "percent_of_cells 越域 [0,50]: %d" % percent_of_cells})
|
||||
if per_level_bonus < 0 or per_level_bonus > 9:
|
||||
errs.append({"category": "domain", "message": "per_level_bonus 越域 [0,9]: %d" % per_level_bonus})
|
||||
if pack_min < 1 or pack_min > pack_max:
|
||||
errs.append({"category": "domain", "message": "pack_min 越域 [1,pack_max=%d]: %d" % [pack_max, pack_min]})
|
||||
if pack_max < pack_min or pack_max > 9:
|
||||
errs.append({"category": "domain", "message": "pack_max 越域 [pack_min=%d,9]: %d" % [pack_min, pack_max]})
|
||||
return errs
|
||||
|
||||
|
||||
## 行域:variant_id 非空与查重/display_name/unlock_level/四词表(scope/effect/trigger/placement)
|
||||
func _validate_row(row: Dictionary, seen: Dictionary) -> Dictionary:
|
||||
var errs: Array[Dictionary] = []
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
spawn_edge = 3
|
||||
rows = [{
|
||||
"level_index": 1,
|
||||
"node_depth": 0,
|
||||
@@ -411,7 +412,7 @@ rows = [{
|
||||
|
||||
; 切片调参登记(SLICE-TUNED provisional,调参所有权仍归 C3):
|
||||
; A1 GDD 参考行(d0.10/0.12)经"先布后删"环内移除后仅幸存 2-3 雷、R0/|S|约0.97、残余挖掘 2-4 格——
|
||||
; 掘近退化板(A2 反例 B 机制的低密度放大)。实验数据(8 种子均值,smoke/experiment_density.gd):
|
||||
; 掘近退化板(A2 反例 B 机制的低密度放大)。实验数据(8 种子均值,出处=退役件 experiment_density.gd(git 历史可考)):
|
||||
; d0.25 → L1 幸存雷 11/残余 13/inf 0.925/att 2.9 · L2 雷 19/残余 21/inf 0.908/att 7.9(seed42 实测 inf 0.889→target 定 0.88 首过),零降级告警。
|
||||
; target 收窄至 0.88/0.88(不低于 0.85 绝对锚)为密度上移的对价——B1 条款③⑤对账重跑义务随调参触发。
|
||||
; 2026-09-11 关卡扩展批:L1 增 D1–D4 行(Boss=Dmax=4)。
|
||||
|
||||
@@ -20,6 +20,7 @@ rows = [
|
||||
"feat_id": "twin_pick",
|
||||
"display_name": "双管镐",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "每第 2 次付费开格返 1 AP",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -52,6 +53,7 @@ rows = [
|
||||
"feat_id": "geologist",
|
||||
"display_name": "地质学家",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "每首开一种新类型资源格集 1 点,满 4 种→揭示 1 格",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -106,6 +108,7 @@ rows = [
|
||||
"feat_id": "hard_hat",
|
||||
"display_name": "安全帽",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "开安全格攒 1 层;触陷阱耗层,每层减 1 伤害(一次清零)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -145,6 +148,7 @@ rows = [
|
||||
"feat_id": "skip_mining",
|
||||
"display_name": "跳格采掘",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "挖开空白格攒 1 点,3 点→揭示 1 格",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -199,6 +203,7 @@ rows = [
|
||||
"feat_id": "blast_accounting",
|
||||
"display_name": "爆破会计",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "轮末:本轮开格花费 AP≥5 → +1 GP",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -231,6 +236,7 @@ rows = [
|
||||
"feat_id": "frontier_miner",
|
||||
"display_name": "边陲矿工",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "挖开棋盘边缘格:产出 +1",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -253,6 +259,7 @@ rows = [
|
||||
"feat_id": "seal_echo",
|
||||
"display_name": "封印回响",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "封印命中计数;轮末计数≥2 → 返 1 AP(下轮)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -301,6 +308,7 @@ rows = [
|
||||
"feat_id": "warning_beacon",
|
||||
"display_name": "示警烟火",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "封印命中:揭示周围 3×3 的资源类型(不揭安危)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -316,6 +324,7 @@ rows = [
|
||||
"feat_id": "seal_smith",
|
||||
"display_name": "封印匠",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "封印命中每第 3 次 → 该危险格中立移除",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -348,6 +357,7 @@ rows = [
|
||||
"feat_id": "seal_stake",
|
||||
"display_name": "封印桩",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "封印命中:本轮 AC +1",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -364,6 +374,7 @@ rows = [
|
||||
"feat_id": "pain_memory",
|
||||
"display_name": "疼痛记忆",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "触陷阱攒 1 层;下次攻击附加层值伤害,用后清零",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -411,6 +422,7 @@ rows = [
|
||||
"feat_id": "fuse_wire",
|
||||
"display_name": "保险丝",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "单次受伤 ≥5 攒 1 点,2 点→护盾 1 层",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -465,6 +477,7 @@ rows = [
|
||||
"feat_id": "blood_insight",
|
||||
"display_name": "见血开窍",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "触陷阱:揭示 1 格",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -480,6 +493,7 @@ rows = [
|
||||
"feat_id": "trap_instinct",
|
||||
"display_name": "陷阱本能",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "触陷阱:本轮攻击 +2(走攻击面板乘区)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -503,6 +517,7 @@ rows = [
|
||||
"feat_id": "chain_traps",
|
||||
"display_name": "连环陷阱",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "同轮第 2 次及以后的触陷阱:每次额外 +1 层(疼痛记忆同账)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -535,6 +550,7 @@ rows = [
|
||||
"feat_id": "trap_burst",
|
||||
"display_name": "陷阱爆发",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "触陷阱:全体存活敌受陷阱伤×1.5 直伤",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -550,6 +566,7 @@ rows = [
|
||||
"feat_id": "blood_honey",
|
||||
"display_name": "血蜜",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "触陷阱:回复 2 生命(陷阱伤仍净亏血)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -565,6 +582,7 @@ rows = [
|
||||
"feat_id": "stone_skin",
|
||||
"display_name": "石肤",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "受伤:AC +1(本场累计,上限 3)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -582,6 +600,7 @@ rows = [
|
||||
"feat_id": "last_stand",
|
||||
"display_name": "背水",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "生命≤50%:受伤额外 −2",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -604,6 +623,7 @@ rows = [
|
||||
"feat_id": "breaker_strike",
|
||||
"display_name": "破封一击",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "挖开带封印的危险格:+1 层(疼痛记忆同账)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -627,6 +647,7 @@ rows = [
|
||||
"feat_id": "map_exchange",
|
||||
"display_name": "弃图兑换",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "停手:剩余 AP≥3 → 折为攻击伤害",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -682,6 +703,7 @@ rows = [
|
||||
"feat_id": "last_drop",
|
||||
"display_name": "一滴不剩",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "上轮 AP 耗尽 → +1 AP",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -705,6 +727,7 @@ rows = [
|
||||
"feat_id": "market_close",
|
||||
"display_name": "收盘",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "轮末:本轮开格 ≥8 → +1 GP",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -736,6 +759,7 @@ rows = [
|
||||
"feat_id": "execution_scholar",
|
||||
"display_name": "斩首学者",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "击杀:揭示 1 格",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -751,6 +775,7 @@ rows = [
|
||||
"feat_id": "executioner",
|
||||
"display_name": "处刑台",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"desc": "敌血量≤30%:主角对其伤害 +2",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -773,6 +798,7 @@ rows = [
|
||||
"feat_id": "field_dressing",
|
||||
"display_name": "战地包扎",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"desc": "战斗胜利:回复 3 生命",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -788,6 +814,7 @@ rows = [
|
||||
"feat_id": "blood_tax",
|
||||
"display_name": "血税",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "本局每触一次陷阱,最大生命 −2;每封印命中一次,攻击 +1(下一战起生效)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -812,6 +839,7 @@ rows = [
|
||||
"feat_id": "venom_edge",
|
||||
"display_name": "淬毒刃纹",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "克制命中追加 +2 伤害",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -834,6 +862,7 @@ rows = [
|
||||
"feat_id": "seal_burst",
|
||||
"display_name": "封印爆裂",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "封印命中:全体存活敌 3 点群伤",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -850,6 +879,7 @@ rows = [
|
||||
"feat_id": "perfect_insurance",
|
||||
"display_name": "完美保险",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"desc": "本场首次封印落空,不计入完美封印判定",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -891,6 +921,7 @@ rows = [
|
||||
"display_name": "省力周转",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"cap": 1,
|
||||
"desc": "刷新半价:弃图刷新成本为基线 50%",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -908,6 +939,7 @@ rows = [
|
||||
"display_name": "崩壁冲击",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"cap": 1,
|
||||
"desc": "刷后群伤:成功刷新后全体存活敌各受 2 点",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -925,6 +957,7 @@ rows = [
|
||||
"display_name": "矿道红利",
|
||||
"rarity": "rare",
|
||||
"cap": 1,
|
||||
"cap": 1,
|
||||
"desc": "刷后GP:每次成功刷新固定 +1 GP",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -941,6 +974,7 @@ rows = [
|
||||
"display_name": "石层缓冲",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"cap": 1,
|
||||
"desc": "当轮减伤:本轮成功刷新后,每次受伤固定 −2(多刷不叠加)",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -973,6 +1007,7 @@ rows = [
|
||||
"display_name": "蓄力周转",
|
||||
"rarity": "excellent",
|
||||
"cap": 1,
|
||||
"cap": 1,
|
||||
"desc": "刷后蓄力:每次刷新 +1 层(上限 3),下一次攻击消费全部层数",
|
||||
"triggers": [
|
||||
{
|
||||
@@ -1029,6 +1064,7 @@ rows = [
|
||||
"display_name": "坑道小憩",
|
||||
"rarity": "common",
|
||||
"cap": 1,
|
||||
"cap": 1,
|
||||
"desc": "当轮恢复:每次刷新累计 1 点(上限 3),本轮轮末合计回复",
|
||||
"triggers": [
|
||||
{
|
||||
|
||||
@@ -16,7 +16,10 @@ rows = [{
|
||||
"exit_count": 1,
|
||||
"battle_floor": 4,
|
||||
"fork_ways_min": 2,
|
||||
"fork_ways_max": 3
|
||||
"fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35,
|
||||
"fill_rest_first_pct": 50,
|
||||
"width_max": 4
|
||||
}, {
|
||||
"level_index": 2,
|
||||
"layers": 9,
|
||||
@@ -29,7 +32,10 @@ rows = [{
|
||||
"exit_count": 2,
|
||||
"battle_floor": 6,
|
||||
"fork_ways_min": 2,
|
||||
"fork_ways_max": 3
|
||||
"fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35,
|
||||
"fill_rest_first_pct": 50,
|
||||
"width_max": 4
|
||||
}, {
|
||||
"level_index": 3,
|
||||
"layers": 13,
|
||||
@@ -42,7 +48,10 @@ rows = [{
|
||||
"exit_count": 3,
|
||||
"battle_floor": 8,
|
||||
"fork_ways_min": 2,
|
||||
"fork_ways_max": 3
|
||||
"fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35,
|
||||
"fill_rest_first_pct": 50,
|
||||
"width_max": 4
|
||||
}]
|
||||
|
||||
; 节点图生成器核心批 卡① §8.1 参数档 provisional 取值表逐行填列(定值批落终值,数值全 provisional)。
|
||||
|
||||
@@ -113,10 +113,14 @@ rows = [{
|
||||
"name_a": "先手压制",
|
||||
"name_b": "稳健拓荒",
|
||||
"effects_a": {
|
||||
"posture_first_strike": 1
|
||||
"posture_first_strike": {
|
||||
"ap_bonus": 1
|
||||
}
|
||||
},
|
||||
"effects_b": {
|
||||
"posture_bulwark": 1
|
||||
"posture_bulwark": {
|
||||
"shield": 1
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"node_id": "fork_sense",
|
||||
@@ -131,10 +135,16 @@ rows = [{
|
||||
"name_a": "锋",
|
||||
"name_b": "守",
|
||||
"effects_a": {
|
||||
"sense_sharp": 1
|
||||
"sense_sharp": {
|
||||
"halve_pct": 50,
|
||||
"cap_per_level": 2
|
||||
}
|
||||
},
|
||||
"effects_b": {
|
||||
"sense_guard": 1
|
||||
"sense_guard": {
|
||||
"ap_refund": 1,
|
||||
"cap_per_level": 2
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"node_id": "fork_economy",
|
||||
@@ -149,10 +159,14 @@ rows = [{
|
||||
"name_a": "聚",
|
||||
"name_b": "散",
|
||||
"effects_a": {
|
||||
"economy_hoard": 1
|
||||
"economy_hoard": {
|
||||
"mult_pct": 150
|
||||
}
|
||||
},
|
||||
"effects_b": {
|
||||
"economy_spend": 1
|
||||
"economy_spend": {
|
||||
"discount_pct": 80
|
||||
}
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1")
|
||||
shelf_size = 3
|
||||
gp_drops = [{
|
||||
"tier": "normal",
|
||||
"display_name": "普通战斗",
|
||||
@@ -93,7 +94,6 @@ pool_shelf = [{
|
||||
price_a = 1.8
|
||||
price_b = 1.1
|
||||
relic_base_price = 15.0
|
||||
spend_discount = 0.8
|
||||
|
||||
; GP+商店房实施批(2026-09-20 用户拍板,设计权威 design/research/proposal-gp-shop-room-2026-09-20.md 裁决记录):
|
||||
; 掉落=均值±方差掷定(gp_drop 封闭流)+关级加值;精英行为预留行(当前无消费方,随精英节点内容批启用);
|
||||
|
||||
@@ -7,6 +7,7 @@ script = ExtResource("1")
|
||||
percent_of_cells = 5
|
||||
per_level_bonus = 0
|
||||
pack_max = 4
|
||||
pack_min = 2
|
||||
rows = [{
|
||||
"variant_id": "thorn",
|
||||
"display_name": "荆棘丛",
|
||||
|
||||
@@ -182,7 +182,7 @@ func _wire_adventure() -> void:
|
||||
|
||||
# 冒险开局参数面构造(主菜单开新/续玩两入口共用:同调三键随开局下传。
|
||||
# 取值口径与营地投影完全同源:装配集=save.attunement_equipped(save_manager.gd 访问器)、Y 刷新池=
|
||||
# meta.attune_refresh_charges(当前等级行表值)、树 rule 键集=meta.tree_rule_keys(Fork 激活侧 effects);
|
||||
# meta.attune_refresh_charges(当前等级行表值)、树规则参数字典=meta.tree_rules(Fork 激活侧 effects_*);
|
||||
# hero_override 沿用既有 meta 现算口径。static 形态=冒烟直呼单测本函数,免实例化整菜单场景)。
|
||||
# 两根类型轴随开局下传(attack_type/armor_type——契约 2 所有权移交+契约 8 冒险开始快照;
|
||||
# 取值=meta.hero_type_axes()=存档选中路线类型轴(attack/armor 两键),未解锁/异常选中态回退锻造配置默认值)。
|
||||
@@ -195,7 +195,7 @@ static func build_adventure_params(save_manager_ref: SaveManagerCore, meta_ref:
|
||||
"armor_type": meta_ref.hero_type_axes()["armor"] if meta_ref != null else "",
|
||||
"attunement": save_manager_ref.meta_state.attune_state().equipped if save_manager_ref != null else [],
|
||||
"attune_refresh_y": meta_ref.attune_refresh_charges() if meta_ref != null else 0,
|
||||
"tree_rules": meta_ref.tree_rule_keys() if meta_ref != null else [],
|
||||
"tree_rules": meta_ref.tree_rules() if meta_ref != null else {},
|
||||
}
|
||||
params.merge(extra, true)
|
||||
return params
|
||||
@@ -630,10 +630,10 @@ func _on_refresh_confirmed() -> void:
|
||||
var result := "rejected_rap" if rap < cost else "rejected_cap"
|
||||
logger.log_ux("ux_refresh_attempt", {"result": result, "round_refresh_index": _refresh_used_snapshot() + 1})
|
||||
if result == "rejected_rap":
|
||||
ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 8)" % (cost - rap), "warn")
|
||||
ui.battle.board_zone.toast.show_toast("RAP 不足 · 还差 %d 点(弃图刷新需 %d)" % [cost - rap, cost], "warn")
|
||||
ui.battle.board_zone.ap_bar.pulse_rap()
|
||||
else:
|
||||
ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 2 次)", "warn")
|
||||
ui.battle.board_zone.toast.show_toast("本轮刷新次数已用完(每轮 %d 次)" % int(db.run.action_economy.refresh_cap_per_round), "warn")
|
||||
ui.battle.board_zone.board_view.refresh()
|
||||
_refresh_presentations()
|
||||
|
||||
|
||||
@@ -13,12 +13,15 @@ var _seed := 0
|
||||
var _recent_steps: Array[Dictionary] = [] # 环形缓冲:最近结算行
|
||||
var _recent_actions: Array[Dictionary] = [] # 环形缓冲:最近玩家动作
|
||||
var _snap_count := 0
|
||||
var ap_per_round := 8 # 组合根注入(AP 消耗口径分母——行动经济 无推送信号)
|
||||
var ap_per_round: int = 0 # 必填注入(零缺省——组合根 start_session 前显式设定,守卫拒未注入)
|
||||
var _ux_inputs := 0 # 本结算轮原始输入动作数(log_action 计数,快照后归零)
|
||||
|
||||
|
||||
# 开启会话(每次 start_level/start_battle 调用——按种子分文件)。返回日志路径。
|
||||
func start_session(seed_val: int) -> String:
|
||||
if ap_per_round < 1 or ap_per_round > 32:
|
||||
push_error("[试玩日志器] ap_per_round 未注入或越域: %d——组合根必填注入" % ap_per_round)
|
||||
return ""
|
||||
_seed = seed_val
|
||||
_recent_steps.clear()
|
||||
_recent_actions.clear()
|
||||
|
||||
@@ -77,7 +77,7 @@ func name_line_probe() -> Dictionary:
|
||||
# ---- setter 面(presenter/组合根零改排) ----
|
||||
|
||||
# data = 战斗结算 presentation_snapshot enemies[] 投影:{order, display_name, hp, max_hp, enemy_id,
|
||||
# pending_skill/pending_type/pending_name, effects[](敌方 side 增益——各立绘同值呈现)}
|
||||
# pending_skill/pending_type/pending_name, effects[](该敌自己名下增益——各立绘各读各的,增益状态 v1.5 个体投影)}
|
||||
func set_data(d: Dictionary) -> void:
|
||||
st._display_name = String(d.get("display_name", "敌人"))
|
||||
st._enemy_id = String(d.get("enemy_id", ""))
|
||||
|
||||
@@ -47,6 +47,11 @@ func _initialize() -> void:
|
||||
var feat_mode := "off"
|
||||
if args.size() >= 7 and args[6] in ["off", "formed", "ab_a", "ab_b", "rules"]:
|
||||
feat_mode = args[6]
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
var feat_profile := {}
|
||||
if feat_mode == "formed":
|
||||
# 成型画像=五事件族覆盖 14 件
|
||||
@@ -58,16 +63,11 @@ func _initialize() -> void:
|
||||
"last_drop", "market_close",
|
||||
"execution_scholar"]}
|
||||
elif feat_mode == "ab_a":
|
||||
feat_profile = {"mode": "off", "tree_rules": ["posture_first_strike"]}
|
||||
feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, ["posture_first_strike"])}
|
||||
elif feat_mode == "ab_b":
|
||||
feat_profile = {"mode": "off", "tree_rules": ["posture_bulwark"]}
|
||||
feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, ["posture_bulwark"])}
|
||||
elif feat_mode == "rules":
|
||||
feat_profile = {"mode": "off", "tree_rules": Array(String(args[7] if args.size() >= 8 else "").split(",", false))} # V19——任意键集配对注入(Array 包裹防 PackedStringArray 被 adventure_runner is Array 守卫整组丢弃)
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
if not db.load_tables("res://data/tables", false):
|
||||
print("表加载失败")
|
||||
quit(1)
|
||||
return
|
||||
feat_profile = {"mode": "off", "tree_rules": _tree_rule_dict(db, Array(String(args[7] if args.size() >= 8 else "").split(",", false)))} # V19——任意键集配对注入(规则键参数字典形态)
|
||||
var attune_profile := {}
|
||||
if attune_mode == "on":
|
||||
# 贪心画像:Slots=3(槽位判定源=树同调位节点点亮数,模拟画像=三节点
|
||||
@@ -153,3 +153,21 @@ func _initialize() -> void:
|
||||
print("BATCH %s N=%d 终局计数=%s 日志=%s" % [policy, n, JSON.stringify(tally), log_path])
|
||||
print("(口径注:零加成 = atk/hp 天赋格百分比均 0;关间大奖励(HP 上限提升+回满)按冒险结构设计自动兑付,未额外剥离 · 全程日志见 user://logs/ · 总耗时 %.1f 秒)" % [float(Time.get_ticks_msec() - t0) / 1000.0])
|
||||
quit(0)
|
||||
|
||||
|
||||
## 树规则参数字典构造(A/B 面注入单点——扫 meta_tree Fork 行 effects_a/b,命中 keys 集的键取其
|
||||
## 参数字典原样透传;db 参鸭子型沿 board_generator 先例——sim 面不在 S9 扫描面)
|
||||
static func _tree_rule_dict(db, keys: Array[Variant]) -> Dictionary[Variant, Variant]:
|
||||
var out: Dictionary[Variant, Variant] = {}
|
||||
var keyset := {}
|
||||
for k in keys:
|
||||
keyset[String(k)] = true
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
for skey in ["effects_a", "effects_b"]:
|
||||
var fx: Dictionary = row.get(skey, {}) as Dictionary
|
||||
for rk in fx.keys():
|
||||
if keyset.has(String(rk)) and not out.has(String(rk)):
|
||||
out[String(rk)] = fx[rk]
|
||||
return out
|
||||
@@ -146,6 +146,16 @@ static func _deduce(minesweeper) -> Dictionary:
|
||||
if not mines.has(c):
|
||||
mines[c] = true
|
||||
changed = true
|
||||
# 0 格连锁算子(架构整改批补齐——与棋盘生成 _apply_chain_rule 对位):safe 集中未揭示格
|
||||
# 挖开必触发 0 格连锁(连锁只经安全格扩散=闭包集全安全,真实玩家挖 c 免费获得的信息面),
|
||||
# 经 preview_reveal 真实闭包取新开格集并入 safe——双口径显式登记:unknown 不含 safe_set oracle
|
||||
# 〔口径差异=board_generator 版含 oracle,本版 no-peek——:80-83 注释在账〕
|
||||
for c in (safe.keys() as Array).duplicate():
|
||||
if board.is_safe(c) and not minesweeper.is_revealed(c):
|
||||
for rc in minesweeper.preview_reveal(c):
|
||||
if not safe.has(rc):
|
||||
safe[rc] = true
|
||||
changed = true
|
||||
return {"safe": _row_major(safe), "mines": _row_major(mines)}
|
||||
|
||||
|
||||
|
||||
+69
-11
@@ -11,26 +11,84 @@ const AutoPlayer := preload("res://sim/auto_player.gd")
|
||||
const GUARD_ACTIONS := 400 # 单轮动作步防挂死上限(被收口探针原值逐一同值)
|
||||
|
||||
|
||||
# 驱动一整轮挖掘。still_running = 调用方战斗活性谓词(缺省恒真):
|
||||
# 驱动一整轮(驱动循环单点——通用签名)。still_running = 调用方战斗活性谓词(缺省恒真):
|
||||
# monte_carlo / skill_rhythm 传 `func(): return out.result == ""`(battle_finished 已发的轮内不再动手),
|
||||
# damage_probe 关卡驱动传 runner 绑定 + battle_result 谓词,battle 级探针传空即可。
|
||||
static func drive_round(settlement, still_running: Callable = Callable()) -> void:
|
||||
# act_provider 契约(策略回调——func(settlement) -> Dictionary):
|
||||
# {"kind":"stop"}/空字典 = 停手出环;单步 {"kind":"move"|"dig"|"seal"|"refresh","cell":V}(refresh 无 cell);
|
||||
# 合步 {"kind":"move","cell":X,"next":{"kind":"dig","cell":Y}}——move 提交后查相位/轮次、变更即断弃 next。
|
||||
# on_action 契约(日志回调——func(submit_kind:String, cell:Vector2i, ok:bool, round_index:int)):
|
||||
# submit_kind ∈ {move, dig, seal_place, refresh, stop}——轮末统一 stop 也经此回调(accept 取 outcomes[0].accepted);
|
||||
# 空 Callable = 不记日志。
|
||||
# max_rejects:策略回调被拒连续上限(≤0 = 不设防——缺省形态零防线语义保持);ok 清零否则累加。
|
||||
# 缺省两参形态(act_provider 空)= AutoPlayer.choose_dig 原语义逐位:guard 400、异格先移、合步 dig、
|
||||
# 移位后相位/轮次离场断弃 next、轮末 stop——四调用方(dump_battle/monte_carlo/damage_probe/skill_rhythm)两参零改动。
|
||||
static func drive_round(settlement, still_running: Callable = Callable(),
|
||||
act_provider: Callable = Callable(), on_action: Callable = Callable(),
|
||||
max_rejects: int = 0) -> void:
|
||||
var start_round: int = settlement.round_index
|
||||
var minesweeper = settlement.minesweeper
|
||||
var guard := GUARD_ACTIONS
|
||||
var rejects := 0
|
||||
var using_provider: bool = not act_provider.is_null()
|
||||
while _alive(still_running) and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and guard > 0:
|
||||
and settlement.round_index == start_round and guard > 0 \
|
||||
and (max_rejects <= 0 or rejects < max_rejects):
|
||||
guard -= 1
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
var act: Dictionary = act_provider.call(settlement) if using_provider else _default_act(minesweeper)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop" or act.is_empty():
|
||||
break
|
||||
if plan.hero_cell != minesweeper.hero_pos():
|
||||
minesweeper.settle_batch([{"action": "move", "cell": plan.hero_cell}])
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != start_round:
|
||||
break
|
||||
minesweeper.settle_batch([{"action": "dig", "cell": plan.dig_cell}])
|
||||
if _submit_act(settlement, act, on_action, start_round):
|
||||
rejects = 0
|
||||
else:
|
||||
rejects += 1
|
||||
if _alive(still_running) and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
minesweeper.settle_batch([{"action": "stop"}])
|
||||
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
_on(on_action, "stop", Vector2i(-1, -1), _accepted(out_s), start_round)
|
||||
|
||||
|
||||
# 缺省策略(AutoPlayer.choose_dig 原生 plan 专用):同位直接 dig、异位先移合步 next=dig
|
||||
static func _default_act(minesweeper) -> Dictionary:
|
||||
var plan := AutoPlayer.choose_dig(minesweeper)
|
||||
if plan.is_empty():
|
||||
return {"kind": "stop"}
|
||||
if plan.hero_cell == minesweeper.hero_pos():
|
||||
return {"kind": "dig", "cell": plan.dig_cell}
|
||||
return {"kind": "move", "cell": plan.hero_cell, "next": {"kind": "dig", "cell": plan.dig_cell}}
|
||||
|
||||
|
||||
# 单步提交分派(move/dig/seal→settle_batch——seal 提交动作名 seal_place;refresh→player_request_refresh;
|
||||
# 合步 move→next 递归提交——相位/轮次离场断弃,与原内联逐位同)
|
||||
static func _submit_act(settlement, act: Dictionary, on_action: Callable, ri: int) -> bool:
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
var cell: Vector2i = act.get("cell", Vector2i(-1, -1))
|
||||
var ok := false
|
||||
if kind == "refresh":
|
||||
ok = bool(settlement.player_request_refresh())
|
||||
_on(on_action, "refresh", Vector2i(-1, -1), ok, ri)
|
||||
return ok
|
||||
var action_name := "seal_place" if kind == "seal" else kind
|
||||
var out_s: Array = settlement.minesweeper.settle_batch([{"action": action_name, "cell": cell}])
|
||||
ok = _accepted(out_s)
|
||||
_on(on_action, action_name, cell, ok, ri)
|
||||
if kind == "move":
|
||||
var next: Dictionary = act.get("next", {})
|
||||
if next.is_empty():
|
||||
return ok
|
||||
if settlement.action_economy.phase != "Active" or settlement.round_index != ri:
|
||||
return ok # 移位后相位/轮次离场——断弃 next
|
||||
_submit_act(settlement, next, on_action, ri)
|
||||
return ok
|
||||
|
||||
|
||||
static func _on(on_action: Callable, kind: String, cell: Vector2i, ok: bool, ri: int) -> void:
|
||||
if not on_action.is_null():
|
||||
on_action.call(kind, cell, ok, ri)
|
||||
|
||||
|
||||
static func _accepted(outcomes: Array) -> bool:
|
||||
return not outcomes.is_empty() and bool(outcomes[0].accepted)
|
||||
|
||||
|
||||
static func _alive(still_running: Callable) -> bool:
|
||||
|
||||
@@ -11,6 +11,7 @@ extends SceneTree
|
||||
class FakeDB:
|
||||
class _FakeBoardGen:
|
||||
var row: Dictionary
|
||||
var spawn_edge: int = 0 # 表头成员(generate 直索引面——真值由 _initialize 逐行注入)
|
||||
func resolve_row(_L: int, _D: int) -> Dictionary:
|
||||
return row
|
||||
class _FakeCombat:
|
||||
@@ -42,9 +43,11 @@ func _initialize() -> void:
|
||||
# 初始展开收窄两参数透传(.get 缺省与生成器防御口径一致)
|
||||
"shave_ring_radius": int(row.get("shave_ring_radius", 1)),
|
||||
"r0_max_cap": int(row.get("r0_max_cap", 0)),
|
||||
"spawn_edge": int(db.combat.board_gen.spawn_edge), # 出生区边长透传(读真表头、零字面量——组E FakeDB 模式不动)
|
||||
}
|
||||
var fdb := FakeDB.new()
|
||||
fdb.row = rd
|
||||
fdb.combat.board_gen.spawn_edge = int(db.combat.board_gen.spawn_edge) # 表头注入(零字面量——读真表头)
|
||||
var mines: Array = []
|
||||
var safes: Array = []
|
||||
var r0s: Array = []
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
# VERTICAL SLICE - NOT FOR PRODUCTION
|
||||
# 一次性实验:不同 mine_density 下的 R₀ 占比 / 幸存雷数 / 残余挖掘量 / 推理率——为切片临时调参提供数据。
|
||||
extends SceneTree
|
||||
|
||||
func _initialize() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
db.load_tables("res://data/tables", true)
|
||||
var board_generator = load("res://core/board_generator.gd").new()
|
||||
print("=== 密度实验(L1 10×10 target0.88 / L2 12×12 target0.90,各 8 种子)===")
|
||||
print("d\t|M|/N\tR₀\tR₀/|S|\t残余挖掘\tinf\tatt")
|
||||
for d in [0.22, 0.25, 0.28, 0.30, 0.33]:
|
||||
for cfg in [[10, 10, 0.88, 50], [12, 12, 0.90, 60]]:
|
||||
var w: int = cfg[0]
|
||||
var h: int = cfg[1]
|
||||
var t: float = cfg[2]
|
||||
var ms: int = cfg[3]
|
||||
var agg := {"m": 0, "r0": 0, "rem": 0, "inf": 0.0, "att": 0, "n": 0}
|
||||
for s in range(8):
|
||||
var row := {
|
||||
"level_index": 1, "node_depth": 0, "board_width": w, "board_height": h,
|
||||
"mine_density": d, "inferability_target": t, "solver_rules": "single_subset",
|
||||
"min_safe_count": ms, "max_gen_attempts": 16,
|
||||
"board_type": "dungeon",
|
||||
}
|
||||
_patch_row(row, d)
|
||||
var params := _params_from(row)
|
||||
var rg := RandomNumberGenerator.new()
|
||||
rg.seed = 1000 + s
|
||||
var b = board_generator.generate(1, 0, rg, _db(db, row))
|
||||
if b == null:
|
||||
continue
|
||||
agg.m += b.mines.size()
|
||||
agg.r0 += int(b.metrics.r0_count)
|
||||
agg.rem += b.safe_cells().size() - int(b.metrics.r0_count)
|
||||
agg.inf += float(b.metrics.inferability)
|
||||
agg.att += int(b.metrics.attempts_used)
|
||||
agg.n += 1
|
||||
if agg.n > 0:
|
||||
var n := float(agg.n)
|
||||
var n_placed: int = int(round(d * float(w * h - 9)))
|
||||
print("%.2f %dx%d\t%d/%d\t%d\t%.2f\t%d\t%.3f\t%.1f" % [d, w, h, int(agg.m / n + 0.5), n_placed, int(agg.r0 / n + 0.5), agg.r0 / n / (w * h - int(agg.m / n + 0.5)), int(agg.rem / n + 0.5), agg.inf / n, agg.att / n])
|
||||
quit(0)
|
||||
|
||||
# --- 简易 db 替身:只供给本实验的行 ---
|
||||
var _row_override: Dictionary = {}
|
||||
|
||||
func _patch_row(row: Dictionary, _d: float) -> void:
|
||||
_row_override = row
|
||||
|
||||
func _params_from(row: Dictionary) -> Dictionary:
|
||||
return {
|
||||
"L": 1, "D": 0, "w": int(row.board_width), "h": int(row.board_height),
|
||||
"density": float(row.mine_density), "target": float(row.inferability_target),
|
||||
"solver_rules": "single_subset", "min_safe": int(row.min_safe_count),
|
||||
"max_attempts": int(row.max_gen_attempts), "board_type": "dungeon",
|
||||
}
|
||||
|
||||
class FakeDB:
|
||||
class _FakeBoardGen:
|
||||
var row: Dictionary
|
||||
func resolve_row(_L: int, _D: int) -> Dictionary:
|
||||
return row
|
||||
class _FakeCombat:
|
||||
var board_gen := _FakeBoardGen.new()
|
||||
var combat := _FakeCombat.new()
|
||||
func status_effect_row(_id: String) -> Dictionary:
|
||||
return {}
|
||||
|
||||
func _db(_real, row: Dictionary):
|
||||
var f := FakeDB.new()
|
||||
f.combat.board_gen.row = row
|
||||
return f
|
||||
@@ -1 +0,0 @@
|
||||
uid://cg7jkrsc03rar
|
||||
+25
-104
@@ -20,9 +20,8 @@
|
||||
# 运行前提:db 已 load_tables;run_level/run_adventure 均须全新 RngService(同种子 ⇒ 全程逐步可回放)。
|
||||
# 实现注:信号处理体全部为静态方法 + bind 传状态——GDScript 多行 lambda 内嵌 match/控制流的解析不可靠。
|
||||
extends RefCounted
|
||||
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd") # 自动玩家策略单一实现
|
||||
|
||||
const BattleDriver := preload("res://sim/battle_driver.gd") # 庚案收敛:驱动循环单点(策略仍归 _decide_act 单点分派)
|
||||
# ── 普通档 v3 失误注入参数(踩雷期望=完美+2/板、每板少挖几个正确格)──
|
||||
# 标雷→误挖转化率:主踩雷源=卡死赌挖(不确定性概率命中),误挖只作偶发真手滑——
|
||||
# 真人几乎不挖自己封印的格(p 过高会让首轮已证封印变保底命中,非失误形态)
|
||||
@@ -32,14 +31,12 @@ const NORMAL_SKIP_DIG_P := 0.05
|
||||
# 空闲提前赌挖率:已证安全格在手时小概率先赌一格——完美系把不确定性攒到残局
|
||||
# (残局 hazard→1=扫雷),真人早赌滚信息(低 hazard 期吃掉不确定性);p×~40 安全 AP/板 ≈ 4 次
|
||||
const NORMAL_IDLE_GAMBLE_P := 0.10
|
||||
|
||||
|
||||
static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct: int, collect_rows: bool = false, logger = null, policy: String = "normal", route: String = "min") -> Dictionary:
|
||||
var own_session := logger == null
|
||||
if own_session:
|
||||
logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round) # 注入序前置(守卫拒后开——R5 必填)
|
||||
logger.start_session(seed_val)
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
|
||||
logger.log_run({"seed": seed_val, "level_index": level_index, "atk_pct": atk_pct, "hp_pct": hp_pct, "policy": policy, "route": route})
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(seed_val)
|
||||
@@ -77,8 +74,6 @@ static func run_level(db, seed_val: int, level_index: int, atk_pct: int, hp_pct:
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
return st
|
||||
|
||||
|
||||
# 单关驱动段(run_level 抽取——零自动推进停点 + 战斗自动玩家;policy 经 st 入 _autoplay_battle)
|
||||
# 入口停点(node_index 尚 -1 = 进节点前无 current_node)也经本分支驱动——
|
||||
# 读 next_candidate_ids 候选(入口单候选)→ pick_next_node,同一 _enter_node 等价(零 RNG 漂移);
|
||||
@@ -100,8 +95,6 @@ static func _drive_level(runner, st: Dictionary, logger, policy: String, route:
|
||||
if this_settlement == null:
|
||||
break # level_finished 已发(settlement 置 null)而信号尚未置 result 的防御
|
||||
_autoplay_battle(runner, this_settlement, st, logger)
|
||||
|
||||
|
||||
# 三关连探冒险驱动——AdventureRunner 编排:主种子恰一次
|
||||
# (start_adventure 唯一 new_adventure 调用点)、大奖励立即确认、冒险终局置 result/summary。
|
||||
# 返回结构 = run_level 同族 + summary / wallet_events / encounter_max_hp_seq / reward_events /
|
||||
@@ -116,8 +109,8 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
var own_session := logger == null
|
||||
if own_session:
|
||||
logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round) # 注入序前置(守卫拒后开——R5 必填)
|
||||
logger.start_session(seed_val)
|
||||
logger.ap_per_round = int(db.run.action_economy.ap_per_round)
|
||||
logger.log_run({"seed": seed_val, "adventure": true, "atk_pct": atk_pct, "hp_pct": hp_pct,
|
||||
"policy": policy, "route": route, "hero_override": hero_override})
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
@@ -137,7 +130,6 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
"seal_placed": 0, "seal_hit": 0, "seal_miss": 0, "seal_hit_gain": 0, "seal_miss_penalty": 0,
|
||||
"prng": _policy_rng(seed_val), "mine_slips": 0, "skip_digs": 0, "shop_buys": 0,
|
||||
"adv": true, # v3 档位行为开关(选路评分/失误注入/商店购买只挂冒险测量面——单关夹具语义不变)
|
||||
|
||||
"level_maps": [], # 逐关节点图标识(生成图 = 关卡序号+本局主种子——MC 分层出数)
|
||||
}
|
||||
# 信号直连(拆件批 T3):runner 信号在 level_started 重取点重连(转发族退役——重取点前移到本回调)
|
||||
@@ -158,9 +150,9 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
# 特性注入(formed 画像=开局注入七流派各 2 件档一成型集;测量面专用)
|
||||
if relic.has("seed_ids"):
|
||||
adv_params["relics_seed"] = relic.get("seed_ids", [])
|
||||
# 树规则键注入(A/B 面读数载体:fork 激活侧 effects rule 键集透传)
|
||||
# 树规则参数字典注入(A/B 面读数载体:fork 激活侧 effects_* 参数字典透传)
|
||||
if relic.has("tree_rules"):
|
||||
adv_params["tree_rules"] = relic.get("tree_rules", [])
|
||||
adv_params["tree_rules"] = relic.get("tree_rules", {}) if relic.get("tree_rules", {}) is Dictionary else {}
|
||||
# 商店分层注入(缺省 legacy 与既有调用方逐位一致零行为变更;pool=共享池货架面)
|
||||
var pool_shop: bool = String(shop.get("mode", "")) == "pool"
|
||||
if pool_shop:
|
||||
@@ -189,8 +181,6 @@ static func run_adventure(db, seed_val: int, atk_pct: int, hp_pct: int, collect_
|
||||
if own_session:
|
||||
logger.close_session()
|
||||
return st
|
||||
|
||||
|
||||
# 每关开始(autoplay 直连面):重取 runner 并重连三信号(恰好一次——同关守卫:
|
||||
# level_started 每关恰一次发射,bind Callable 每次新建故以关卡号守卫防重入)
|
||||
static func _on_autoplay_level_started(level_index: int, _display_name: String, adr, st: Dictionary, collect_rows: bool, logger) -> void:
|
||||
@@ -203,8 +193,6 @@ static func _on_autoplay_level_started(level_index: int, _display_name: String,
|
||||
r.step_row.connect(_on_step_row.bind(adr, st, collect_rows))
|
||||
r.node_entered.connect(_on_adventure_node_entered.bind(adr, st, logger))
|
||||
r.wallet_changed.connect(_on_adventure_wallet.bind(st, logger))
|
||||
|
||||
|
||||
# 冒险模式节点采集(_on_node_entered 的 AdventureRunner 门面版——runner 逐关更替,经 adr.runner() 重取;
|
||||
# encounter_max_hp_seq 抓 effective_max_hp:node_entered 先于 begin_battle,encounter 尚为空字典,
|
||||
# 该值 = begin_battle 将收到 hero_max_hp 同一表达式〔规划书抓取点与代码事实冲突,按代码为准已登记〕)
|
||||
@@ -215,7 +203,6 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
if not st.has("level_maps"):
|
||||
st["level_maps"] = []
|
||||
st["level_maps"].append("%d#%d" % [int(r.level_row.get("level_index", 0)), int(adr.master_seed)]) # MC 分层出数(关卡×生成图标识)
|
||||
|
||||
if t == "battle" or t == "boss":
|
||||
st.battles += 1
|
||||
st.last_battle_enemies = [] # 末场战斗敌方名序列按战斗重置(失败局终值 = 死亡遭遇)
|
||||
@@ -259,8 +246,6 @@ static func _on_adventure_node_entered(node: Dictionary, info: Dictionary, adr,
|
||||
r.continue_after_node()
|
||||
if logger != null:
|
||||
logger.log_node(node, info, r.node_index)
|
||||
|
||||
|
||||
# 冒险钱包事件采集(wallet 行族——GP 跨关延续与冒险收束 clear 的验收取数面;同日志合规转发)
|
||||
static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.wallet_events.append({"event": String(event), "gp": int(gp_val),
|
||||
@@ -268,8 +253,6 @@ static func _on_adventure_wallet(event: String, gp_val: int, extra: Dictionary,
|
||||
"source": String(extra.get("source", ""))})
|
||||
if logger != null:
|
||||
logger.log_wallet(event, gp_val, extra)
|
||||
|
||||
|
||||
# 三选一停点驱动(确定性策略;TP0 起 316-f 判读:1 级最稀有、同档并列取最左;
|
||||
# 奖励事件随 reward_events 留档)
|
||||
static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> void:
|
||||
@@ -283,8 +266,6 @@ static func _on_adventure_bonus(payload: Dictionary, st: Dictionary, adr) -> voi
|
||||
var pick: Dictionary = _pick_token(candidates, String(st.get("feat_mode", "off")))
|
||||
if not adr.stop().pick_curio(String(pick.get("kind", "")), String(pick.get("tier", "")), String(pick.get("feat_id", ""))):
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 三选一确定性选件(TP0 316-f——1 级优先度=最稀有〔四级:legendary>rare>excellent>common〕、2 级=最左;
|
||||
# 候选并列同档取最左=稳定序保证。空候选/未知档位 = 回退最左防御口径。
|
||||
# 成型画像(relic 模式 on:候选含 relic 令牌时优先在 relic 集内按 316-f 择一
|
||||
@@ -302,22 +283,11 @@ static func _pick_token(candidates: Array, feat_mode: String = "off") -> Diction
|
||||
var best_rank := -1
|
||||
for tok in pool:
|
||||
var t: Dictionary = tok as Dictionary
|
||||
var rank := 0
|
||||
match String(t.get("tier", "")):
|
||||
"legendary":
|
||||
rank = 4
|
||||
"rare":
|
||||
rank = 3
|
||||
"excellent":
|
||||
rank = 2
|
||||
"common":
|
||||
rank = 1
|
||||
var rank: int = int(TreasureItemTable.TIER_RANK.get(String(t.get("tier", "")), 0)) # 档位排序读表(未知档 0=原 match 落空同语义)
|
||||
if best.is_empty() or rank > best_rank:
|
||||
best = t
|
||||
best_rank = rank
|
||||
return best
|
||||
|
||||
|
||||
# 三选一兑付完成 → 上下文分派(关卡完成停点=推进下一关 / 奖励节点=回图选路)
|
||||
static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr) -> void:
|
||||
st.reward_events.append({"curio_resolved": result.duplicate(true)})
|
||||
@@ -328,8 +298,6 @@ static func _on_adventure_curio_resolved(result: Dictionary, st: Dictionary, adr
|
||||
return
|
||||
if not adr.continue_to_next_level():
|
||||
st.result = "advance_failed"
|
||||
|
||||
|
||||
# 商店贪心买齐:GP 局内死钱——买得起就买( affordable 集里挑最便宜的
|
||||
# 反复买到达不上任何商品为止,guard 防死循环)。确定性(零随机流);购买出账/加算区入账全走
|
||||
# runner.shop.buy_product 核心编排(与呈现层购买同一出账路径,wallet_changed 信号照常采集)。
|
||||
@@ -350,8 +318,6 @@ static func _autoplay_shop(r, st: Dictionary) -> void:
|
||||
if not r.shop.buy_product(String(pick.get("product_id", ""))):
|
||||
return
|
||||
st.shop_buys = int(st.get("shop_buys", 0)) + 1
|
||||
|
||||
|
||||
# 商店共享池贪心:货架三选一同构面贪心买齐——affordable 集挑最便宜反复买
|
||||
# (每次买后全重抽、货架随买随换,枯竭/不可负担即止,guard 防死循环)。购买走 runner.shop.buy_pool_item
|
||||
# 核心编排(与呈现层购买同一出账+池消费路径,wallet_changed 信号照常采集)。确定性(零随机流)。
|
||||
@@ -372,24 +338,18 @@ static func _autoplay_shop_pool(r, st: Dictionary) -> void:
|
||||
if not r.shop.buy_pool_item(String(pick.get("kind_id", ""))):
|
||||
return
|
||||
st.shop_buys = int(st.get("shop_buys", 0)) + 1
|
||||
|
||||
|
||||
# 冒险终局(completed/failed/abandoned——result 载 summary.result,外层驱动循环随之收敛)
|
||||
static func _on_adventure_finished(summary: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.summary = summary.duplicate(true)
|
||||
st.result = String(summary.get("result", ""))
|
||||
if logger != null:
|
||||
logger.log_level_end(summary)
|
||||
|
||||
|
||||
# 策略流 RNG:种子派生自局种子 × 黄金比率常数 + 偏移——与核心统一流
|
||||
# (rng_service 单盐派生)零共享零扰动;逐局确定性保持(同种子 ⇒ 同决策序列 ⇒ 同回放)
|
||||
static func _policy_rng(seed_val: int) -> RandomNumberGenerator:
|
||||
var prng := RandomNumberGenerator.new()
|
||||
prng.seed = seed_val * 2654435761 + 912931
|
||||
return prng
|
||||
|
||||
|
||||
# 路径轴选路(目标价值评分制):候选按节点类型评分取最高,
|
||||
# 同分 tie-break 沿 route 方向(min=小句柄/max=大句柄——verifier 双窗轴保持)。
|
||||
# 评分语义(确定性,零随机流):rest=伤重高值(缺口比例×100)/shop=买得起高值(GP 局内死钱,
|
||||
@@ -438,8 +398,6 @@ static func _route_pick(runner, route: String, st: Dictionary) -> int:
|
||||
if better:
|
||||
best = c
|
||||
return int(best.id)
|
||||
|
||||
|
||||
static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows: bool) -> void:
|
||||
if row.type == "damage" and row.source_class == "enemy_action":
|
||||
st.hero_dmg += int(row.value)
|
||||
@@ -470,8 +428,6 @@ static func _on_step_row(row: Dictionary, _runner, st: Dictionary, collect_rows:
|
||||
st.seal_miss_penalty += int(row.value)
|
||||
if collect_rows:
|
||||
st.rows.append(row)
|
||||
|
||||
|
||||
# 敌方显示名采集:敌方行动/施放行文本首词 = 单位显示名
|
||||
#(攻击行「地精 撕咬(暴击)」、施放行「地精 施放 毒雾」——显示名不含空格,去重连续重复)
|
||||
static func _note_enemy(text: String, st: Dictionary) -> void:
|
||||
@@ -483,8 +439,6 @@ static func _note_enemy(text: String, st: Dictionary) -> void:
|
||||
var arr: Array = st.last_battle_enemies
|
||||
if arr.is_empty() or String(arr[arr.size() - 1]) != ename:
|
||||
arr.append(ename)
|
||||
|
||||
|
||||
static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dictionary, logger) -> void:
|
||||
var t := String(node.get("type", ""))
|
||||
if t == "battle" or t == "boss":
|
||||
@@ -506,8 +460,6 @@ static func _on_node_entered(node: Dictionary, info: Dictionary, runner, st: Dic
|
||||
# 不掺购买行为;冒险口径(run_adventure)已在 _on_adventure_node_entered 改贪心买齐
|
||||
if logger != null:
|
||||
logger.log_node(node, info, runner.node_index)
|
||||
|
||||
|
||||
static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.result = String(p.get("result", ""))
|
||||
st.hp_left = int(p.get("hp", 0))
|
||||
@@ -516,8 +468,6 @@ static func _on_level_finished(p: Dictionary, st: Dictionary, logger) -> void:
|
||||
st.defeat_node_type = String(p.get("node_type", ""))
|
||||
if logger != null:
|
||||
logger.log_level_end(p)
|
||||
|
||||
|
||||
static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void:
|
||||
# 策略档经 _decide_act 单点分派(单循环承载三档——三档决策语义逐位不变,见 _decide_act)。
|
||||
# 普通策略档 v3:完美策略 + 受控失误注入(标定依据=真人实证:
|
||||
@@ -534,40 +484,27 @@ static func _autoplay_battle(runner, settlement, st: Dictionary, logger) -> void
|
||||
round_guard -= 1
|
||||
st.rounds += 1
|
||||
var start_round: int = settlement.round_index
|
||||
var action_guard := 400
|
||||
var rejects := 0
|
||||
while runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" \
|
||||
and settlement.round_index == start_round and action_guard > 0 and rejects < 3:
|
||||
action_guard -= 1
|
||||
var act: Dictionary = _decide_act(st, settlement)
|
||||
var kind := String(act.get("kind", "stop"))
|
||||
if kind == "stop":
|
||||
break
|
||||
var ok := false
|
||||
if kind == "move":
|
||||
ok = _submit_intent(settlement, "move", act.get("cell"), logger, start_round)
|
||||
elif kind == "dig":
|
||||
ok = _submit_intent(settlement, "dig", act.get("cell"), logger, start_round)
|
||||
elif kind == "seal":
|
||||
ok = _submit_intent(settlement, "seal_place", act.get("cell"), logger, start_round)
|
||||
elif kind == "refresh":
|
||||
ok = bool(settlement.player_request_refresh())
|
||||
if logger != null:
|
||||
logger.log_action("refresh", Vector2i(-1, -1), ok, {"round": start_round})
|
||||
if ok:
|
||||
if kind == "refresh":
|
||||
st.refreshes = int(st.refreshes) + 1
|
||||
elif kind == "seal":
|
||||
st.seal_placed = int(st.get("seal_placed", 0)) + 1 # 封印数(η 分母=全部封印数〔Q5〕;失误注入转化封印不计数=转化后实为挖)
|
||||
rejects = 0
|
||||
else:
|
||||
rejects += 1
|
||||
if runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round:
|
||||
var out_s: Array = settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
if logger != null:
|
||||
logger.log_action("stop", Vector2i(-1, -1), _accepted(out_s), {"round": start_round})
|
||||
# 内层单轮循环委托 BattleDriver.drive_round(驱动循环单点——庚案收敛:本类禁再持第二轮驱动循环;
|
||||
# 策略分派/失误注入全保留在 _decide_act 原地,日志与计数经 _action_logger 回调承载)
|
||||
BattleDriver.drive_round(settlement,
|
||||
func() -> bool: return runner.current_battle() == settlement and settlement.battle_result == "" and settlement.action_economy.phase == "Active" and settlement.round_index == start_round,
|
||||
_act_provider(st), _action_logger(st, logger), 3)
|
||||
|
||||
|
||||
# 策略回调适配器(drive_round act_provider 契约——三档分派 perfect/novice/slip 全在 _decide_act 原地)
|
||||
static func _act_provider(st: Dictionary) -> Callable:
|
||||
return func(s) -> Dictionary: return _decide_act(st, s)
|
||||
|
||||
|
||||
# 日志回调适配器(drive_round on_action 契约——log_action 带轮号 + refresh/seal 计数语义逐位同原内联)
|
||||
static func _action_logger(st: Dictionary, logger) -> Callable:
|
||||
return func(kind: String, cell: Vector2i, ok: bool, ri: int) -> void:
|
||||
if logger != null:
|
||||
logger.log_action(kind, cell, ok, {"round": ri})
|
||||
if ok and kind == "refresh":
|
||||
st.refreshes = int(st.refreshes) + 1
|
||||
elif ok and kind == "seal_place":
|
||||
st.seal_placed = int(st.get("seal_placed", 0)) + 1 # 封印数(η 分母=全部封印数〔Q5〕;失误注入转化封印不计数=转化后实为挖)
|
||||
# 普通档 v3 失误注入:三类——
|
||||
# ①标雷误挖:把「封印已证雷」动作直接转成「挖该格」(真人手滑原型:知道是雷、位置点错,
|
||||
# 踩雷吃伤且该雷奖励报废);期望踩雷增量 ≈ 每板已证雷数 × NORMAL_MINE_SLIP_P(目标完美+2/板)
|
||||
@@ -598,8 +535,6 @@ static func _apply_normal_slips(act: Dictionary, settlement, st: Dictionary) ->
|
||||
if not plan.is_empty():
|
||||
return _gamble_action(plan, settlement)
|
||||
return act
|
||||
|
||||
|
||||
# 赌挖动作组装(v3.4 抽出):目标邻接=直接挖,否则先移到目标的已开邻格(下一拍再挖)
|
||||
static func _gamble_action(plan: Dictionary, settlement) -> Dictionary:
|
||||
var ms = settlement.minesweeper
|
||||
@@ -609,8 +544,6 @@ static func _gamble_action(plan: Dictionary, settlement) -> Dictionary:
|
||||
if maxi(d.x, d.y) == 1:
|
||||
return {"kind": "dig", "cell": target}
|
||||
return {"kind": "move", "cell": plan.hero_cell}
|
||||
|
||||
|
||||
# 战斗决策单行分派:
|
||||
# perfect 档 = 完美策略单步决策(上界口径);
|
||||
# normal 档 v3 = 完美+受控失误注入(冒险测量面 adv=true);单关机制夹具保持 v2 新手画像(adv 缺省)。
|
||||
@@ -621,15 +554,3 @@ static func _decide_act(st: Dictionary, settlement) -> Dictionary:
|
||||
if bool(st.get("adv", false)):
|
||||
return _apply_normal_slips(AutoPlayer.perfect_action(settlement.minesweeper, settlement), settlement, st) # v3:完美+失误
|
||||
return AutoPlayer.novice_action(settlement.minesweeper) # 单关机制夹具:v2 新手画像保持
|
||||
|
||||
|
||||
static func _submit_intent(settlement, action: String, cell, logger, round_index: int) -> bool:
|
||||
var out: Array = settlement.minesweeper.settle_batch([{"action": action, "cell": cell}])
|
||||
if logger != null:
|
||||
logger.log_action(action, cell, _accepted(out), {"round": round_index})
|
||||
return _accepted(out)
|
||||
|
||||
|
||||
static func _accepted(outcomes: Array) -> bool:
|
||||
return not outcomes.is_empty() and bool(outcomes[0].accepted)
|
||||
|
||||
@@ -1,61 +1,65 @@
|
||||
{
|
||||
"note": "\u5b58\u91cf\u57fa\u7ebf\u00b7\u95f8\u51b5\u53d8\u5316\u65f6\u672c\u811a\u672c\u4f1a\u5224\u7ea2\uff0c\u9700\u4eba\u5de5\u786e\u8ba4\u540e\u91cd\u751f\u6210\uff1b2026-10-29 \u4e8c\u6b21\u8bb0\u8d26\uff1d03/04/05 \u4e09\u95f8\u6309 80b49c0f \u5148\u4f8b\u7ecf\u7528\u6237\u88c1\u51b3\u8bb0\u5165\u73b0\u884c\u7ea2\u9879\u6307\u7eb9\uff0803=8a3cae0d0446/04=591b27250b36/05=de89589e49ed\uff09\u2014\u2014master \u540c\u503c\u590d\u73b0\u975e\u8fed\u4ee342\u65b0\u589e\uff08stash \u5bf9\u7167\u6cd5\u53d6\u8bc1\uff0c\u627f\u300c\u8fed\u4ee3 41 \u6d4b\u8bd5\u503a\u4fee\u590d\u300d\u6761\u76ee\u6269\u5c55\uff09\uff1b\u539f 2026-10-17 06 \u6761\u76ee\u4eba\u5de5\u91cd\u751f\u6210\u8bb0\u5f55\u89c1 git \u63d0\u4ea4\u53f2\uff1b2026-10-30 \u4e09\u952e\u6362\u4ee3\uff1d02/05/09 \u6309\u5de5\u7a0b\u5c0f\u4ef6\u6279\u56de\u6267 6a3203a8 \u5b9e\u6d4b\u7eff\u91cd\u951a\uff0802 \u5173\u8054\u6863\u5206\u72474/4\u5168\u7eff\uff0f05 \u6e32\u67d3\u95f8\u771f\u8dd1\u7eff\uff0f09 \u9759\u6001\u626b\u63cf\u7eff\u2014\u2014run_all \u9017\u53f7\u89e3\u6790\u4fee\u590d\uff0b\u6052\u771f\u4e94\u6761\u5224\u636e\u4fee\u6b63\u6279\uff0c\u4e09\u952e\u65e7\u7ea2\u6307\u7eb9\u5168\u90e8\u5f52\u7eff\u6d88\u9664\uff09",
|
||||
"gates": {
|
||||
"00 \u5192\u70df\u6d4b\u8bd5": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"01 \u7cfb\u7edf\u6d4b\u8bd5": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"02 \u5355\u5143\u4e0e\u96c6\u6210\u6d4b\u8bd5": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"03 \u573a\u666f\u63a2\u9488": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"05 \u6e32\u67d3\u95f8": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06 gdd_lint": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06b \u53d8\u66f4\u8bb0\u5f55\u8282": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"07 doc_link": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08 doc_term": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08c \u6d41\u6c34\u7ebf\u95f8": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"09 \u9759\u6001\u626b\u63cf": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"10 \u71b5\u589e\u95f8": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08d \u5f15\u7528\u53ef\u6838": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08e \u80fd\u529b\u5ba1\u8ba1": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
}
|
||||
"note": "存量基线·首次自动生成;闸况变化时本脚本会判红,需人工确认后重生成",
|
||||
"gates": {
|
||||
"00 冒烟测试": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"01 系统测试": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"02 单元与集成测试": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"03 场景探针": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"05 渲染闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"05b 色板闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06 gdd_lint": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"06b 变更记录节": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"07 doc_link": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08 doc_term": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08c 流水线闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08d 引用可核": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"08e 能力审计": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"09 静态扫描": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
},
|
||||
"10 熵增闸": {
|
||||
"exit": "0",
|
||||
"fp": "green"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,7 +183,7 @@ tests/
|
||||
| 同名状态合并策略(叠加/累加/并存)与剩余次数语义 | 战斗循环×增益状态 | `StatusContainer.apply_effect()`(编排宿主 begin_battle 链) | `roll_multiplier_test.gd:_smoke_armor_roll` |
|
||||
| 状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) | 增益状态 | `StatusContainer.apply_effect()`/`decay_round()`/`notify_battle_end()` | `system_contracts_test.gd:_test_status_decay_and_clear`;`system_contracts_test.gd:_test_status_stack_full_reject`;`system_contracts_test.gd:_test_status_independent_branch`;`system_contracts_test.gd:_test_status_snapshot_first_grant` |
|
||||
| 状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 | 增益状态 | `StatusContainer.apply_effect()`(个体键)/`purge_owner()` | `system_contracts_test.gd:_test_status_individual_isolation`;`system_contracts_test.gd:_test_status_grant_all_per_unit`;`system_contracts_test.gd:_test_status_death_clears`;`system_contracts_test.gd:_test_status_merge_per_unit` |
|
||||
| 敌方增益自体落点与乘区池按个体 | 敌人系统×战斗结算×增益状态 | 技能兑付编排链/`DamageController.clamped_pool()` | `round_flow_skill_pool_test.gd:_test_enemy_buff_targets_self`;`damage_controller_test.gd:_test_pool_consumption_per_unit`;`damage_controller_test.gd:_test_defender_ac_per_unit`;`battle_narrator_test.gd:_test_snapshot_effects_per_unit` |
|
||||
| 敌方增益自体落点与乘区池按个体 | 敌人系统×战斗结算×增益状态 | 技能兑付编排链/`DamageController.clamped_pool()` | `round_flow_skill_pool_test.gd:_smoke_enemy_buff_targets_self`;`damage_controller_test.gd:_test_pool_consumption_per_unit`;`damage_controller_test.gd:_test_defender_ac_per_unit`;`battle_narrator_test.gd:_test_snapshot_effects_per_unit` |
|
||||
| 敌人技能池跨表链接校验 | 数据配置×敌人系统 | `ConfigDbCrossValidator.validate_skill_pool_links()` | `config_db_cross_validator_test.gd:_test_all_links_valid`;`config_db_cross_validator_test.gd:_test_dangling_skill_id`;`config_db_cross_validator_test.gd:_test_cast_side_mismatch` |
|
||||
|
||||
**覆盖域表**(`design/gdd/systems/` 逐篇对照——状态三档:已覆盖/部分覆盖/未覆盖;部分与未覆盖行的缺口指待办清单条目,覆盖面提升时同批改本表):
|
||||
@@ -208,7 +208,7 @@ tests/
|
||||
| 表现与支撑 | 存档序列化.md | 存档写盘⇄读回往返保真;三槽位隔离与激活失败回滚;槽位摘要窥视与坏槽三态;启动态无激活不写盘;旧档迁移链与断点续玩零跳号(集成);存档⇄RNG 快照往返保真(集成) | 已覆盖 | — |
|
||||
| 表现与支撑 | 教学引导.md | — | 未覆盖 | 教学形态与首遇时机待设计;见待办清单「教学形态未定」条 |
|
||||
| 表现与支撑 | 数据配置.md | 全表装载、就绪态与表清单目录对账;进度坐标查行:精确命中/向上取整/封顶/警告去重;运行期越域致命与进程非 0 退出;加载期违规表拒绝:跨表悬空/计数器前缀/真视窄域;表结构守卫:域校验/行序键/跨字段互锁/配额锚 | 已覆盖 | — |
|
||||
| 表现与支撑 | 设置与本地化.md | 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) | 部分覆盖 | 存储契约断言由「断言补齐批」承接;多语言切换面未建(P1);见待办清单「能力清单覆盖缺口补齐」条 |
|
||||
| 表现与支撑 | 设置与本地化.md | 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) | 已覆盖 | 存储契约三断言已落地(迭代 50 断言补齐批);多语言切换面未建(P1,归设置与本地化待设计问题清单) |
|
||||
| 表现与支撑 | 音频.md | — | 未覆盖 | MVP 无音频(P1 后置,无断言面);见待办清单「能力清单覆盖缺口补齐」条 |
|
||||
| 跨局循环 | 元进度.md | 元进度入账与锻造两轴 | 已覆盖 | — |
|
||||
| 跨局循环 | 天赋树.md | 天赋树购买与六键合成 | 已覆盖 | — |
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 329 KiB |
@@ -142,6 +142,7 @@ func _smoke_playtest_logger() -> void:
|
||||
rng.set_master_seed(20260912)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
var logger = load("res://presentation/playtest_logger.gd").new()
|
||||
logger.ap_per_round = 8 # 必填注入(R5 守卫——夹具沿组合根表值 8)
|
||||
var path: String = logger.start_session(20260912)
|
||||
logger.attach_battle(settlement)
|
||||
var snaps: Array = []
|
||||
|
||||
+27
-3
@@ -149,6 +149,10 @@ func _init() -> void:
|
||||
return
|
||||
if failures > 0:
|
||||
push_error("[run_all] %d 个套件失败" % failures)
|
||||
for fp in failed_paths:
|
||||
push_error("[run_all] 失败套件: %s" % fp)
|
||||
for tp in timed_out_paths:
|
||||
push_error("[run_all] 超时套件: %s" % tp)
|
||||
quit(1)
|
||||
return
|
||||
var tag := ",filtered" if filtered else ""
|
||||
@@ -186,6 +190,12 @@ func _suites() -> Array:
|
||||
{"path": "res://tests/unit/core/curio_refresh_paid_test.gd", "expect": 0}, # 三选一刷新免费双源+GP 付费档(对齐商店 MC-1:价曲线/优先序/停点清零/钱包单口)
|
||||
{"path": "res://tests/unit/core/level_map_type_vis_test.gd", "expect": 0}, # 信息迷雾——类型可见性(层距 N+Boss/候选豁免+单圈+N 读表+续玩同值)
|
||||
{"path": "res://tests/unit/presentation/pacing_queue_test.gd", "expect": 0}, # AC⑤/T9——排空零分叉/零丢行/延迟双态
|
||||
{"path": "res://tests/unit/presentation/settings_store_test.gd", "expect": 0}, # 减动效开关存储契约(持久化回读/缺省值/引擎动效档应用)〔断言补齐批〕
|
||||
{"path": "res://tests/unit/battle_settlement/boss_gate_test.gd", "expect": 0}, # Boss 节点开战必选 Boss 行集(行类路由+端到端)〔断言补齐批〕
|
||||
{"path": "res://tests/unit/data_config/effect_kind_retired_test.gd", "expect": 1, "negative": true}, # 甲案词表退役——overflow/attack_segments 误写加载致命〔架构整改〕
|
||||
{"path": "res://tests/unit/core/playtest_logger_guard_test.gd", "expect": 0}, # R5 必填注入守卫——未注入拒绝开会话〔架构整改〕
|
||||
{"path": "res://tests/unit/core/battle_driver_drive_test.gd", "expect": 0}, # R12 驱动循环契约——缺省确定性/provider/rejects/合步〔架构整改〕
|
||||
{"path": "res://tests/unit/core/auto_player_deduce_test.gd", "expect": 0}, # R15 0 格连锁算子——远端格经真实闭包入 safe〔架构整改〕
|
||||
{"path": "res://tests/unit/presentation/standee_fx_sweep_test.gd", "expect": 0}, # 呈现域缺陷扫掠(d2 蓄力档/d4 庆祝句柄/d5+d8 互斥/d12 站桩按 id 解析)
|
||||
{"path": "res://tests/unit/presentation/tune_sim_batch_parity_test.gd", "expect": 0}, # 测速循环批双实现对拍(帧索引/运行态/进度串/stats/报告五面——信号步进 vs await 基线)
|
||||
{"path": "res://tests/unit/presentation/standee_present_values_test.gd", "expect": 0}, # 战斗一屏样张批 V5–V12(血条危险红/名字行素底不压地台与提档 FS_HINT/徽标/护甲 PNG 绑定/素材入包/地台色合并退役)
|
||||
@@ -269,9 +279,18 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictiona
|
||||
DirAccess.make_dir_recursive_absolute(proj + "/tests/.gates")
|
||||
# /V:ON=延迟展开:!ERRORLEVEL! 到 echo 执行时(套件已退出)才取值;
|
||||
# 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。
|
||||
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile]
|
||||
var pid: int = -1
|
||||
var out := PackedStringArray()
|
||||
var pid: int = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd]))
|
||||
if OS.get_name() == "Windows":
|
||||
# Windows 形态(原实现逐位保留):/V:ON 延迟展开,!ERRORLEVEL! 到 echo 时(套件已退出)才取值;
|
||||
# 延迟展开会吞字面 !,命令串各段(引擎路径/工程路径/套件路径/日志路径)不得含 !。
|
||||
var cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1 & echo !ERRORLEVEL! > "%s"' % [bin, proj, path, log, codefile]
|
||||
pid = OS.create_process("cmd.exe", PackedStringArray(["/V:ON", "/C", cmd]))
|
||||
else:
|
||||
# POSIX 形态(mac/Linux runner):/bin/sh -c 同步串行——sh 前台等引擎退出再落 $?,
|
||||
# 无延迟展开问题;create_process 拿 sh 的 pid(存活轮询=sh 前台等待期),截止杀走 kill 树。
|
||||
var sh_cmd := '"%s" --headless --path "%s" --script "%s" > "%s" 2>&1; echo $? > "%s"' % [bin, proj, path, log, codefile]
|
||||
pid = OS.create_process("/bin/sh", PackedStringArray(["-c", sh_cmd]))
|
||||
if pid < 0:
|
||||
push_error("[run_all] FAIL %s — cmd 子进程启动失败(pid=%d)" % [path, pid])
|
||||
return {"fail": 1, "timed_out": false}
|
||||
@@ -289,7 +308,12 @@ func _run_suite(suite: Dictionary, idx: int, filtered: bool = false) -> Dictiona
|
||||
var negative := bool(suite.get("negative", false))
|
||||
if timed_out:
|
||||
var kill_out := PackedStringArray()
|
||||
OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true)
|
||||
if OS.get_name() == "Windows":
|
||||
OS.execute("taskkill", PackedStringArray(["/F", "/T", "/PID", str(pid)]), kill_out, true)
|
||||
else:
|
||||
# POSIX:先杀 sh 的直接子进程(引擎),再杀 sh 自身(顺序要紧——先杀父会让子成孤儿续跑)
|
||||
OS.execute("pkill", PackedStringArray(["-9", "-P", str(pid)]), kill_out, true)
|
||||
OS.execute("kill", PackedStringArray(["-9", str(pid)]), kill_out, true)
|
||||
# taskkill 落刀到进程对象消失有间隔:等死确认再放行下一套件(10s 宽限),
|
||||
# 确认不了也如实报红留 PID 供手工清杀,不静默放行。
|
||||
var reap_deadline := Time.get_ticks_msec() + 10000
|
||||
|
||||
@@ -20,6 +20,16 @@ fail=0
|
||||
DIR_CORE="game/core"
|
||||
DIR_PRES="game/presentation"
|
||||
|
||||
# python 解释器解析:PATH 有 python 用 python,只有 python3(runner 常态)回落 python3——
|
||||
# 硬编码 python 在无别名机器上=127 command not found,三审计零输出静默判红(CI run15 实证)。
|
||||
PY_BIN=""
|
||||
if command -v python >/dev/null 2>&1; then PY_BIN="python"
|
||||
elif command -v python3 >/dev/null 2>&1; then PY_BIN="python3"
|
||||
else
|
||||
echo "[static_scan] FAIL: 无 python 解释器(PATH 上既无 python 也无 python3)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
scan_files() { # scan_files <目录> — 该目录下 .gd 文件数
|
||||
find "$1" -name '*.gd' 2>/dev/null | wc -l
|
||||
}
|
||||
@@ -36,7 +46,7 @@ require_scan_face() { # require_scan_face <规则名> <目录> — 扫描面
|
||||
}
|
||||
|
||||
# S9 — 类型闸(code-conventions §4 机器闸:产品码无类型容器/有值返回缺类型/参数缺类型)
|
||||
if python tools/audit_type_hints.py; then
|
||||
if "$PY_BIN" tools/audit_type_hints.py; then
|
||||
echo "[static_scan] S9 ok"
|
||||
else
|
||||
echo "[static_scan] S9 FAIL: 类型闸存量未归零(R1/R2/R3 见上清单)"
|
||||
@@ -44,7 +54,7 @@ else
|
||||
fi
|
||||
|
||||
# S10 — 文件头闸(code-conventions §1.4.1:## 载体+日期/变更叙事黑名单;行数上限已停用)
|
||||
if python tools/audit_file_headers.py; then
|
||||
if "$PY_BIN" tools/audit_file_headers.py; then
|
||||
echo "[static_scan] S10 ok"
|
||||
else
|
||||
echo "[static_scan] S10 FAIL: 文件头存量见上清单"
|
||||
@@ -296,7 +306,7 @@ fi
|
||||
# 簿,登记值只准随还债下降,禁抬高到现状。基线重生成=人工专用(python tools/gdstyle_gate.py
|
||||
# --refresh,附对账;键数骤降逾 10% 或登记值被抬高均拒写,须 --force),禁清基线换绿。二进制本机固定路径
|
||||
# D:/code/gdstyle/gdstyle.exe(不入库),GDSTYLE_BIN 环境变量覆盖;版本与钉定版不符即红(升级须重测)。
|
||||
if python tools/gdstyle_gate.py; then
|
||||
if "$PY_BIN" tools/gdstyle_gate.py; then
|
||||
echo "[static_scan] S13 ok"
|
||||
else
|
||||
echo "[static_scan] S13 FAIL: 基线外新违规/指标恶化或二进制/版本/扫描面问题(清单见上)"
|
||||
@@ -308,13 +318,15 @@ fi
|
||||
# 可回放纯逻辑步骤流(同种子同输入逐位复现),await 会打断确定性——「模块归属」B1 行已写明
|
||||
# 「禁 await——切片 88 冒烟实证」。本闸为防回归防线(2026-10-24 人工 grep 盘面干净)。
|
||||
# 扫描面:B1 域文件集(与 architecture.md「模块归属」B1 行同口径)=battle_orchestrator.gd
|
||||
# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,共 14 文件,显式清单见 S8_FILES。
|
||||
# + battle_fx_host.gd + reward_grant.gd + board_installer.gd 三拆件 + settlement/ 五段段类 + damage_controller.gd,清单单点=B1_SET。
|
||||
# 执行侧字面值豁免:脚本必须带字面值才能工作;文件集与架构文档冲突时以 B1 域文件集为准。
|
||||
# 判据:文件集内出现 await 关键字即红(词边界 \bawait\b,任何形态——await 信号/协程/
|
||||
# get_tree());排除注释行(行首 #/##)与字符串字面量(双引号包裹段内的 await 非代码),
|
||||
# 口径仿 S1/S2 的 grep 排除手法。
|
||||
# 自检:清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿。
|
||||
S8_FILES="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||||
# 自检:①清单内文件缺位(路径写错/被改名)即判红——防扫面为空静默绿;②settlement/ 目录域反向对账——新段类未登记即判红(见下)。
|
||||
# B1 域文件集=单点清单(S8 await 禁扫面与 S7 豁免名单共用;改成员先改 architecture.md「模块归属」B1 行再改此处,2026-11-03 架构审查 P1 裁并点)
|
||||
B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||||
S8_FILES="$B1_SET"
|
||||
S8_MISS=0
|
||||
for f in $S8_FILES; do
|
||||
if [ ! -f "$f" ]; then
|
||||
@@ -322,6 +334,17 @@ for f in $S8_FILES; do
|
||||
S8_MISS=1
|
||||
fi
|
||||
done
|
||||
# 自检二(2026-11-03 架构审查 P1 裁):目录域反向对账——settlement/ 实有 .gd 未登记进 B1_SET=该扫的没扫,判红。
|
||||
# 只反向对 settlement/ 一个目录域:装板/授予/效果端口/掷格四件结算码住 game/core 根下,纯目录域覆盖不住,
|
||||
# 根下新增仍由「模块归属」B1 行+写卡段申报面承接(禁把目录判据当成全域判据)。
|
||||
S8_UNREG=""
|
||||
for f in $(find game/core/settlement -name '*.gd' 2>/dev/null | sort); do
|
||||
case " $B1_SET " in *" $f "*) ;; *) S8_UNREG="$S8_UNREG $f" ;; esac
|
||||
done
|
||||
if [ -n "$S8_UNREG" ]; then
|
||||
echo "[static_scan] S8 FAIL: settlement/ 有结算段类未登记进 B1 域文件集——await 禁扫面对它静默失效:$S8_UNREG(先改 architecture.md「模块归属」B1 行,再补 B1_SET)"
|
||||
S8_MISS=1
|
||||
fi
|
||||
if [ "$S8_MISS" -eq 1 ]; then
|
||||
fail=1
|
||||
else
|
||||
@@ -334,7 +357,7 @@ else
|
||||
grep -rnE '\bawait\b' $S8_FILES 2>/dev/null | grep -vE '^[^:]+:[0-9]+:[[:space:]]*#' | grep -vE '"[^"]*\bawait\b[^"]*"'
|
||||
fail=1
|
||||
else
|
||||
echo "[static_scan] S8 ok(扫描面 B1 文件集 10 个 .gd,await 零命中)"
|
||||
echo "[static_scan] S8 ok(扫描面 B1 文件集 $(echo $S8_FILES | wc -w) 个 .gd+settlement/ 目录域已反向对账,await 零命中)"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -358,7 +381,7 @@ fi
|
||||
if require_scan_face "S7" "$DIR_CORE"; then
|
||||
# B1 域文件集(迭代 46 卡A 增补 battle_refresh_flow——换图刷新流程件=C13 拆分边界的 B1 域拆件,
|
||||
# 承载旧资源退役/装板编排调用;architecture.md「模块归属」BattleRefreshFlow 行随反哺点落文)
|
||||
S7_B1_SET="game/core/battle_orchestrator.gd game/core/battle_fx_host.gd game/core/reward_grant.gd game/core/board_installer.gd game/core/settlement/aura_segment.gd game/core/settlement/combat_segment.gd game/core/settlement/decay_segment.gd game/core/settlement/grant_segment.gd game/core/settlement/verdict_segment.gd game/core/settlement/round_cycle.gd game/core/damage_controller.gd game/core/settlement/battle_refresh_flow.gd game/core/effect_port.gd game/core/reward_roller.gd"
|
||||
S7_B1_SET="$B1_SET" # 与 S8 同一单点清单(原第二份抄本,2026-11-03 并点——两份靠人工同步必漂)
|
||||
S7_MISS=0
|
||||
for f in $S7_B1_SET game/core/unit_container.gd game/core/status_container.gd game/core/reward_economy.gd; do
|
||||
if [ ! -f "$f" ]; then
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# tests/unit/battle_settlement/boss_gate_test.gd — Boss 节点开战必选 Boss 行集(端到端)
|
||||
# 断言面 = 行类路由(resolver:boss 行集全行 boss / battle 行集零 boss 行)+端到端
|
||||
# (begin_battle(node_type="boss") → 实例化敌人全部出自 boss 行集)。固定种子经 RngService 流口。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/battle_settlement/boss_gate_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_boss_node_spawns_boss_rowset()
|
||||
quit(Kit.finish("boss_gate_test"))
|
||||
|
||||
|
||||
func _test_boss_node_spawns_boss_rowset() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0
|
||||
Kit.check("前置:表加载零 error", load_ok)
|
||||
if not load_ok:
|
||||
return
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20261103) # 固定种子经 RngService 流口(RNG 单点)
|
||||
# 行类路由面:resolver 预解析(boss 行集全行 row_kind=="boss";对照 battle 行集零 boss 行)
|
||||
var boss_ids := {}
|
||||
var battle_ids := {}
|
||||
var boss_kind_ok := true
|
||||
for row in db.combat.enemy.resolve_rowset(1, 4, "boss"):
|
||||
boss_ids[String(row["enemy_id"])] = true
|
||||
if String(row["row_kind"]) != "boss":
|
||||
boss_kind_ok = false
|
||||
var battle_kind_ok := true
|
||||
for row in db.combat.enemy.resolve_rowset(1, 4, "battle"):
|
||||
battle_ids[String(row["enemy_id"])] = true
|
||||
if String(row["row_kind"]) == "boss":
|
||||
battle_kind_ok = false
|
||||
Kit.check("行类路由:boss 行集非空全行 boss、battle 行集非空零 boss 行〔断言补齐批 V10〕",
|
||||
boss_kind_ok and battle_kind_ok and boss_ids.size() > 0 and battle_ids.size() > 0)
|
||||
# 端到端:begin_battle(node_type="boss") → 实例化敌人行键全部出自 boss 行集
|
||||
var oc = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
var ok_boss: bool = oc.begin_battle({"level_index": 1, "node_depth": 4, "hero_hp": int(db.combat.hero.max_hp), "node_type": "boss"})
|
||||
Kit.check("前置:boss 节点开局受理(L1 D4 Boss 行集)", ok_boss)
|
||||
if not ok_boss:
|
||||
return
|
||||
var all_from_boss := true
|
||||
for u in oc._enemy_system.all_enemies():
|
||||
if not boss_ids.has(String(u["enemy_id"])):
|
||||
all_from_boss = false
|
||||
Kit.check("Boss 节点开战必选 Boss 行集(实例化敌人全部出自 boss 行集——跨宿主真实入口链)〔断言补齐批 V10〕", all_from_boss)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dkmph2dfqjqer
|
||||
@@ -35,6 +35,8 @@ func _init() -> void:
|
||||
_test_hero_death_halt(db)
|
||||
_test_ac_merge(db)
|
||||
_test_pool_clamp(db)
|
||||
_test_pool_consumption_per_unit(db)
|
||||
_test_defender_ac_per_unit(db)
|
||||
quit(Kit.finish("damage_controller_test"))
|
||||
|
||||
|
||||
@@ -45,7 +47,7 @@ func _on_row(head, ri, display, value, extra) -> void:
|
||||
"round_index": ri, "display": display, "value": value, "extra": extra, "highlight": head.get("highlight", "")})
|
||||
|
||||
|
||||
func _harness(db):
|
||||
func _harness(db, depth: int = 0):
|
||||
var rng = RngServiceScript.new()
|
||||
rng.set_master_seed(TEST_SEED)
|
||||
var units = UnitContainerScript.new(db)
|
||||
@@ -53,7 +55,7 @@ func _harness(db):
|
||||
var ctrl = DamageControllerScript.new(db, rng, units, effects)
|
||||
ctrl.emit_row_cb = Callable(self, "_on_row")
|
||||
effects.notify_battle_start()
|
||||
var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)})
|
||||
var spawn_ok: bool = units.spawn({"level_index": 1, "node_depth": depth, "hero_hp": int(db.combat.hero.max_hp)})
|
||||
return {"db": db, "rng": rng, "units": units, "effects": effects, "ctrl": ctrl, "spawn_ok": spawn_ok}
|
||||
|
||||
|
||||
@@ -219,6 +221,7 @@ func _test_source_death_void(db) -> void:
|
||||
# 单位字典键集复刻(剥离 handle_id:生产键集含句柄,`==` 误命的可达条件被破坏,夹具刻意不携句柄)
|
||||
func _unit_fixture() -> Dictionary:
|
||||
return {
|
||||
"handle_id": 0, # 生成序轴(个体化批——个体键导出沿 handle_id;手搭夹具同契约)
|
||||
"enemy_id": "fixture_enemy",
|
||||
"display_name": "地精",
|
||||
"hp": 5, "max_hp": 5, "atk": 1,
|
||||
@@ -342,7 +345,7 @@ func _test_ac_merge(db) -> void:
|
||||
Kit.check("前置:状态表存在 hero 侧 ac 型行", false)
|
||||
return
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
h.effects.apply_effect(String(ac_eff.effect_id), src)
|
||||
h.effects.apply_effect(String(ac_eff.effect_id), StatusContainerScript.OWNER_HERO, src)
|
||||
var expected: int = override_v + int(ac_eff.magnitude)
|
||||
Kit.check("ac 型效果叠层入 AC(层数×幅度,不入池)", h.ctrl.defender_ac(h.units.hero_unit()) == expected)
|
||||
|
||||
@@ -356,7 +359,7 @@ func _test_pool_clamp(db) -> void:
|
||||
return
|
||||
var lo: int = int(h.db.combat.settlement.core.pool_clamp_lo)
|
||||
var hi: int = int(h.db.combat.settlement.core.pool_clamp_hi)
|
||||
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense") == 0 and lo <= 0 and hi >= 0)
|
||||
Kit.check("空效果池读 0(表 clamp 域含 0 前提)", h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == 0 and lo <= 0 and hi >= 0)
|
||||
# 找 hero_offense 池的 percent 型 hero 侧效果行
|
||||
var pool_eff := {}
|
||||
for row in h.db.combat.status_effects.rows:
|
||||
@@ -369,12 +372,12 @@ func _test_pool_clamp(db) -> void:
|
||||
Kit.check("前置:hero_offense 池存在 percent 型 hero 侧行", false)
|
||||
return
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src)
|
||||
var first_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), StatusContainerScript.OWNER_HERO, src)
|
||||
var expected1: int = clampi(int(pool_eff.magnitude), lo, hi)
|
||||
Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
|
||||
var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), src)
|
||||
Kit.check("池值 = 单实例 L×m 加算后 clamp", first_ok and h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == expected1)
|
||||
var second_ok: bool = h.effects.apply_effect(String(pool_eff.effect_id), StatusContainerScript.OWNER_HERO, src)
|
||||
Kit.check("累加政策重复授予受理且层数恒 1(同名重复授予加时长)——池值不变",
|
||||
second_ok and h.ctrl.clamped_pool("hero_offense") == expected1)
|
||||
second_ok and h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == expected1)
|
||||
# ac 型效果不入 percent 池
|
||||
var ac_eff2 := {}
|
||||
for row2 in h.db.combat.status_effects.rows:
|
||||
@@ -382,9 +385,9 @@ func _test_pool_clamp(db) -> void:
|
||||
ac_eff2 = row2
|
||||
break
|
||||
if not ac_eff2.is_empty():
|
||||
var before: int = h.ctrl.clamped_pool("hero_offense")
|
||||
h.effects.apply_effect(String(ac_eff2.effect_id), src)
|
||||
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense") == before)
|
||||
var before: int = h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO)
|
||||
h.effects.apply_effect(String(ac_eff2.effect_id), StatusContainerScript.OWNER_HERO, src)
|
||||
Kit.check("ac 型效果不入池(percent 池隔离)", h.ctrl.clamped_pool("hero_offense", StatusContainerScript.OWNER_HERO) == before)
|
||||
# stack 政策效果:全新容器授予至满层 → 再授拒绝(满层拒绝原子——容器零写入)
|
||||
var stack_eff := {}
|
||||
for row3 in h.db.combat.status_effects.rows:
|
||||
@@ -399,6 +402,47 @@ func _test_pool_clamp(db) -> void:
|
||||
var granted := 0
|
||||
var max_n: int = int(stack_eff.max_stack)
|
||||
for i in range(max_n + 1):
|
||||
if effects2.apply_effect(String(stack_eff.effect_id), src):
|
||||
if effects2.apply_effect(String(stack_eff.effect_id), StatusContainerScript.OWNER_HERO, src):
|
||||
granted += 1
|
||||
Kit.check("stack 政策恰授予 max_stack 层后拒绝(满层拒绝原子)", granted == max_n)
|
||||
|
||||
|
||||
# ---- 个体池读数 / 个体 AC 合流(增益状态 v1.5——敌方状态个体化批)----
|
||||
|
||||
func _test_pool_consumption_per_unit(db) -> void:
|
||||
var h = _harness(db, 3) # depth 3 = hobgoblin 行集(unit_count 2-3——个体对照面需 ≥2 敌)
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var enemies: Array = h.units.all_enemies()
|
||||
Kit.check("V6 前置:行集 ≥2 敌(个体池对照面)", enemies.size() >= 2)
|
||||
if enemies.size() < 2:
|
||||
return
|
||||
var wmag: int = int(db.combat.status_effects.row_by_id("weaken").magnitude)
|
||||
var lo: int = int(db.combat.settlement.core.pool_clamp_lo)
|
||||
var hi: int = int(db.combat.settlement.core.pool_clamp_hi)
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var k1: String = StatusContainerScript.owner_key_for(enemies[0])
|
||||
var k2: String = StatusContainerScript.owner_key_for(enemies[1])
|
||||
Kit.check("V6 前置:e1 受理削弱", h.effects.apply_effect("weaken", k1, src))
|
||||
Kit.check("B 敌的削弱不进 A 敌的账:e1 池含 weaken 值(clamp 域内)、e2 恒 0〔个体化批 V6〕",
|
||||
h.ctrl.clamped_pool("enemy_output", k1) == clampi(wmag, lo, hi) and h.ctrl.clamped_pool("enemy_output", k2) == 0)
|
||||
|
||||
|
||||
func _test_defender_ac_per_unit(db) -> void:
|
||||
var h = _harness(db, 3)
|
||||
if not h.spawn_ok:
|
||||
return
|
||||
var enemies: Array = h.units.all_enemies()
|
||||
Kit.check("V7 前置:行集 ≥2 敌(个体 AC 对照面)", enemies.size() >= 2)
|
||||
if enemies.size() < 2:
|
||||
return
|
||||
var amag: int = int(db.combat.status_effects.row_by_id("alert").magnitude)
|
||||
var src := {"source_class": "mine_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var u1: Dictionary = enemies[0]
|
||||
var u2: Dictionary = enemies[1]
|
||||
var k1: String = StatusContainerScript.owner_key_for(u1)
|
||||
var base1: int = int(u1.get("armor_class", 0))
|
||||
var base2: int = int(u2.get("armor_class", 0))
|
||||
Kit.check("V7 前置:e1 受理警觉", h.effects.apply_effect("alert", k1, src))
|
||||
Kit.check("alert 各算各的:e1 AC = 自身基数 + 警觉幅度、e2 AC = 自身基数(该敌自己名下 ac 型)〔个体化批 V7〕",
|
||||
h.ctrl.defender_ac(u1) == base1 + amag and h.ctrl.defender_ac(u2) == base2)
|
||||
@@ -33,7 +33,7 @@ func _init() -> void:
|
||||
|
||||
func _enc(feats: Array) -> Dictionary:
|
||||
return {"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp),
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [],
|
||||
"hero_armor_class": 10, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": {},
|
||||
"battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ func _test_biz_fact_face_closed() -> void:
|
||||
var ctrl = orch.fx_host().damage
|
||||
# 阳性对照(载荷携带事实——harness 能见命中):合成行独占订阅面(rebuild 持有 id 过滤——
|
||||
# 只挂阳性行),board_flat_hero 真实单据流,base_value 足量避开伤害下限保底
|
||||
orch.fx_host().rebuild_subscriptions([pos_id], [], [])
|
||||
orch.fx_host().rebuild_subscriptions([pos_id], [], {})
|
||||
var hp0 := int(ctrl.units.hero_unit().get("hp", 0))
|
||||
var t_a: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测甲",
|
||||
{"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()})
|
||||
@@ -121,7 +121,7 @@ func _test_biz_fact_face_closed() -> void:
|
||||
var hp1 := int(ctrl.units.hero_unit().get("hp", 0))
|
||||
Kit.check("V3 阳性对照:hp_ratio 载荷 when 通过——承伤减量恰 1(10→9)", hp0 - hp1 == 9)
|
||||
# 阴性对照(闭面外事实——宿主链答而业务面不答):订阅面换代为阴性合成行独占,同路径零减量=F10 现状形态守卫
|
||||
orch.fx_host().rebuild_subscriptions([neg_id], [], [])
|
||||
orch.fx_host().rebuild_subscriptions([neg_id], [], {})
|
||||
var t_b: Dictionary = ctrl.open_ticket(ctrl.KIND_BOARD_FLAT_HERO, "probe_port", "闭面探测乙",
|
||||
{"channel": "board", "base_value": 10, "target_rule": "hero", "ri": 1, "origin_cell": orch.minesweeper.hero_pos()})
|
||||
ctrl.flush_submit(t_b)
|
||||
|
||||
@@ -48,7 +48,7 @@ func _init() -> void:
|
||||
func _install_dt_probe() -> void:
|
||||
if _db.crossrun.feats.row_by_id("probe_dt_counter").is_empty():
|
||||
(_db.crossrun.feats.rows as Array).append({
|
||||
"feat_id": "probe_dt_counter", "display_name": "承伤计数探针", "rarity": "common",
|
||||
"feat_id": "probe_dt_counter", "display_name": "承伤计数探针", "rarity": "common", "cap": 1,
|
||||
"desc": "测试夹具:每次主角承伤计数 +1",
|
||||
"triggers": [{"on": "damage_taken", "do": {"kind": "counter_add", "counter_id": "dt_probe_hits", "value": 1}}],
|
||||
})
|
||||
@@ -62,7 +62,7 @@ func _orch(seed_v: int):
|
||||
|
||||
func _enc(feats: Array, voucher_rap: int = 0, armor: int = 10, hero_hp: int = 200) -> Dictionary:
|
||||
return {"level_index": 1, "node_depth": 4, "hero_hp": hero_hp, "hero_atk": 8, "hero_max_hp": 200,
|
||||
"hero_armor_class": armor, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": [],
|
||||
"hero_armor_class": armor, "hero_ap_bonus": 0, "attunement": [], "feats": feats, "tree_rules": {},
|
||||
"battle_index": 1, "voucher_rap": voucher_rap, "voucher_time_sand": 0, "node_type": "boss", "adv_bucket": {}}
|
||||
|
||||
|
||||
@@ -361,7 +361,7 @@ func test_no_feats_baseline() -> void:
|
||||
func _install_br_probe() -> void:
|
||||
if _db.crossrun.feats.row_by_id("probe_br_counter").is_empty():
|
||||
(_db.crossrun.feats.rows as Array).append({
|
||||
"feat_id": "probe_br_counter", "display_name": "刷新事实探针", "rarity": "common",
|
||||
"feat_id": "probe_br_counter", "display_name": "刷新事实探针", "rarity": "common", "cap": 1,
|
||||
"desc": "测试夹具:每次成功刷新计数 +1",
|
||||
"triggers": [{"on": "board_refreshed", "do": {"kind": "counter_add", "counter_id": "br_probe_hits", "value": 1}}],
|
||||
})
|
||||
|
||||
@@ -231,24 +231,24 @@ func _weaken_accumulate_probe() -> Dictionary:
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 2, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
return {}
|
||||
var src: Dictionary = {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index}
|
||||
settlement._status_effects.apply_effect("weaken", src)
|
||||
settlement._status_effects.apply_effect("weaken", "e1", src)
|
||||
settlement._status_effects.decay_round() # remaining 2 → 1(先兑付后衰减——只借衰减器,不结算)
|
||||
settlement._status_effects.apply_effect("weaken", src) # 累加:remaining 1+2=3,层数不动
|
||||
settlement._status_effects.apply_effect("weaken", "e1", src) # 累加:remaining 1+2=3,层数不动
|
||||
var regrant := _weaken_inst(settlement)
|
||||
settlement._status_effects.apply_effect("weaken", src, 3) # 覆盖时长按覆盖值累加:3+3=6
|
||||
settlement._status_effects.apply_effect("weaken", "e1", src, 3) # 覆盖时长按覆盖值累加:3+3=6
|
||||
var override_grant := _weaken_inst(settlement)
|
||||
settlement._status_effects.apply_effect("weaken", src) # 同轮再授:6+2=8
|
||||
settlement._status_effects.apply_effect("weaken", "e1", src) # 同轮再授:6+2=8
|
||||
var extra := _weaken_inst(settlement)
|
||||
if regrant.is_empty() or override_grant.is_empty() or extra.is_empty():
|
||||
return {}
|
||||
# 走伤害控制器公有算术面 clamped_pool
|
||||
return {"layers": int(extra.layers), "pool": int(settlement._damage.clamped_pool("enemy_output")),
|
||||
return {"layers": int(extra.layers), "pool": int(settlement._damage.clamped_pool("enemy_output", "e1")),
|
||||
"after_regrant": int(regrant.remaining_settlements), "after_override": int(override_grant.remaining_settlements),
|
||||
"after_extra": int(extra.remaining_settlements), "snap_magnitude": int(regrant.snap_magnitude)}
|
||||
|
||||
|
||||
func _weaken_inst(settlement: Object) -> Dictionary:
|
||||
for eff in settlement._status_effects.query_active("enemy"):
|
||||
for eff in settlement._status_effects.query_active("e1"):
|
||||
if String(eff.effect_id) == "weaken":
|
||||
return eff
|
||||
return {}
|
||||
@@ -269,7 +269,7 @@ func _ac11_tier_samples() -> Array:
|
||||
var guard := 12
|
||||
while settlement.battle_result == "" and guard > 0:
|
||||
guard -= 1
|
||||
settlement._status_effects.apply_effect("ac_up", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement._status_effects.apply_effect("ac_up", "hero", {"source_class": "safe_cell", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
var tiers: Array = []
|
||||
for row in rows:
|
||||
@@ -368,7 +368,10 @@ func _hero_attack_row_with(opts: Dictionary, hero_atk_val: int, seed_val: int) -
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp), "hero_atk": hero_atk_val}):
|
||||
return {}
|
||||
for eff in opts.get("effects", []):
|
||||
settlement._status_effects.apply_effect(String(eff), {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
# 个体键读数(个体化批):按状态行 target_side 路由个体——主角行归主角桶、敌方行归首敌(被打者)
|
||||
var erow: Dictionary = _db.combat.status_effects.row_by_id(String(eff))
|
||||
var okey: String = "e1" if String(erow.get("target_side", "")) == "enemy" else "hero"
|
||||
settlement._status_effects.apply_effect(String(eff), okey, {"source_class": "mine_hit", "origin_cell": Vector2i(0, 0), "round_index": settlement.round_index})
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}])
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "damage" and String(row.get("source_class", "")) == "hero_attack":
|
||||
|
||||
@@ -39,6 +39,7 @@ func _init() -> void:
|
||||
_smoke_skill_pool()
|
||||
_smoke_counter_buckets()
|
||||
_smoke_extended_rows()
|
||||
_smoke_enemy_buff_targets_self()
|
||||
quit(Kit.finish("round_flow_skill_pool_test"))
|
||||
|
||||
|
||||
@@ -420,7 +421,7 @@ func _smoke_settlement_segments() -> void:
|
||||
if decays != 1 or verdicts != 1:
|
||||
decay_verdict_ok = false # 衰减/判定每结算恰一次
|
||||
Kit.check("每轮行相位单调(授予→增益/减益→战斗→衰减→判定)", order_ok)
|
||||
# 案C 段内序:段内伤害序 = 主角先手 →〔连珠矢随先手段〕→ 敌方逐个 → 地图账殿后。
|
||||
# 案C 段内序:段内伤害序 = 主角先手 → 敌方逐个 → 地图账殿后。
|
||||
# 地图账 = 全部棋盘来源伤害行 + 主角倒下不兑留痕行;违序(先于先手或插进敌方还手中间)即失败。
|
||||
var mapdmg_ok := true
|
||||
for ri_key in by_round.keys():
|
||||
@@ -439,13 +440,13 @@ func _smoke_settlement_segments() -> void:
|
||||
var rw: Dictionary = rows_r[i]
|
||||
var sc := String(rw.get("source_class", ""))
|
||||
var is_board_acct: bool = (String(rw.get("type", "")) == "damage" \
|
||||
and not ["hero_attack", "enemy_action", "fx_streak_segments"].has(sc)) \
|
||||
and not ["hero_attack", "enemy_action"].has(sc)) \
|
||||
or String(rw.get("forfeit", "")) == "hero_down"
|
||||
if not is_board_acct:
|
||||
continue
|
||||
if (first_hero >= 0 and i < first_hero) or (last_enemy >= 0 and i < last_enemy):
|
||||
mapdmg_ok = false
|
||||
Kit.check("案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账;连珠矢随先手段)", mapdmg_ok)
|
||||
Kit.check("案C 段内序:地图伤害账殿后(主角先手→敌方逐个→地图账)", mapdmg_ok)
|
||||
Kit.check("衰减/判定段每结算恰一段(先兑付后衰减——含末轮)", decay_verdict_ok)
|
||||
|
||||
|
||||
@@ -994,7 +995,7 @@ func _smoke_counter_buckets() -> void:
|
||||
var RngS = load("res://core/rng_service.gd")
|
||||
var enc1 = {"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": {},
|
||||
"feats": ["twin_pick"], "tree_rules": [], "battle_index": 1,
|
||||
"feats": ["twin_pick"], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": led}
|
||||
var o1 = RigS.new(_db, _fresh_rng(779301))
|
||||
@@ -1013,7 +1014,7 @@ func _smoke_counter_buckets() -> void:
|
||||
# V6——battle 桶随宿主销毁:第1战标错落 insurance_used(battle 桶无前缀),次战新编排器读 0。
|
||||
var encA = {"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": {},
|
||||
"feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 1,
|
||||
"feats": ["perfect_insurance"], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": {}}
|
||||
var oA = RigS.new(_db, _fresh_rng(779401))
|
||||
@@ -1052,7 +1053,7 @@ func _smoke_extended_rows() -> void:
|
||||
var RigS = load("res://core/battle_orchestrator.gd") # 局部装配别名(_smoke_counter_buckets 同款——跨函数不共享局部量)
|
||||
var enc_fx = {"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": {},
|
||||
"feats": ["geologist"], "tree_rules": [], "battle_index": 1,
|
||||
"feats": ["geologist"], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": {}}
|
||||
# ② 兑付行:有未揭示安全格 → 掷格揭示成功 → 扩展行带真实格与 fx_reveal 身份
|
||||
@@ -1086,3 +1087,50 @@ func _smoke_extended_rows() -> void:
|
||||
Kit.check("卡④ wr2③:揭示落空行 type=extended 且 effect_id=fx_reveal 且 origin_cell=越界哨值〔明细收敛 V2〕",
|
||||
not missed.is_empty() and String(missed.get("type", "")) == "extended"
|
||||
and Vector2i(missed["origin_cell"][0], missed["origin_cell"][1]).x == -1)
|
||||
|
||||
|
||||
# ---- 敌方增益技自体落点(增益状态 v1.5 目标规则「自己」——敌方状态个体化批)----
|
||||
# 真实入口=战斗结算编排链(技能施放):begin_battle → stop 结算推进一轮(战斗段施放+轮末衰减,
|
||||
# 蓄力表值 2 拍衰减后剩 1 拍仍在池)→ 逐敌读敌方输出池。判据=施放者与被他敌的池读数差。
|
||||
|
||||
func _smoke_enemy_buff_targets_self() -> void:
|
||||
var emp_mag: int = int(_db.combat.status_effects.row_by_id("empower").magnitude)
|
||||
var sc_script = load("res://core/status_container.gd")
|
||||
var found_seed := -1
|
||||
var found_ok := false
|
||||
var carrier_pool := -999
|
||||
for s in range(1, 401):
|
||||
var rg = load("res://core/rng_service.gd").new()
|
||||
rg.set_master_seed(s)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rg)
|
||||
var rows: Array = []
|
||||
settlement.step_row.connect(func(row): rows.append(row))
|
||||
# L1 depth 3(hobgoblin 行集 2-3 只——depth 0 单 goblin 常被主角先手一击秒杀,死亡即不动手、cast 不发生)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
continue
|
||||
for turn in range(4):
|
||||
if settlement.battle_result != "":
|
||||
break
|
||||
settlement.minesweeper.settle_batch([{"action": "stop"}]) # 推进一轮:施放→衰减(蓄力仍持 1 拍)
|
||||
if settlement.battle_result != "":
|
||||
break
|
||||
var charge_casts := 0
|
||||
for row in rows:
|
||||
if String(row.get("type", "")) == "cast" and String(row.get("skill_id", "")) == "charge":
|
||||
charge_casts += 1
|
||||
if charge_casts != 1:
|
||||
continue # 只取恰一次蓄力施放的时刻——判据单义
|
||||
var carriers: Array = []
|
||||
for u in settlement._enemy_system.query_alive("enemy"):
|
||||
var okey: String = sc_script.owner_key_for(u)
|
||||
if settlement._damage.clamped_pool("enemy_output", okey) != 0:
|
||||
carriers.append(okey)
|
||||
if carriers.size() == 1:
|
||||
found_seed = s
|
||||
carrier_pool = settlement._damage.clamped_pool("enemy_output", String(carriers[0]))
|
||||
found_ok = carrier_pool == emp_mag
|
||||
break
|
||||
if found_seed > 0:
|
||||
break
|
||||
Kit.check("蓄力施放种子可达(恰一次 cast=charge,seed=%d)" % found_seed, found_seed > 0)
|
||||
Kit.check("敌方增益技自体落点:施放者 output 池 = 蓄力幅度(%d,实测 %d)、他敌恒 0〔个体化批 V5〕" % [emp_mag, carrier_pool], found_ok)
|
||||
@@ -748,7 +748,7 @@ func _smoke_negative_dictionary() -> void:
|
||||
var snap_b: Dictionary = settlement._reward_economy.query_full_snapshot(by_type["blast"])
|
||||
settlement.reward_grant().grant_routed(snap_b, "negative", {"source_class": "mine_hit", "cell": by_type["blast"], "ri": 1})
|
||||
var alert_ok: bool = false
|
||||
for eff_a in settlement._status_effects.query_active("enemy"):
|
||||
for eff_a in settlement._status_effects.query_active_side("enemy"):
|
||||
if String(eff_a.effect_id) == "alert":
|
||||
alert_ok = true
|
||||
Kit.check("blast 型负向 → 警觉(敌方护甲等级 +1 状态在位)", alert_ok)
|
||||
|
||||
@@ -113,7 +113,7 @@ func _init() -> void:
|
||||
var orch = RigS.new(db, _rng(700001))
|
||||
Kit.check_quiet(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": [],
|
||||
"feats": [], "tree_rules": [], "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"feats": [], "tree_rules": {}, "battle_index": 1, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": {}}), "V6 前置:战斗受理")
|
||||
# facts 组包经真实摇号路径消费(mines 全零时条件技 sneak_ambush 不入集合——facts 四键在位的可观察面)
|
||||
Kit.check_quiet(int(orch._rounds.mines_hits["total"]) == 0 and int(orch._rounds.mines_hits["last"]) == 0 and int(orch._rounds.mines_hits["round"]) == 0,
|
||||
|
||||
@@ -28,6 +28,7 @@ func _init() -> void:
|
||||
_smoke_variant_placement(1)
|
||||
_smoke_variant_placement(2)
|
||||
_smoke_global_variant()
|
||||
_test_pack_min_from_table()
|
||||
quit(Kit.finish("variant_placement_test"))
|
||||
|
||||
|
||||
@@ -331,3 +332,36 @@ func _errs_has_cross_field(errs: Array) -> bool:
|
||||
# ——互锁负样本/门控三维/资源面偏斜守恒与合成序/快照投影不变)----
|
||||
|
||||
# 取号计数桩(variant_place 取号断言——子类拦截计数,派生与取数语义零改动)
|
||||
|
||||
|
||||
# ---- 成包下界读表(架构整改——pack_min 表头单点)----
|
||||
# 观测面局限申报(实现期裁定,详见实现报告/对照表):pack_min 消费面=randi_range 下界(软目标——
|
||||
# 8 邻贴接禁令下生长早停为合法态〔board_installer 注释明示〕,候选池碎片化使「产物零 <min 片」
|
||||
# 全称不可断)。可达判据=①静态(布置面零字面量下界)+②同种子改值产物互异(真实入口=begin_battle 布置段)。
|
||||
|
||||
func _test_pack_min_from_table() -> void:
|
||||
# ① 静态:布置面零字面量下界(randi_range(2, 退役——下界读 pack_min 表头单点)
|
||||
var src := FileAccess.get_file_as_string("res://core/board_installer.gd")
|
||||
Kit.check("布置面零字面量下界(randi_range(2, 退役、下界读表单点在位)〔架构整改 V10·静态〕",
|
||||
not src.contains("randi_range(2,") and src.contains("int(db.combat.variation.pack_min)"))
|
||||
# ② 同种子改值产物互异(扫种子取首个差异局——真实入口 begin_battle 布置段)
|
||||
var vt = _db.combat.variation
|
||||
var saved_min: int = int(vt.pack_min)
|
||||
var found_diff := false
|
||||
for s in range(42, 102):
|
||||
var sigs := {}
|
||||
for pm in [3, 1]:
|
||||
vt.pack_min = pm
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(s)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(_db, rng)
|
||||
if not settlement.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(_db.combat.hero.max_hp)}):
|
||||
continue
|
||||
var b = settlement.minesweeper.current_board()
|
||||
sigs[pm] = str(b.variants.keys())
|
||||
vt.pack_min = saved_min
|
||||
if String(sigs.get(3, "")) != "" and String(sigs.get(3, "")) != String(sigs.get(1, "")):
|
||||
found_diff = true
|
||||
break
|
||||
vt.pack_min = saved_min
|
||||
Kit.check("同种子 pack_min=3 vs 1 布置产物互异(下界进 randi_range——改值可观测)〔架构整改 V10〕", found_diff)
|
||||
@@ -25,6 +25,14 @@ func _init() -> void:
|
||||
_test_rng_tag_closure(db)
|
||||
_test_ticket_nine_columns()
|
||||
_test_status_boolean_acceptance(db)
|
||||
_test_status_individual_isolation(db)
|
||||
_test_status_grant_all_per_unit(db)
|
||||
_test_status_death_clears(db)
|
||||
_test_status_merge_per_unit(db)
|
||||
_test_status_decay_and_clear(db)
|
||||
_test_status_stack_full_reject(db)
|
||||
_test_status_independent_branch(db)
|
||||
_test_status_snapshot_first_grant(db)
|
||||
quit(Kit.finish("system_contracts_test"))
|
||||
|
||||
|
||||
@@ -112,13 +120,174 @@ func _test_status_boolean_acceptance(db) -> void:
|
||||
return
|
||||
var probe_id: String = String(any_row.effect_id)
|
||||
var src := {"source_class": "safe_cell", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, src) == false)
|
||||
Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", src) == false)
|
||||
Kit.check("域闭授予拒绝(EC1——战斗域外 apply_effect = false)", effects.apply_effect(probe_id, StatusContainerScript.OWNER_HERO, src) == false)
|
||||
Kit.check("未知 effect_id 拒绝(EC2)", effects.apply_effect("no_such_effect_id__", StatusContainerScript.OWNER_HERO, src) == false)
|
||||
effects.notify_battle_start()
|
||||
Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, src) == true)
|
||||
var active: Array = effects.query_active(String(any_row.target_side))
|
||||
Kit.check("开域后受理已知 id(布尔受理口径 true 通道)", effects.apply_effect(probe_id, StatusContainerScript.OWNER_HERO, src) == true)
|
||||
var active: Array = effects.query_active(StatusContainerScript.OWNER_HERO)
|
||||
var projected := false
|
||||
for a in active:
|
||||
if String(a.effect_id) == probe_id:
|
||||
projected = int(a.effective_value) == int(a.layers) * int(a.snap_magnitude)
|
||||
Kit.check("受理后投影 effective_value = 层数×授予时快照(抗热重载 EC9 面)", projected)
|
||||
|
||||
|
||||
# ---- B4 个体归属契约(增益状态 v1.5——敌方状态个体化批):个体隔离/全体逐体发/死亡即清/同体合并 ----
|
||||
# 个体键直传字面量("e1"/"e2"/"e3"——拼写约定单点在 StatusContainer.owner_key_for,测试面模拟授予路径逐只授)
|
||||
|
||||
func _test_status_individual_isolation(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken")
|
||||
Kit.check("前置:weaken 行在册(累加策略——个体合并语义载体)", not wrow.is_empty() and String(wrow.stack_policy) == "accumulate")
|
||||
Kit.check("个体隔离:A 敌受理", effects.apply_effect("weaken", "e1", src) == true)
|
||||
Kit.check("个体隔离:B 敌受理(同名不同体互不合并)", effects.apply_effect("weaken", "e2", src) == true)
|
||||
Kit.check("个体隔离:A/B 各持一条(各体桶独立)〔个体化批 V1〕",
|
||||
effects.query_active("e1").size() == 1 and effects.query_active("e2").size() == 1)
|
||||
effects.decay_round() # A/B 各自 −1——先验证互不干扰后验证独立倒数
|
||||
var a_after: Array = effects.query_active("e1")
|
||||
var b_after: Array = effects.query_active("e2")
|
||||
var a_rem: int = int(a_after[0].remaining_settlements) if a_after.size() == 1 else -1
|
||||
var b_rem: int = int(b_after[0].remaining_settlements) if b_after.size() == 1 else -1
|
||||
Kit.check("个体隔离:各体独立倒数(A=表值−1、B=表值−1 同步各减)〔个体化批 V1〕",
|
||||
a_rem == int(wrow.duration_settlements) - 1 and b_rem == int(wrow.duration_settlements) - 1)
|
||||
Kit.check("个体隔离:投影携带 owner_key(逐个体承载出户)〔个体化批 V1〕",
|
||||
a_after.size() == 1 and String(a_after[0].owner_key) == "e1" and String(b_after[0].owner_key) == "e2")
|
||||
|
||||
|
||||
func _test_status_grant_all_per_unit(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src := {"source_class": "mine_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage")
|
||||
Kit.check("前置:enrage 行在册(叠加策略——逐体叠层载体)", not erow.is_empty() and String(erow.stack_policy) == "stack")
|
||||
# 模拟授予路径全体规则(reward_grant._apply_percent_row 同构):循环三敌逐只各发一条
|
||||
var keys: Array[String] = ["e1", "e2", "e3"]
|
||||
var granted := 0
|
||||
for k in keys:
|
||||
if effects.apply_effect("enrage", k, src):
|
||||
granted += 1
|
||||
Kit.check("全体授予逐体发:三敌各受理一条(对全体=逐只各发)〔个体化批 V2〕", granted == 3)
|
||||
var e2_ok: bool = effects.apply_effect("enrage", "e2", src) # e2 再授一层——各体独立合并
|
||||
Kit.check("全体授予逐体发:同体叠加至 2 层、异体层数不动〔个体化批 V2〕",
|
||||
e2_ok and int(effects.query_active("e2")[0].layers) == 2
|
||||
and int(effects.query_active("e1")[0].layers) == 1 and int(effects.query_active("e3")[0].layers) == 1)
|
||||
effects.decay_round()
|
||||
var all_decayed := true
|
||||
for k in keys:
|
||||
for eff in effects.query_active(k):
|
||||
if int(eff.remaining_settlements) != int(erow.duration_settlements) - 1:
|
||||
all_decayed = false
|
||||
Kit.check("全体授予逐体发:各体同轮独立衰减 −1(逐桶各减)〔个体化批 V2〕", all_decayed)
|
||||
|
||||
|
||||
func _test_status_death_clears(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
effects.apply_effect("weaken", "e1", src)
|
||||
effects.apply_effect("weaken", "e2", src)
|
||||
Kit.check("死亡即清:purge_owner 返回移除数(e1 名下 1 条)〔个体化批 V3〕", effects.purge_owner("e1") == 1)
|
||||
Kit.check("死亡即清:e1 桶清零(重 purge 零移除)〔个体化批 V3〕", effects.query_active("e1").is_empty() and effects.purge_owner("e1") == 0)
|
||||
Kit.check("死亡即清:e2 不受影响(不转移不并回)〔个体化批 V3〕", effects.query_active("e2").size() == 1)
|
||||
var side_all: Array = effects.query_active_side("enemy")
|
||||
Kit.check("死亡即清:侧汇总只含存活个体实例〔个体化批 V3〕", side_all.size() == 1 and String(side_all[0].owner_key) == "e2")
|
||||
|
||||
|
||||
func _test_status_merge_per_unit(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src := {"source_class": "enemy_action", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken")
|
||||
var w_dur: int = int(wrow.duration_settlements)
|
||||
effects.apply_effect("weaken", "e1", src)
|
||||
effects.apply_effect("weaken", "e1", src)
|
||||
Kit.check("同体合并照旧:累加行同体再授时长累加(%d+%d——仍单实例)〔个体化批 V4〕" % [w_dur, w_dur],
|
||||
effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur * 2)
|
||||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage")
|
||||
var max_stack: int = int(erow.max_stack)
|
||||
var granted_n := 0
|
||||
for i in range(max_stack + 1):
|
||||
if effects.apply_effect("enrage", "e2", src):
|
||||
granted_n += 1
|
||||
Kit.check("同体合并照旧:叠加行满层拒绝(恰授 max_stack=%d 层)〔个体化批 V4〕" % max_stack, granted_n == max_stack)
|
||||
Kit.check("同体合并照旧:满层时异体仍受理(拒绝只在同体内原子)〔个体化批 V4〕", effects.apply_effect("enrage", "e3", src) == true)
|
||||
|
||||
|
||||
# ---- B4 结算链契约(断言补齐批——①增益状态结算链):衰减逐值/战斗结束全清/满层拒绝/独立并存/首授快照 ----
|
||||
|
||||
func _test_status_decay_and_clear(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var wrow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("weaken")
|
||||
var w_dur: int = int(wrow.duration_settlements)
|
||||
Kit.check("V1 前置:weaken 时长 ≥2(两拍衰减观测面)", w_dur >= 2)
|
||||
Kit.check("V1 前置:e1 受理", effects.apply_effect("weaken", "e1", src))
|
||||
Kit.check("衰减逐值:首次 decay 返回移除数 0(未归零)〔断言补齐批 V1〕", effects.decay_round() == 0)
|
||||
Kit.check("衰减逐值:每结算恰 −1(remaining=%d−1)〔断言补齐批 V1〕" % (w_dur - 1),
|
||||
effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].remaining_settlements) == w_dur - 1)
|
||||
Kit.check("到期移除:第二次 decay 返回移除数 1〔断言补齐批 V2〕", effects.decay_round() == 1)
|
||||
Kit.check("到期移除:归零实例当轮销毁(投影零行)〔断言补齐批 V2〕", effects.query_active("e1").is_empty())
|
||||
effects.apply_effect("weaken", "e2", src)
|
||||
effects.apply_effect("attack_up", StatusContainerScript.OWNER_HERO, src)
|
||||
Kit.check("V2 前置:两桶各持实例", effects.query_active_side("enemy").size() == 1 and effects.query_active(StatusContainerScript.OWNER_HERO).size() == 1)
|
||||
Kit.check("战斗结束全清:notify_battle_end 返回 true 且全实例清空(两桶零行)〔断言补齐批 V2〕",
|
||||
effects.notify_battle_end() and effects.query_active_side("enemy").is_empty() and effects.query_active(StatusContainerScript.OWNER_HERO).is_empty())
|
||||
|
||||
|
||||
func _test_status_stack_full_reject(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
var erow: Dictionary[Variant, Variant] = db.combat.status_effects.row_by_id("enrage")
|
||||
var max_stack: int = int(erow.max_stack)
|
||||
for i in range(max_stack):
|
||||
effects.apply_effect("enrage", "e1", src)
|
||||
Kit.check("V3 前置:满层(恰 %d 层)" % max_stack,
|
||||
effects.query_active("e1").size() == 1 and int(effects.query_active("e1")[0].layers) == max_stack)
|
||||
var before_layers: int = int(effects.query_active("e1")[0].layers)
|
||||
var before_rem: int = int(effects.query_active("e1")[0].remaining_settlements)
|
||||
Kit.check("满层拒绝:再授返 false〔断言补齐批 V3〕", effects.apply_effect("enrage", "e1", src) == false)
|
||||
Kit.check("满层拒绝:层数与剩余零写入(容器逐字不变——原子拒绝)〔断言补齐批 V3〕",
|
||||
int(effects.query_active("e1")[0].layers) == before_layers and int(effects.query_active("e1")[0].remaining_settlements) == before_rem)
|
||||
|
||||
|
||||
func _test_status_independent_branch(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var probe := {
|
||||
"effect_id": "probe_independent_v4",
|
||||
"display_name": "并存试验",
|
||||
"target_side": "enemy",
|
||||
"magnitude": 7,
|
||||
"unit": "flat",
|
||||
"duration_settlements": 2,
|
||||
"stack_policy": "independent",
|
||||
"max_stack": 1,
|
||||
}
|
||||
db.combat.status_effects.rows.append(probe) # 直改已加载表(运行时注入试验行——并存分支零内容行,夹具面)
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
Kit.check("independent 并存:第一次授予受理〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src))
|
||||
Kit.check("independent 并存:第二次授予受理(并存——无合并无满层语义)〔断言补齐批 V4〕", effects.apply_effect("probe_independent_v4", "e1", src))
|
||||
var both: Array = effects.query_active("e1")
|
||||
Kit.check("independent 并存:两实例各持时长 2 独立倒数〔断言补齐批 V4〕",
|
||||
both.size() == 2 and int(both[0].remaining_settlements) == 2 and int(both[1].remaining_settlements) == 2)
|
||||
db.combat.status_effects.rows.pop_back() # 测毕还原(表行零残留)
|
||||
Kit.check("independent 试验行测毕还原(row_by_id 零命中)", db.combat.status_effects.row_by_id("probe_independent_v4").is_empty())
|
||||
|
||||
|
||||
func _test_status_snapshot_first_grant(db) -> void:
|
||||
var effects = StatusContainerScript.new(db)
|
||||
effects.notify_battle_start()
|
||||
var src_first := {"source_class": "seal_hit", "origin_cell": Vector2i(3, 4), "round_index": 1}
|
||||
var src_second := {"source_class": "mine_hit", "origin_cell": Vector2i(8, 9), "round_index": 5}
|
||||
effects.apply_effect("weaken", "e1", src_first)
|
||||
effects.apply_effect("weaken", "e1", src_second) # 累加:时长加、归因与快照沿首授
|
||||
var inst: Array = effects.query_active("e1")
|
||||
Kit.check("首授快照:累加合并仍单实例〔断言补齐批 V5〕", inst.size() == 1)
|
||||
Kit.check("首授快照:origin_cell/round_index/source_class 沿首授(合并实例只持首授归因)〔断言补齐批 V5〕",
|
||||
String(inst[0].source_class) == "seal_hit" and inst[0].origin_cell == Vector2i(3, 4) and int(inst[0].round_index) == 1)
|
||||
Kit.check("首授快照:幅度快照 = 首授时表值(抗热重载域)〔断言补齐批 V5〕",
|
||||
int(inst[0].snap_magnitude) == int(db.combat.status_effects.row_by_id("weaken").magnitude))
|
||||
@@ -589,7 +589,7 @@ func _smoke_feats() -> void:
|
||||
var sv_c = h.get("set", null)
|
||||
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)) if sv_c == null else int(sv_c), sv_c != null)
|
||||
# 开格系
|
||||
fx.rebuild(["twin_pick"], [], [])
|
||||
fx.rebuild(["twin_pick"], [], {})
|
||||
for i in 4:
|
||||
var hits_dp: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1})
|
||||
var refund := false
|
||||
@@ -601,7 +601,7 @@ func _smoke_feats() -> void:
|
||||
Kit.check("双管镐:第 %d 次付费挖格返 1 AP(每第 2 次——mod_eq 预评估口径)" % (i + 1), refund)
|
||||
else:
|
||||
Kit.check("双管镐反例:第 %d 次不返(未满 2 次)" % (i + 1), not refund)
|
||||
fx.rebuild(["geologist"], [], [])
|
||||
fx.rebuild(["geologist"], [], {})
|
||||
var geo_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": true})
|
||||
apply_fx.call(geo_hits)
|
||||
Kit.check("地质学家:首开新类型集 1 点〔专长批 V3〕", _hits_kind(geo_hits, "counter_add").size() == 1)
|
||||
@@ -611,7 +611,7 @@ func _smoke_feats() -> void:
|
||||
Kit.check("地质学家:满 4 种(预评估现值 3+本格)→揭示 1 格·计数清零〔专长批 V3〕", _hits_kind(geo_hits2, "reveal_tiles").size() == 1 and EffectEngine.counter(orch.fx_host().counters_ctx, "geo_types") == 0)
|
||||
var geo_hits3: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"new_resource_type": false})
|
||||
Kit.check("地质学家反例:同类型重复首开不叠点〔专长批 V3〕", _hits_kind(geo_hits3, "counter_add").is_empty())
|
||||
fx.rebuild(["hard_hat"], [], [])
|
||||
fx.rebuild(["hard_hat"], [], {})
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}))
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}))
|
||||
Kit.check("安全帽:开安全格攒层 ×2〔专长批 V3〕", EffectEngine.counter(orch.fx_host().counters_ctx, "helm_layers") == 2)
|
||||
@@ -621,7 +621,7 @@ func _smoke_feats() -> void:
|
||||
if String(h.get("kind", "")) == "damage_reduce":
|
||||
hh_reduce = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
|
||||
Kit.check("安全帽:踩雷耗层减伤=层值 2(一次清零随施加面 set 行)〔专长批 V3〕", hh_reduce == 2)
|
||||
fx.rebuild(["skip_mining"], [], [])
|
||||
fx.rebuild(["skip_mining"], [], {})
|
||||
var sk_zero: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": true})
|
||||
apply_fx.call(sk_zero)
|
||||
Kit.check("跳格采掘:空白 0 格攒 1 点〔专长批 V3〕", _hits_kind(sk_zero, "counter_add").size() == 1)
|
||||
@@ -630,24 +630,24 @@ func _smoke_feats() -> void:
|
||||
apply_fx.call(sk_full)
|
||||
Kit.check("跳格采掘:满 3 点→揭示 1 格〔专长批 V3〕", _hits_kind(sk_full, "reveal_tiles").size() == 1)
|
||||
Kit.check("跳格采掘反例:普通开格不计点〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_blank_zero": false}), "counter_add").is_empty())
|
||||
fx.rebuild(["blast_accounting"], [], [])
|
||||
fx.rebuild(["blast_accounting"], [], {})
|
||||
for i in 5:
|
||||
var ba_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_paid", {"ap_cost": 1})
|
||||
for h in ba_hits:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1)
|
||||
Kit.check("爆破会计:本轮挖格花费≥5→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
|
||||
fx.rebuild(["frontier_miner"], [], [])
|
||||
fx.rebuild(["frontier_miner"], [], {})
|
||||
Kit.check("边陲矿工:边缘格产出 +1·中心格反例零产出〔专长批 V3〕",
|
||||
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": true}), "grant_material").size() == 1
|
||||
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {"is_edge_cell": false}), "grant_material").is_empty())
|
||||
# 封印系
|
||||
fx.rebuild(["seal_echo"], [], [])
|
||||
fx.rebuild(["seal_echo"], [], {})
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}))
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}))
|
||||
var se_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_end", {})
|
||||
Kit.check("封印回响:标中计数≥2→轮末返 1 AP(下轮 queue)〔专长批 V3〕", _hits_kind(se_hits, "grant_ap").size() == 1 and String((_hits_kind(se_hits, "grant_ap")[0] as Dictionary).get("mode", "")) == "queue")
|
||||
fx.rebuild(["seal_smith"], [], [])
|
||||
fx.rebuild(["seal_smith"], [], {})
|
||||
var sm_remove := 0
|
||||
for i in 3:
|
||||
var sm_dispatch: Array = fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {})
|
||||
@@ -656,14 +656,14 @@ func _smoke_feats() -> void:
|
||||
if String(h.get("kind", "")) == "remove_mines":
|
||||
sm_remove += 1
|
||||
Kit.check("封印匠:标中每第 3 次→中立移除恰一次(频率闸死)〔专长批 V3〕", sm_remove == 1)
|
||||
fx.rebuild(["seal_stake"], [], [])
|
||||
fx.rebuild(["seal_stake"], [], {})
|
||||
Kit.check("封印桩:标中本轮 AC +N(round 域——round_start 清零)〔专长批 V3〕",
|
||||
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus").size() == 1
|
||||
and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "ac_bonus")[0] as Dictionary).get("mode", "")) == "round")
|
||||
fx.rebuild(["warning_beacon"], [], [])
|
||||
fx.rebuild(["warning_beacon"], [], {})
|
||||
Kit.check("示警烟火:标中揭示 3×3 资源类型(不揭安危位置)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "preview_area").size() == 1)
|
||||
# 踩雷/受伤系
|
||||
fx.rebuild(["pain_memory"], [], [])
|
||||
fx.rebuild(["pain_memory"], [], {})
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
|
||||
var pm_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
@@ -671,7 +671,7 @@ func _smoke_feats() -> void:
|
||||
for h in orch._hits_of(pm_hits, "attack_bonus"):
|
||||
pm_bonus = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
|
||||
Kit.check("疼痛记忆:攻击附加层值伤害=层值 2·用后清零(set 行随施加面)〔专长批 V3〕", pm_bonus == 2)
|
||||
fx.rebuild(["fuse_wire"], [], [])
|
||||
fx.rebuild(["fuse_wire"], [], {})
|
||||
var fw_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "terrain", "amount": 6})
|
||||
var fw_shield := 0
|
||||
for h in fw_hits:
|
||||
@@ -682,43 +682,43 @@ func _smoke_feats() -> void:
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), int(h.get("value", 0)))
|
||||
Kit.check("保险丝:首点不出盾(2 点才出——预评估口径)〔专长批 V3〕", fw_shield == 0)
|
||||
fx.rebuild(["blood_insight"], [], [])
|
||||
fx.rebuild(["blood_insight"], [], {})
|
||||
Kit.check("见血开窍:踩雷揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "reveal_tiles").size() == 1)
|
||||
fx.rebuild(["trap_instinct"], [], [])
|
||||
fx.rebuild(["trap_instinct"], [], {})
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
|
||||
var ti_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0})
|
||||
var ti_val := 0
|
||||
for h in orch._hits_of(ti_hits, "attack_bonus"):
|
||||
ti_val = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
|
||||
Kit.check("陷阱本能:踩雷本轮攻击 +2(attack_bonus 通道——面板加算)〔专长批 V3〕", ti_val == 2)
|
||||
fx.rebuild(["chain_traps", "pain_memory"], [], [])
|
||||
fx.rebuild(["chain_traps", "pain_memory"], [], {})
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
|
||||
var ct_before: int = EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers")
|
||||
apply_fx.call(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}))
|
||||
Kit.check("连环陷阱:同轮第 2 颗起每颗额外 +1 层(疼痛记忆同账)〔专长批 V3〕", EffectEngine.counter(orch.fx_host().counters_ctx, "pain_layers") == ct_before + 2)
|
||||
fx.rebuild(["trap_burst"], [], [])
|
||||
fx.rebuild(["trap_burst"], [], {})
|
||||
var tb_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"damage": 8})
|
||||
Kit.check("陷阱爆发:全体敌雷伤×倍率直伤(blast_scaled——施加面值=damage×mult)〔专长批 V3〕",
|
||||
_hits_kind(tb_hits, "blast_scaled").size() == 1
|
||||
and float((_hits_kind(tb_hits, "blast_scaled")[0] as Dictionary).get("mult", 0)) > 1.0)
|
||||
fx.rebuild(["blood_honey"], [], [])
|
||||
fx.rebuild(["blood_honey"], [], {})
|
||||
Kit.check("血蜜:踩雷回复 N 生命〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "heal").size() == 1)
|
||||
fx.rebuild(["stone_skin"], [], [])
|
||||
fx.rebuild(["stone_skin"], [], {})
|
||||
var ss_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1})
|
||||
Kit.check("石肤:受伤 AC+1(本场域 cap 3——达上限不再涨由累积器封顶)〔专长批 V3〕",
|
||||
_hits_kind(ss_hits, "ac_bonus").size() == 1
|
||||
and int((_hits_kind(ss_hits, "ac_bonus")[0] as Dictionary).get("cap", 0)) == 3)
|
||||
fx.rebuild(["last_stand"], [], [])
|
||||
fx.rebuild(["last_stand"], [], {})
|
||||
Kit.check("背水:生命≤50% 减伤·过半血反例零减伤〔专长批 V3〕",
|
||||
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 1, "hp_ratio": 40}), "damage_reduce").size() == 1
|
||||
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "damage_taken", {"kind": "enemy_skill", "amount": 5, "source": "s", "round": 2, "hp_ratio": 80}), "damage_reduce").is_empty())
|
||||
# 跨族桥
|
||||
fx.rebuild(["breaker_strike"], [], [])
|
||||
fx.rebuild(["breaker_strike"], [], {})
|
||||
Kit.check("破封一击:挖开带封印危险格 +1 层·无封印反例零触发〔专长批 V3〕",
|
||||
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": true}), "counter_add").size() == 1
|
||||
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {"dug_under_seal": false}), "counter_add").is_empty())
|
||||
# 节奏系
|
||||
fx.rebuild(["map_exchange"], [], [])
|
||||
fx.rebuild(["map_exchange"], [], {})
|
||||
var me_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 4})
|
||||
apply_fx.call(me_hits)
|
||||
EffectEngine.counter_add(orch.fx_host().counters_ctx, "stop_swap", 4) # value_event="ap_left" 解析归施加面(action_economy 载荷取值)——冒烟手动落账
|
||||
@@ -728,41 +728,41 @@ func _smoke_feats() -> void:
|
||||
me_atk = EffectEngine.counter(orch.fx_host().counters_ctx, String(h.get("value_counter", "")))
|
||||
Kit.check("弃图兑换:折为攻击伤害=剩余 AP 全额·用后清零〔专长批 V3〕", me_atk == 4)
|
||||
Kit.check("弃图兑换反例:剩余 AP<3 不触发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "dig_stop", {"ap_left": 2}), "counter_add").is_empty())
|
||||
fx.rebuild(["last_drop"], [], [])
|
||||
fx.rebuild(["last_drop"], [], {})
|
||||
var ld_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 2, "ap_exhausted_last": true})
|
||||
Kit.check("一滴不剩:上轮 AP 耗尽→+1 AP〔专长批 V3〕", _hits_kind(ld_hits, "grant_ap").size() == 1)
|
||||
Kit.check("一滴不剩反例:上轮有余不发〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_start", {"round": 3, "ap_exhausted_last": false}), "grant_ap").is_empty())
|
||||
fx.rebuild(["market_close"], [], [])
|
||||
fx.rebuild(["market_close"], [], {})
|
||||
for i in 8:
|
||||
for h in fx.dispatch(orch.fx_host().counters_ctx, "dig_reveal", {}):
|
||||
if String(h.get("kind", "")) == "counter_add":
|
||||
EffectEngine.counter_add(orch.fx_host().counters_ctx, String(h.get("counter_id", "")), 1)
|
||||
Kit.check("收盘:本轮开格≥8→+1 GP〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "round_end", {}), "grant_gp").size() == 1)
|
||||
# 战斗系
|
||||
fx.rebuild(["execution_scholar"], [], [])
|
||||
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌", "overflow": 0}), "reveal_tiles").size() == 1)
|
||||
fx.rebuild(["executioner"], [], [])
|
||||
fx.rebuild(["execution_scholar"], [], {})
|
||||
Kit.check("斩首学者:击杀揭示 1 格〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "kill", {"target": "敌"}), "reveal_tiles").size() == 1)
|
||||
fx.rebuild(["executioner"], [], {})
|
||||
var ex_hits: Array = fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 30, "is_boss": false})
|
||||
Kit.check("处刑台:敌血量≤30% 挂单位标记加伤·阈值外反例零命中〔专长批 V3〕",
|
||||
_hits_kind(ex_hits, "target_attack_bonus").size() == 1
|
||||
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "enemy_hp_threshold", {"unit": "e1", "pct": 60, "is_boss": false}), "target_attack_bonus").is_empty())
|
||||
fx.rebuild(["field_dressing"], [], [])
|
||||
fx.rebuild(["field_dressing"], [], {})
|
||||
Kit.check("战地包扎:战斗胜利回复 N 生命(battle_won 新挂点)〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "battle_won", {"round": 2}), "heal").size() == 1)
|
||||
# 契约尖点
|
||||
fx.rebuild(["blood_tax"], [], [])
|
||||
fx.rebuild(["blood_tax"], [], {})
|
||||
Kit.check("血税:双向同局账本(踩陷阱−2max_hp/封印命中+1atk——bonus_delta 战末汇入)〔专长批 V3〕",
|
||||
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "mine_hit", {}), "bonus_delta").size() == 1
|
||||
and String((_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "bonus_delta")[0] as Dictionary).get("axis", "")) == "atk")
|
||||
# 迁入 2 件
|
||||
fx.rebuild(["venom_edge"], [], [])
|
||||
fx.rebuild(["venom_edge"], [], {})
|
||||
Kit.check("淬毒刃纹(迁入):克制正面(magic_vs_heavy 矩阵值>0)追加·中性面反例零触发〔专长批 V3〕",
|
||||
_hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "magic_vs_heavy"}), "modify_ticket").size() == 1
|
||||
and _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "clamp_check", {"matchup": "normal_vs_light"}), "modify_ticket").is_empty())
|
||||
fx.rebuild(["seal_burst"], [], [])
|
||||
fx.rebuild(["seal_burst"], [], {})
|
||||
Kit.check("封印爆裂(迁入):标中全体敌群伤〔专长批 V3〕", _hits_kind(fx.dispatch(orch.fx_host().counters_ctx, "seal_hit", {}), "open_ticket").size() == 1)
|
||||
# 零持有
|
||||
var orch0 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(74))
|
||||
orch0.fx_host().rebuild_subscriptions([], [], [])
|
||||
orch0.fx_host().rebuild_subscriptions([], [], {})
|
||||
Kit.check("零持有订阅=零命中(relic 关着零扰动——伤害管线既有行为逐位不变)",
|
||||
orch0.fx_host().fx_emit("attack_commit", {"atk": 10, "target": "敌", "matchup": "", "dig_streak": 0, "nest_count": 0}).is_empty()
|
||||
and orch0.fx_host().fx_emit("dig_reveal", {"cell": Vector2i(1, 1), "round": 1, "is_first_dig_of_battle": true, "terrain_kind": []}).is_empty()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# tests/unit/core/auto_player_deduce_test.gd — 模拟玩家 0 格连锁算子〔架构整改 V15〕
|
||||
# 断言面 = AutoPlayer._deduce 补齐后:safe 集含「与所有已开格 8 邻不接」的远端格(该形态格
|
||||
# single/subset 约束不可达——唯一来源=0 格连锁经 preview_reveal 真实闭包);零盲挖语义回归
|
||||
# (mines 集零安全格)。真实入口=AutoPlayer._deduce(真板真流——begin_battle 装配链)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/auto_player_deduce_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const AutoPlayer := preload("res://sim/auto_player.gd")
|
||||
const BattleDriver := preload("res://sim/battle_driver.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_smoke_zero_chain_rule()
|
||||
quit(Kit.finish("auto_player_deduce_test"))
|
||||
|
||||
|
||||
func _smoke_zero_chain_rule() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0
|
||||
Kit.check("前置:表加载零 error", load_ok)
|
||||
if not load_ok:
|
||||
return
|
||||
var found := false
|
||||
var found_seed := -1
|
||||
var remote_ok := false
|
||||
var mines_clean := false
|
||||
for s in range(42, 142):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(s)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
if not settlement.begin_battle({"level_index": 2, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}):
|
||||
continue
|
||||
var ms = settlement.minesweeper
|
||||
BattleDriver.drive_round(settlement) # 真实挖一轮(0 连锁实挖触发——信息面展开)
|
||||
if ms.current_board() == null:
|
||||
continue
|
||||
var dd: Dictionary = AutoPlayer._deduce(ms)
|
||||
var safe: Array = dd["safe"]
|
||||
var mines: Array = dd["mines"]
|
||||
# 远端格:safe 中与所有已开格 8 邻距离 >1(single/subset 约束只覆盖已开数字格邻域——
|
||||
# 此形态格唯一来源=0 格连锁算子)
|
||||
var revealed: Array = ms.revealed_cells()
|
||||
var remote := false
|
||||
for rc_v in safe:
|
||||
var rc: Vector2i = rc_v
|
||||
var touches := false
|
||||
for rv in revealed:
|
||||
var d: Vector2i = rc - rv
|
||||
if maxi(absi(d.x), absi(d.y)) <= 1:
|
||||
touches = true
|
||||
break
|
||||
if not touches:
|
||||
remote = true
|
||||
break
|
||||
# 零盲挖:mines 集全落真雷(板面雷集)
|
||||
var board = ms.current_board()
|
||||
var clean := true
|
||||
for mv in mines:
|
||||
if not board.is_mine(mv):
|
||||
clean = false
|
||||
if remote and clean:
|
||||
found = true
|
||||
found_seed = s
|
||||
remote_ok = true
|
||||
mines_clean = true
|
||||
break
|
||||
Kit.check("0 格连锁种子可达(远端 safe 形态在案,seed=%d)〔架构整改 V15〕" % found_seed, found)
|
||||
Kit.check("远端格入 safe 集(桥接 0 区域传播——改前约束模型必缺)〔架构整改 V15〕", remote_ok)
|
||||
Kit.check("零盲挖语义保持(mines 集零安全格)〔架构整改 V15〕", mines_clean)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cggeuptv8skhl
|
||||
@@ -0,0 +1,150 @@
|
||||
# tests/unit/core/battle_driver_drive_test.gd — drive_round 驱动循环契约〔架构整改 V13〕
|
||||
# 断言面 = BattleDriver.drive_round 通用签名四形态:
|
||||
# ① 缺省两参(真编排器同种子两跑逐位——确定性自证;四调用方零改动回归由全套承担);
|
||||
# ② provider 形态(桩 settlement——策略回调产出 dig/seal_place/refresh 各一、日志回调收四元组);
|
||||
# ③ max_rejects(策略恒返被拒动作——第 3 拒后停手出环);
|
||||
# ④ 合步 next(move 后相位变更即断弃 dig)。
|
||||
# 桩形态沿 seal_polarity_test 状态袋先例(手搭 settlement——引用步骤不复制代码)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/battle_driver_drive_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const BattleDriver := preload("res://sim/battle_driver.gd")
|
||||
|
||||
const Vector2iArray := []
|
||||
|
||||
|
||||
class _StubEcon:
|
||||
var phase := "Active"
|
||||
|
||||
|
||||
class _StubMS:
|
||||
var log: Array = []
|
||||
var accept := true
|
||||
var reject_cells := {}
|
||||
var on_submit: Callable = Callable() # 提交后钩子(④ 相位变更注入)
|
||||
|
||||
func settle_batch(actions: Array) -> Array:
|
||||
var out: Array = []
|
||||
for a in actions:
|
||||
var cell: Vector2i = a.get("cell", Vector2i(-1, -1))
|
||||
var ok := accept and not reject_cells.has(cell)
|
||||
log.append({"action": String(a.get("action", "")), "cell": cell, "ok": ok})
|
||||
out.append({"accepted": ok, "outcome": {}, "reject_reason": "" if ok else "stub_reject"})
|
||||
if on_submit.is_valid():
|
||||
on_submit.call(String(a.get("action", "")))
|
||||
return out
|
||||
|
||||
|
||||
class _StubSettlement:
|
||||
var round_index := 1
|
||||
var battle_result := ""
|
||||
var action_economy := _StubEcon.new()
|
||||
var minesweeper := _StubMS.new()
|
||||
var refresh_calls := 0
|
||||
|
||||
func player_request_refresh() -> bool:
|
||||
refresh_calls += 1
|
||||
return true
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_smoke_drive_round_contract()
|
||||
quit(Kit.finish("battle_driver_drive_test"))
|
||||
|
||||
|
||||
# ① 缺省两参(真编排器):同种子两跑整轮驱动逐位(确定性自证——动作序列快照经 cell_revealed 事件流)
|
||||
func _smoke_drive_round_contract() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
var load_ok: bool = db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0
|
||||
Kit.check("前置:表加载零 error", load_ok)
|
||||
if not load_ok:
|
||||
return
|
||||
var sigs: Array = []
|
||||
for run in range(2):
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(777)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
if not settlement.begin_battle({"level_index": 1, "node_depth": 0, "hero_hp": int(db.combat.hero.max_hp)}):
|
||||
Kit.check("前置:开局受理(run %d)" % run, false)
|
||||
return
|
||||
var moves: Array = []
|
||||
settlement.minesweeper.cell_revealed.connect(func(evt): moves.append(str(evt.get("cell", "")) + ":" + String(evt.get("kind", ""))))
|
||||
BattleDriver.drive_round(settlement)
|
||||
sigs.append(str(moves))
|
||||
Kit.check("缺省两参:真编排器同种子两跑事件流逐位(驱动语义确定性自证)〔架构整改 V13〕", sigs[0] == sigs[1] and String(sigs[0]) != "")
|
||||
_provider_shapes()
|
||||
_max_rejects()
|
||||
_combined_step_next()
|
||||
|
||||
|
||||
# ② provider 形态:策略回调产出 dig/seal_place/refresh 各一——提交与日志回调四元组在案
|
||||
func _provider_shapes() -> void:
|
||||
var st := _StubSettlement.new()
|
||||
var script := [ {"kind": "dig", "cell": Vector2i(1, 1)}, {"kind": "seal", "cell": Vector2i(2, 2)}, {"kind": "refresh"}, {"kind": "stop"} ]
|
||||
var logs: Array = []
|
||||
var provider := func(_s) -> Dictionary:
|
||||
return script.pop_front() if script.size() > 0 else {"kind": "stop"}
|
||||
var logger := func(kind: String, cell: Vector2i, ok: bool, ri: int) -> void:
|
||||
logs.append({"kind": kind, "cell": cell, "ok": ok, "ri": ri})
|
||||
BattleDriver.drive_round(st, Callable(), provider, logger, 0)
|
||||
Kit.check("provider 形态:dig 提交在案(桩日志 action=dig cell=(1,1))〔架构整改 V13〕",
|
||||
logs.size() >= 1 and String(logs[0]["kind"]) == "dig" and logs[0]["cell"] == Vector2i(1, 1) and bool(logs[0]["ok"]))
|
||||
Kit.check("provider 形态:seal 提交动作名 seal_place(提交分派换名)〔架构整改 V13〕",
|
||||
logs.size() >= 2 and String(logs[1]["kind"]) == "seal_place" and logs[1]["cell"] == Vector2i(2, 2))
|
||||
Kit.check("provider 形态:refresh 经 player_request_refresh(计数面外——桩 refresh_calls=1)+回调四元组 round_index 同轮〔架构整改 V13〕",
|
||||
st.refresh_calls == 1 and logs.size() >= 3 and String(logs[2]["kind"]) == "refresh" and int(logs[2]["ri"]) == 1)
|
||||
Kit.check("provider 形态:轮末 stop 统一回调(submit_kind=stop)〔架构整改 V13〕",
|
||||
logs.size() >= 4 and String(logs[logs.size() - 1]["kind"]) == "stop")
|
||||
|
||||
|
||||
# ③ max_rejects:策略恒返被拒动作——第 3 拒后停手出环(循环次数=3)
|
||||
func _max_rejects() -> void:
|
||||
var st := _StubSettlement.new()
|
||||
st.minesweeper.accept = false
|
||||
st.minesweeper.reject_cells = {Vector2i(9, 9): true}
|
||||
var provider := func(_s) -> Dictionary:
|
||||
return {"kind": "dig", "cell": Vector2i(9, 9)}
|
||||
var logs: Array = []
|
||||
var logger := func(kind: String, _cell: Vector2i, _ok: bool, _ri: int) -> void:
|
||||
logs.append(kind)
|
||||
BattleDriver.drive_round(st, Callable(), provider, logger, 3)
|
||||
var dig_rejects := 0
|
||||
for k in logs:
|
||||
if String(k) == "dig":
|
||||
dig_rejects += 1
|
||||
Kit.check("max_rejects=3:第 3 拒后停手出环(被拒 dig 日志恰 3 行——轮末 stop 另行在案)〔架构整改 V13〕", dig_rejects == 3)
|
||||
Kit.check("max_rejects=3:轮末 stop 回调在案〔架构整改 V13〕", logs.size() >= 1 and String(logs[logs.size() - 1]) == "stop")
|
||||
|
||||
|
||||
# ④ 合步 next:move 后相位变更即断弃 dig(桩 on_submit 钩子把 phase 推离 Active)
|
||||
func _combined_step_next() -> void:
|
||||
var st := _StubSettlement.new()
|
||||
st.minesweeper.on_submit = func(action: String) -> void:
|
||||
if action == "move":
|
||||
st.action_economy.phase = "Settling" # 移位触发结算相位变更
|
||||
var provider := func(_s) -> Dictionary:
|
||||
return {"kind": "move", "cell": Vector2i(3, 3), "next": {"kind": "dig", "cell": Vector2i(3, 4)}}
|
||||
BattleDriver.drive_round(st, Callable(), provider, Callable(), 0)
|
||||
var dig_count := 0
|
||||
for entry in st.minesweeper.log:
|
||||
if String(entry["action"]) == "dig":
|
||||
dig_count += 1
|
||||
Kit.check("合步 next:move 后相位变更断弃 dig(提交日志零 dig——与原内联逐位同)〔架构整改 V13〕", dig_count == 0)
|
||||
# 对照:相位不变时合步 dig 照常提交
|
||||
var st2 := _StubSettlement.new()
|
||||
var fired := {"once": false}
|
||||
var provider2 := func(_s) -> Dictionary:
|
||||
if bool(fired["once"]):
|
||||
return {"kind": "stop"}
|
||||
fired["once"] = true
|
||||
return {"kind": "move", "cell": Vector2i(3, 3), "next": {"kind": "dig", "cell": Vector2i(3, 4)}}
|
||||
BattleDriver.drive_round(st2, Callable(), provider2, Callable(), 0)
|
||||
var dig2 := 0
|
||||
var move2 := 0
|
||||
for entry in st2.minesweeper.log:
|
||||
if String(entry["action"]) == "dig":
|
||||
dig2 += 1
|
||||
if String(entry["action"]) == "move":
|
||||
move2 += 1
|
||||
Kit.check("合步 next:相位不变时 dig 照常提交(move+dig 两行)〔架构整改 V13〕", dig2 == 1 and move2 == 1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://dj6k6mmyk26pc
|
||||
@@ -130,6 +130,7 @@ func _smoke_reward_identity_unit() -> void:
|
||||
class _BoardRowStub:
|
||||
class _StubBoardGen:
|
||||
var row: Dictionary
|
||||
var spawn_edge: int = 3 # 表头成员(generate 直索引面——现行边长 3)
|
||||
func resolve_row(_L: int, _D: int) -> Dictionary:
|
||||
return row
|
||||
class _StubCombat:
|
||||
@@ -200,22 +201,23 @@ func _smoke_board_generator() -> void:
|
||||
# ③ ring_removed 量级:≤ 域内候选危险格数(5×5−出生 9=16——无全域剃雷);
|
||||
# ④ 闸压线行为:cap=9(出生保障面下限)时出图必 r0≤9 或 accept_worst 非法标记(三分出口诚实降级,玩家永不卡死)
|
||||
var cap_val: int = int(row.get("r0_max_cap", 0))
|
||||
Kit.check("L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 9——板面 30%% 锚)" % [L, cap_val],
|
||||
cap_val >= BoardData.SPAWN_AREA_CELLS)
|
||||
var spawn_cells_n: int = int(_db.combat.board_gen.spawn_edge) * int(_db.combat.board_gen.spawn_edge)
|
||||
Kit.check("L%d r0_max_cap 正式行投值在位(%d ≥ 出生保障面 %d——板面 30%% 锚)" % [L, cap_val, spawn_cells_n],
|
||||
cap_val >= spawn_cells_n)
|
||||
Kit.check("L%d 结构断言 R₀=%d ≤ cap=%d(R₀≤cap 重掷闸承载)" % [L, int(b1.metrics.r0_count), cap_val],
|
||||
int(b1.metrics.r0_count) <= cap_val)
|
||||
Kit.check("L%d r0_cap_rejected 尝试事实指标在位(本图 %d 次触发)" % [L, int(b1.metrics.get("r0_cap_rejected", -1))],
|
||||
int(b1.metrics.get("r0_cap_rejected", -1)) >= 0)
|
||||
Kit.check("L%d ring_removed=%d ≤ 剃雷域候选危险格数((3+2r)²−9——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99))],
|
||||
int(b1.metrics.get("ring_removed", 99)) <= (2 + int(row.get("shave_ring_radius", 1))) * (2 + int(row.get("shave_ring_radius", 1))) * 4 - 5)
|
||||
Kit.check("L%d ring_removed=%d ≤ 剃雷域候选危险格数((edge+2r)²−edge²——全域剃雷退役)" % [L, int(b1.metrics.get("ring_removed", 99))],
|
||||
int(b1.metrics.get("ring_removed", 99)) <= (int(_db.combat.board_gen.spawn_edge) + 2 * int(row.get("shave_ring_radius", 1))) * (int(_db.combat.board_gen.spawn_edge) + 2 * int(row.get("shave_ring_radius", 1))) - int(_db.combat.board_gen.spawn_edge) * int(_db.combat.board_gen.spawn_edge))
|
||||
var row9: Dictionary = row.duplicate()
|
||||
row9["r0_max_cap"] = BoardData.SPAWN_AREA_CELLS
|
||||
row9["r0_max_cap"] = int(_db.combat.board_gen.spawn_edge) * int(_db.combat.board_gen.spawn_edge) # cap 压线=出生保障面(spawn_edge² 推导——语义 9 不减)
|
||||
row9["max_gen_attempts"] = 4
|
||||
var stub9 := _BoardRowStub.new()
|
||||
stub9.combat.board_gen.row = row9
|
||||
var b9 = board_generator.generate(1, 0, _mk_stream("board_gen", 909), stub9)
|
||||
Kit.check("L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS else ">9→illegal_product"],
|
||||
b9 != null and (int(b9.metrics.r0_count) <= BoardData.SPAWN_AREA_CELLS or b9.illegal_product))
|
||||
Kit.check("L1 cap 压线 9:出图 R₀=%d %s(三分出口诚实降级——超阈即 accept_worst 非法标记,玩家永不卡死)" % [int(b9.metrics.r0_count), "≤9" if int(b9.metrics.r0_count) <= spawn_cells_n else ">9→illegal_product"],
|
||||
b9 != null and (int(b9.metrics.r0_count) <= spawn_cells_n or b9.illegal_product))
|
||||
Kit.check("L1 cap 压线出图指标集完整(accept_mode=%s / 闸触发 %d 次)" % [String(b9.metrics.get("accept_mode", "")), int(b9.metrics.get("r0_cap_rejected", 0))],
|
||||
int(b9.metrics.get("r0_cap_rejected", 0)) >= 0 and String(b9.metrics.get("accept_mode", "")) != "")
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const BoardGeneratorScript := preload("res://core/board_generator.gd")
|
||||
const BoardDataScript := preload("res://core/board_data.gd")
|
||||
const RngServiceScript := preload("res://core/rng_service.gd")
|
||||
|
||||
var _db: ConfigDb
|
||||
@@ -19,6 +20,7 @@ func _init() -> void:
|
||||
return
|
||||
_test_generate_shape_and_mines()
|
||||
_test_target_mine_count_single_source()
|
||||
_test_spawn_edge_from_table()
|
||||
_test_digits_equal_neighbor_mine_count()
|
||||
_test_same_seed_bit_identical()
|
||||
_test_clearable_and_inferability_selfconsistent()
|
||||
@@ -66,9 +68,11 @@ func _test_target_mine_count_single_source() -> void:
|
||||
var params := {
|
||||
"w": int(row.board_width), "h": int(row.board_height),
|
||||
"density": float(row.mine_density),
|
||||
"spawn_edge": int(_db.combat.board_gen.spawn_edge),
|
||||
}
|
||||
var from_fn: int = BoardGeneratorScript.target_mine_count(params)
|
||||
var from_expr: int = int(round(float(params.density) * float(params.w * params.h - BoardData.SPAWN_AREA_CELLS)))
|
||||
var edge: int = int(params.spawn_edge)
|
||||
var from_expr: int = int(round(float(params.density) * float(params.w * params.h - edge * edge)))
|
||||
checked += 1
|
||||
if from_fn != from_expr:
|
||||
bad += 1
|
||||
@@ -80,7 +84,8 @@ func _test_target_mine_count_single_source() -> void:
|
||||
var row1: Dictionary = _db.combat.board_gen.resolve_row(1, 0)
|
||||
Kit.check("产物断言读数 = target_mine_count 同源(metrics.target_mines)〔wd14〕",
|
||||
int(b.metrics.get("target_mines", -1)) == BoardGeneratorScript.target_mine_count({
|
||||
"w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density)}))
|
||||
"w": int(row1.board_width), "h": int(row1.board_height), "density": float(row1.mine_density),
|
||||
"spawn_edge": int(_db.combat.board_gen.spawn_edge)}))
|
||||
|
||||
|
||||
func _test_digits_equal_neighbor_mine_count() -> void:
|
||||
@@ -124,3 +129,36 @@ func _test_clearable_and_inferability_selfconsistent() -> void:
|
||||
var safe_total := b.width * b.height - b.mines.size()
|
||||
Kit.check("清图判定与安全格总数同在(安全格 %d 个,可清=%s)" % [safe_total, str(clearable)],
|
||||
safe_total > 0)
|
||||
|
||||
|
||||
# ---- V8 出生区边长表单点(架构整改——spawn_edge 表头驱动布雷域基/几何/护栏全导出)----
|
||||
|
||||
func _test_spawn_edge_from_table() -> void:
|
||||
# ① 纯函数对拍:edge=5 夹具 → target = round(d×(W×H−25))(25 格布雷域基可观测)
|
||||
var p5 := {"w": 12, "h": 12, "density": 0.15, "spawn_edge": 5}
|
||||
Kit.check("V8 纯函数:edge=5 布雷域基 25 格(target=round(d×(144−25)))〔架构整改 V8〕",
|
||||
BoardGeneratorScript.target_mine_count(p5) == int(round(0.15 * (144 - 25))))
|
||||
# ② BoardData 几何:spawn_cells() 按 spawn_edge 导出(5→25 / 3→9)
|
||||
var b5 = BoardDataScript.new()
|
||||
b5.width = 12
|
||||
b5.height = 12
|
||||
b5.spawn_center = Vector2i(5, 5)
|
||||
b5.spawn_edge = 5
|
||||
Kit.check("V8 几何:edge=5 → spawn_cells 25 格(in_bounds 过滤前满方)〔架构整改 V8〕", b5.spawn_cells().size() == 25)
|
||||
var b3 = BoardDataScript.new()
|
||||
b3.width = 12
|
||||
b3.height = 12
|
||||
b3.spawn_center = Vector2i(5, 5)
|
||||
b3.spawn_edge = 3
|
||||
Kit.check("V8 几何:edge=3 → spawn_cells 9 格(与退役常量同值——逐位回归)〔架构整改 V8〕", b3.spawn_cells().size() == 9)
|
||||
# ③ 真路径回归:真表头 spawn_edge=3 驱动出图(spawn_cells 9 格——生成器注入面)
|
||||
var b := _generate(1, 0, 74000)
|
||||
Kit.check("V8 真路径:真表头 edge=3 出图 spawn_cells 9 格〔架构整改 V8〕", b != null and b.spawn_cells().size() == 9)
|
||||
# ④ 表头域校验:edge=5 时 r0_max_cap=9 低于出生保障面 25 → validate 拒(护栏读表头)
|
||||
var tbl = load("res://data/schemas/board_gen_table.gd").new()
|
||||
tbl.spawn_edge = 5
|
||||
var probe_row := {"level_index": 1, "node_depth": 0, "board_width": 12, "board_height": 12, "mine_density": 0.15,
|
||||
"inferability_target": 0.95, "solver_rules": "single_subset", "min_safe_count": 3, "max_gen_attempts": 8,
|
||||
"board_type": "dungeon", "shave_ring_radius": 1, "r0_max_cap": 9}
|
||||
tbl.rows.append(probe_row) # typed Array[Dictionary]——append 面
|
||||
Kit.check("V8 护栏:edge=5 表头下 r0_cap=9 低于出生保障面 25 被拒〔架构整改 V8〕", not tbl.validate().is_empty())
|
||||
@@ -59,7 +59,7 @@ func _init() -> void:
|
||||
var fx = EffectEngineScript.new(null)
|
||||
var ctx_f := EffectContext.new()
|
||||
var hits: Array = []
|
||||
fx.rebuild([], [], [])
|
||||
fx.rebuild([], [], {})
|
||||
fx._push_trigger("test", "v1_row", {"on": "dig_stop",
|
||||
"when": [{"fact": "ap_left", "op": "gte", "value": 3}],
|
||||
"do": {"kind": "counter_add", "counter_id": "stop_swap", "value_event": "ap_left"}}, {})
|
||||
@@ -73,7 +73,7 @@ func _init() -> void:
|
||||
# ⑤ mod_eq 算子(双管镐落法——预评估口径每第 2 次判定;battle 桶无前缀)
|
||||
var fx2 = EffectEngineScript.new(null)
|
||||
var ctx2 := EffectContext.new()
|
||||
fx2.rebuild([], [], [])
|
||||
fx2.rebuild([], [], {})
|
||||
fx2._push_trigger("test", "mod_row", {"on": "dig_paid",
|
||||
"when": [{"fact": "counter", "id": "dbl_digs", "op": "mod_eq", "value": 2, "ref": 1}],
|
||||
"do": {"kind": "grant_ap", "value": 1}}, {})
|
||||
@@ -108,7 +108,7 @@ func _init() -> void:
|
||||
# ⑦ V4——前缀归桶清零/消耗语义(round_ 桶随 round_start 整桶清空;battle 无前缀计数跨 round_start 存活)
|
||||
var fx5 = EffectEngineScript.new(null)
|
||||
var ctx5 := EffectContext.new()
|
||||
fx5.rebuild([], [], [])
|
||||
fx5.rebuild([], [], {})
|
||||
fx5._push_trigger("test", "round_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "round_mines", "value": 1}}, {})
|
||||
for h in fx5.dispatch(ctx5, "mine_hit", {}):
|
||||
@@ -126,7 +126,7 @@ func _init() -> void:
|
||||
# battle 桶对照:无前缀 helm_layers 不受 round_start 影响(跨 round_start 存活)
|
||||
var fx6 = EffectEngineScript.new(null)
|
||||
var ctx6 := EffectContext.new()
|
||||
fx6.rebuild([], [], [])
|
||||
fx6.rebuild([], [], {})
|
||||
fx6._push_trigger("test", "battle_row", {"on": "mine_hit",
|
||||
"do": {"kind": "counter_add", "counter_id": "helm_layers", "value": 1}}, {})
|
||||
for h in fx6.dispatch(ctx6, "mine_hit", {}):
|
||||
|
||||
@@ -165,7 +165,7 @@ func _init() -> void:
|
||||
var orch = RigScript.new(db, rig_rng)
|
||||
Kit.check_quiet(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": {},
|
||||
"feats": ["perfect_insurance"], "tree_rules": ["sense_guard"], "battle_index": 1,
|
||||
"feats": ["perfect_insurance"], "tree_rules": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
|
||||
"保险 rig:战斗启动(feats=perfect_insurance 与 tree_rules=sense_guard 双订阅 seal_missed)〔卡② R2〕")
|
||||
@@ -215,7 +215,7 @@ func _init() -> void:
|
||||
var orch2 = RigScript.new(db, rig_rng2)
|
||||
Kit.check_quiet(orch2.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": {},
|
||||
"feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 2,
|
||||
"feats": ["perfect_insurance"], "tree_rules": {}, "battle_index": 2,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
|
||||
"次战 rig:第二战编排启动(feats 同件)〔卡② R2〕")
|
||||
@@ -240,7 +240,7 @@ func _init() -> void:
|
||||
o10.step_row.connect(func(row): rows10.append(row))
|
||||
Kit.check_quiet(o10.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": {},
|
||||
"feats": [], "tree_rules": [], "battle_index": 1,
|
||||
"feats": [], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
|
||||
"A10 rig:无保险战斗启动〔卡① N7〕")
|
||||
@@ -267,7 +267,7 @@ func _init() -> void:
|
||||
o11.step_row.connect(func(row): rows11.append(row))
|
||||
Kit.check_quiet(o11.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": {},
|
||||
"feats": ["perfect_insurance"], "tree_rules": [], "battle_index": 1,
|
||||
"feats": ["perfect_insurance"], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}),
|
||||
"A11 rig:带保险战斗启动〔卡① N7〕")
|
||||
|
||||
@@ -251,6 +251,8 @@ func _init() -> void:
|
||||
_smoke_shop_graph()
|
||||
_smoke_multi_terminal_settlement()
|
||||
_smoke_shop_pool()
|
||||
_test_shelf_size_from_table()
|
||||
_test_spend_discount_from_rule_key()
|
||||
_test_shop_node_entered_payload()
|
||||
_smoke_generated_coverage()
|
||||
quit(Kit.finish("level_shop_economy_test"))
|
||||
@@ -261,7 +263,6 @@ func _init() -> void:
|
||||
func _smoke_table_migration() -> void:
|
||||
print("--- 数值入表(shop 两 knob + hero 显示名 · 原值迁移)---")
|
||||
Kit.check("shop.relic_base_price 表值 == 迁移前字面量(15.0)〔wd11〕", _db.run.shop.relic_base_price == 15.0)
|
||||
Kit.check("shop.spend_discount 表值 == 迁移前字面量(0.8)〔wd11〕", _db.run.shop.spend_discount == 0.8)
|
||||
Kit.check("hero.display_name 表值 == 迁移前字面量(探险者)〔wd16〕", _db.combat.hero.display_name == "探险者")
|
||||
# 消费端读表:spawn 真实构造链的英雄单位显示名与表值同
|
||||
var units = load("res://core/unit_container.gd").new(_db)
|
||||
@@ -1554,3 +1555,42 @@ func _has_err_category(errs: Array, category: String) -> bool:
|
||||
# ⑦ 实装探针(妨害技蛛网束缚 + 毒爆必中附加分量,实装面 = L2 Boss 卓尔):
|
||||
# 停手轮驱动 (2,9)——零挖掘零封印 → 零池,伤害行可逐拍精确复算;种子 hunt 至少各命一粒毒爆伤害行
|
||||
# 与蛛网束缚施放行;毒分量随开战掷定的攻击基线缩放(atk 区间 2–4 → rider 1–3)。
|
||||
|
||||
|
||||
# ---- 货架位数读表(架构整改——shelf_size 表头单点:进店与重抽同源)----
|
||||
|
||||
func _test_shelf_size_from_table() -> void:
|
||||
var autoplay = load("res://sim/level_autoplay.gd")
|
||||
var st = _db.run.shop
|
||||
var saved: int = int(st.shelf_size)
|
||||
st.shelf_size = 5 # 运行时表头改值夹具(测毕还原——真表行值零触碰)
|
||||
var got5 := -1
|
||||
for s in range(42, 92):
|
||||
var a9 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(s))
|
||||
a9.set_hero_override(900, 400)
|
||||
if not a9.start_adventure(s, {"shop_mode": "pool"}):
|
||||
continue
|
||||
if not _adv_drive_to_shop(a9, _adv_st(), autoplay):
|
||||
continue
|
||||
got5 = (a9.runner().shop.shop_shelf() as Array).size()
|
||||
break
|
||||
st.shelf_size = saved
|
||||
Kit.check("夹具 shelf_size=5 → 货架五件可观测(实测 %d;真表值 3 回归=既有「进店抽 3」断言承载)〔架构整改 V9〕" % got5, got5 == 5)
|
||||
|
||||
|
||||
# ---- 散折价收回规则键(架构整改 R2——shop.spend_discount 表字段退役,折价读 economy_spend.discount_pct)----
|
||||
|
||||
func _test_spend_discount_from_rule_key() -> void:
|
||||
# 字段真删:shop.tres 加载零诊断 + spend_discount 属性不在位
|
||||
Kit.check("shop.tres 加载零诊断(spend_discount 字段真删——schema 与表行同卸)〔架构整改 V2〕",
|
||||
_db.diagnostics_count("error") == 0 and _db.run.shop.get("spend_discount") == null)
|
||||
# 80% 折价值逐位同(真实入口=LevelRunner 装配链 shop_session.current_price,economy_spend 注入)
|
||||
var bonus: int = int(_db.run.shop.price_level_bonus_at(0))
|
||||
var spend_dict: Dictionary = {"economy_spend": {"discount_pct": 80}}
|
||||
var lr_on = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005), {"tree_rules": spend_dict})
|
||||
var lr_off = load("res://core/level_runner.gd").new(_db, _fresh_rng(777006), {"tree_rules": {}})
|
||||
var p_on: int = int(lr_on.shop.current_price({"base_price": 100, "price_increment": 0}))
|
||||
var p_off: int = int(lr_off.shop.current_price({"base_price": 100, "price_increment": 0}))
|
||||
var expect_on: int = maxi(1, int(floor(float(100 + bonus) * float(spend_dict["economy_spend"]["discount_pct"]) / 100.0)))
|
||||
Kit.check("散激活:折后价 = (基价+关卡加成)×discount_pct/100 floor(%d vs 期望 %d)〔架构整改 V2〕" % [p_on, expect_on], p_on == expect_on)
|
||||
Kit.check("散未激活:原价(无 economy_spend 键——%d)〔架构整改 V2〕" % p_off, p_off == 100 + bonus)
|
||||
@@ -37,6 +37,10 @@ func _init() -> void:
|
||||
_smoke_forge_axes()
|
||||
_smoke_tree()
|
||||
_smoke_tree_rules()
|
||||
_test_forge_buy_reject()
|
||||
_test_blessing_no_forge_leak()
|
||||
_test_default_axes_pre_key()
|
||||
_test_armor_switch_dual_axes()
|
||||
quit(Kit.finish("meta_progression_forge_test"))
|
||||
|
||||
|
||||
@@ -50,7 +54,8 @@ const _META_SMOKE_PATHS := ["user://smoke_meta_buy.json", "user://smoke_meta_dry
|
||||
"user://smoke_meta_tree.json", "user://smoke_meta_career.json",
|
||||
"user://smoke_meta_career_v5.json", "user://smoke_meta_treasure_unlock.json", "user://smoke_meta_treasure_blank.json",
|
||||
"user://smoke_meta_forge.json", "user://smoke_meta_forge2.json", "user://smoke_meta_forge3.json",
|
||||
"user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json"]
|
||||
"user://smoke_meta_forge4.json", "user://smoke_meta_forge5.json",
|
||||
"user://smoke_meta_reject.json", "user://smoke_meta_blessing.json", "user://smoke_meta_axesw.json"]
|
||||
|
||||
|
||||
func _meta_smoke_cleanup() -> void:
|
||||
@@ -309,7 +314,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
sv_e.meta_state.set_attune_refresh_level(2)
|
||||
sv_e.meta_state.set_fork_side("fg_posture", "a")
|
||||
var mp_e = load("res://core/meta_progression.gd").new(_db, sv_e)
|
||||
var rules_e: Array = mp_e.tree_rule_keys()
|
||||
var rules_e: Dictionary[Variant, Variant] = mp_e.tree_rules()
|
||||
Kit.check("开局传参夹具前置:Fork 侧 a 激活 rule 键非空 + Y=Lv2 行表值 2(防断言空转)",
|
||||
rules_e.has("posture_first_strike") and mp_e.attune_refresh_charges() == 2)
|
||||
var p_new: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
|
||||
@@ -319,7 +324,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
Kit.check("主菜单开新冒险参数面:Y 刷新池 = 当前等级行表值(营地升几档开局就给几次)",
|
||||
int(p_new.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges())
|
||||
Kit.check("主菜单开新冒险参数面:树 rule 键集 = Fork 激活侧 effects(点哪侧带哪侧)",
|
||||
(p_new.get("tree_rules", []) as Array) == rules_e)
|
||||
(p_new.get("tree_rules", {}) as Dictionary) == rules_e)
|
||||
Kit.check("主菜单开新冒险参数面:入口特有键(treasure_unlocked/shop_mode)随 extra 原样并入",
|
||||
bool((p_new.get("treasure_unlocked", {}) as Dictionary).get("wide_scout_band", false)) and String(p_new.get("shop_mode", "")) == "pool")
|
||||
var p_resume: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv_e, mp_e,
|
||||
@@ -327,7 +332,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
Kit.check("主菜单继续上局参数面:三键与开新冒险同源同值(续玩不吃白板开局)",
|
||||
(p_resume.get("attunement", []) as Array) == (sv_e.meta_state.attune_state().equipped as Array)
|
||||
and int(p_resume.get("attune_refresh_y", -1)) == mp_e.attune_refresh_charges()
|
||||
and (p_resume.get("tree_rules", []) as Array) == rules_e)
|
||||
and (p_resume.get("tree_rules", {}) as Dictionary) == rules_e)
|
||||
# 续玩路径 tree_rules 受理断言:直呼 runner 续玩参数面
|
||||
# (v2 旧断点夹具同式最小断点——smoke ⑫ 受理路径同形)验 _tree_rules 与传入一致。
|
||||
var adr_res = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(19))
|
||||
@@ -339,7 +344,7 @@ func _meta_settle_and_workshop() -> void:
|
||||
adr_res.resume_adventure(bp_min, p_resume))
|
||||
Kit.check("继续上局受理面:tree_rules 随参数面进 runner._tree_rules(消费面 set_tree_rules 下传现成)",
|
||||
adr_res.runner() != null and adr_res.runner().pick_next_node(int(adr_res.runner().next_candidate_ids()[0]))
|
||||
and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", []) as Array) == rules_e))
|
||||
and ((adr_res.runner().current_battle()._facts.encounter.get("tree_rules", {}) as Dictionary) == rules_e))
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
@@ -418,6 +423,12 @@ func _smoke_forge_axes() -> void:
|
||||
Kit.check("长剑二级购:扣 30、等级→2;轻甲独立零扰动(六路线各练各的)〔锻造工坊批 V2〕",
|
||||
mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 2
|
||||
and sv2.meta_state.keystone_balance() == 50 and sv2.meta_state.forge_state().levels.get("light", 0) == 0)
|
||||
# V12:长剑购买链走至 3 级(45 档扣款——余额 50−45=5,几何价第三档);链后续夹具段余额基准
|
||||
# 由 add_keystone 补回(注入口径——直写存档域构造状态,下游 V3 段 50−20 语义不变)
|
||||
Kit.check("长剑三级购:扣 45、等级→3(余额 50−45=5)〔断言补齐批 V12〕",
|
||||
mp2.forge_buy_level("sword") and sv2.meta_state.forge_state().levels.get("sword", 0) == 3
|
||||
and sv2.meta_state.keystone_balance() == 5)
|
||||
sv2.meta_state._add_keystone(45)
|
||||
# V2③:余额不足拒且零副作用(等级/选中/解锁三面零变化)
|
||||
var sv2d = _meta_new_save(_META_SMOKE_PATHS[14])
|
||||
var mp2d = load("res://core/meta_progression.gd").new(_db, sv2d)
|
||||
@@ -605,10 +616,10 @@ func _smoke_tree() -> void:
|
||||
Kit.check("零币切态 b(购买解锁后 A/B 可切)",
|
||||
mp.tree_toggle_fork("fg_posture", "b") and sv.meta_state.tree_state().fork_sides.get("fg_posture", "") == "b")
|
||||
Kit.check("切态零消费(钥石余额差分不动)", int(sv.meta_state.keystone_balance()) == bal_pre_toggle)
|
||||
Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rule_keys 随切态换键集)",
|
||||
(mp.tree_rule_keys() as Array).has("posture_bulwark") and not (mp.tree_rule_keys() as Array).has("posture_first_strike"))
|
||||
Kit.check("规则键投影=激活态效果键(B 态稳健拓荒——tree_rules 随切态换键集)",
|
||||
(mp.tree_rules() as Dictionary).has("posture_bulwark") and not (mp.tree_rules() as Dictionary).has("posture_first_strike"))
|
||||
Kit.check("切回 A 态键集随换(六键词表在册——V11 校验面归单元段)",
|
||||
mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rule_keys() as Array).has("posture_first_strike"))
|
||||
mp.tree_toggle_fork("fg_posture", "a") and (mp.tree_rules() as Dictionary).has("posture_first_strike"))
|
||||
# 彩蛋特化觉醒(V8)
|
||||
Kit.check("彩蛋不经购买(手动购亮=红——觉醒唯一入口=特化判定)", not mp.tree_can_buy("easter_ley"))
|
||||
Kit.check("觉醒前置未满足不亮(两线未拉满)", sv.meta_state.tree_state().nodes.get("easter_ley", 0) == 0)
|
||||
@@ -629,7 +640,7 @@ func _smoke_tree_rules() -> void:
|
||||
print("--- 天赋树六键行为(V10:窗/限次/聚散/守侧)---")
|
||||
# 开局姿态组:每关首轮窗(battle_index 事实——引擎直构 + 自建 ctx)
|
||||
var fx = load("res://core/effect_engine.gd").new(null)
|
||||
fx.rebuild([], [], ["posture_first_strike", "posture_bulwark"])
|
||||
fx.rebuild([], [], {"posture_first_strike": {"ap_bonus": 1}, "posture_bulwark": {"shield": 1}})
|
||||
var ctx_p := EffectContext.new()
|
||||
Kit.check("先手压制:每关首战首轮窗命中(battle_index=1 且 round=1 → grant_ap queue)〔天赋树批 V10〕",
|
||||
(fx.dispatch(ctx_p, "round_start", {"battle_index": 1, "round_index": 1}, Callable()) as Array).size() == 1)
|
||||
@@ -641,7 +652,7 @@ func _smoke_tree_rules() -> void:
|
||||
and (fx.dispatch(ctx_p, "damage_taken", {"battle_index": 2, "round_index": 1}, Callable()) as Array).is_empty())
|
||||
# 危险直觉·锋:限次闸(引擎直构——mine_hit 挂点现成;限次计数住 level 桶,跨战持久=共享同一字典引用)
|
||||
var fz = load("res://core/effect_engine.gd").new(null)
|
||||
fz.rebuild([], [], ["sense_sharp"])
|
||||
fz.rebuild([], [], {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2}})
|
||||
var lvl_shared := {} # 同一关的 level 桶(两编排器共享引用 = 跨战持久,替代旧副本搬运)
|
||||
var ctx_fz := EffectContext.new()
|
||||
ctx_fz.level_bucket = lvl_shared
|
||||
@@ -658,7 +669,7 @@ func _smoke_tree_rules() -> void:
|
||||
Kit.check("锋:限次耗尽全额(counter 2 不<2 闸关——每关限次 provisional 2)〔无状态化 V5〕",
|
||||
(fz.dispatch(ctx_fz, "mine_hit", {}, Callable()) as Array).is_empty())
|
||||
var fz2 = load("res://core/effect_engine.gd").new(null)
|
||||
fz2.rebuild([], [], ["sense_sharp"])
|
||||
fz2.rebuild([], [], {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2}})
|
||||
var ctx_fz2 := EffectContext.new()
|
||||
ctx_fz2.level_bucket = lvl_shared # 挂同一 level 桶引用(新形态跨战持久:无 export/seed,同字典即持久)
|
||||
Kit.check("锋:限次跨战持久(level 桶共享引用——替代旧副本搬运链)〔无状态化 V4〕",
|
||||
@@ -668,7 +679,7 @@ func _smoke_tree_rules() -> void:
|
||||
var orch_nc = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777003))
|
||||
orch_nc.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": {},
|
||||
"feats": [], "tree_rules": [], "battle_index": 1,
|
||||
"feats": [], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}) # 真实战斗上下文(状态容器开域——dispatch 链受理)
|
||||
var nc_rows: Array = [] # step_row 采集盒(lambda 捕引用容器——捕获变量赋值不出闭包)
|
||||
@@ -692,7 +703,7 @@ func _smoke_tree_rules() -> void:
|
||||
Kit.check("守侧 rig:战斗编排启动(tree_rules=sense_guard 携带)",
|
||||
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": {},
|
||||
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
|
||||
"feats": [], "tree_rules": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
|
||||
var board = orch.minesweeper.current_board()
|
||||
@@ -722,7 +733,7 @@ func _smoke_tree_rules() -> void:
|
||||
Kit.check("守⑤ rig:次战编排启动(新战新队列实例)",
|
||||
orch2.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": {},
|
||||
"feats": [], "tree_rules": ["sense_guard"], "battle_index": 1,
|
||||
"feats": [], "tree_rules": {"sense_guard": {"ap_refund": 1, "cap_per_level": 2}}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
|
||||
Kit.check("守⑤:末轮判出不返(queue 只在 notify_round_start 兑现、战亡即弃——无跨战泄漏)",
|
||||
@@ -730,33 +741,103 @@ func _smoke_tree_rules() -> void:
|
||||
# 树限次按关清零改由 LevelRunner.level_counters 随 new 承载(旧硬编码名单冒烟断言随函数删除——换代同生死;新语义由 V4/V5 段承载)
|
||||
# 财富之道·散:商品标价折价(level_runner 计价单点)
|
||||
var lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777004),
|
||||
{"tree_rules": []}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
{"tree_rules": {}}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check("散未激活:商品标价原价(无 economy_spend 键)",
|
||||
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == 100 + int(_db.run.shop.price_level_bonus_at(0)))
|
||||
var spend_dict: Dictionary = {"economy_spend": {"discount_pct": 80}}
|
||||
lr = load("res://core/level_runner.gd").new(_db, _fresh_rng(777005),
|
||||
{"tree_rules": ["economy_spend"]}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
{"tree_rules": spend_dict}) # 构造注入(拆件批 T3——setter 群退役)
|
||||
Kit.check("散激活:商品购买价 ×80%(provisional 折率——floor 偏向玩家、下限 1;刷新价不折=另一计价点零触碰)",
|
||||
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount))))
|
||||
and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(_db.run.shop.spend_discount)))))
|
||||
int(lr.shop.current_price({"base_price": 100, "price_increment": 0})) == maxi(1, int(floor(float(100 + int(_db.run.shop.price_level_bonus_at(0))) * float(spend_dict["economy_spend"]["discount_pct"]) / 100.0)))
|
||||
and int(lr.shop.current_price({"base_price": 1, "price_increment": 0})) == maxi(1, int(floor(float(1 + int(_db.run.shop.price_level_bonus_at(0))) * float(spend_dict["economy_spend"]["discount_pct"]) / 100.0))))
|
||||
# 财富之道·聚:战斗掉落乘算·入账前(编排 _roll_gp_drop——同种子配对对拍 1.5×)
|
||||
var orch3 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
|
||||
Kit.check("聚 rig:基线编排启动(无增益侧)",
|
||||
orch3.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": {},
|
||||
"feats": [], "tree_rules": [], "battle_index": 1,
|
||||
"feats": [], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
|
||||
orch3._facts.encounter["tree_rules"] = []
|
||||
orch3._facts.encounter["tree_rules"] = {}
|
||||
var gp_base: int = orch3._roll_gp_drop()
|
||||
var orch4 = load("res://core/battle_orchestrator.gd").new(_db, _fresh_rng(777005))
|
||||
Kit.check("聚 rig:增益编排启动(tree_rules=economy_hoard)",
|
||||
orch4.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": {},
|
||||
"feats": [], "tree_rules": ["economy_hoard"], "battle_index": 1,
|
||||
"feats": [], "tree_rules": {"economy_hoard": {"mult_pct": 150}}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "adventure_counters": {},
|
||||
"voucher_rap": 0, "voucher_time_sand": 0, "node_type": "battle"}))
|
||||
orch4._facts.encounter["tree_rules"] = ["economy_hoard"]
|
||||
orch4._facts.encounter["tree_rules"] = {"economy_hoard": {"mult_pct": 150}}
|
||||
var gp_hoard: int = orch4._roll_gp_drop()
|
||||
Kit.check("聚:战斗掉落乘算 ×1.5(provisional——同种子配对 %d→%d,入账前=结算框显示实得)",
|
||||
gp_hoard == int(round(float(gp_base) * 1.5)) and gp_hoard >= gp_base)
|
||||
gp_hoard == int(round(float(gp_base) * float({"economy_hoard": {"mult_pct": 150}}["economy_hoard"]["mult_pct"]) / 100.0)) and gp_hoard >= gp_base)
|
||||
_meta_smoke_cleanup()
|
||||
|
||||
|
||||
# ---- 锻造契约补齐(断言补齐批——⑥锻造工坊):拒购零副作用/祝福件零泄漏/默认值解耦/甲型双轴 ----
|
||||
|
||||
func _test_forge_buy_reject() -> void:
|
||||
var sv = _meta_new_save("user://smoke_meta_reject.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
sv.meta_state._add_keystone(10) # 低于首购价 20——余额不足面
|
||||
var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true)
|
||||
var bal0: int = sv.meta_state.keystone_balance()
|
||||
Kit.check("余额不足拒购:sword false、等级与余额零变化〔断言补齐批 V13〕",
|
||||
not mp.forge_buy_level("sword") and sv.meta_state.forge_state() == fs0 and sv.meta_state.keystone_balance() == bal0)
|
||||
sv.meta_state._add_keystone(1000) # 充值后未解锁面不变(spear 直购未走/练满未达)
|
||||
Kit.check("未解锁拒购:spear false、等级与余额零变化〔断言补齐批 V13〕",
|
||||
not mp.forge_buy_level("spear") and sv.meta_state.forge_state() == fs0
|
||||
and sv.meta_state.keystone_balance() == bal0 + 1000)
|
||||
|
||||
|
||||
func _test_blessing_no_forge_leak() -> void:
|
||||
var sv = _meta_new_save("user://smoke_meta_blessing.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var fs0: Dictionary = sv.meta_state.forge_state().duplicate(true)
|
||||
# 真实入口=商店购买口(LevelRunner 装配的 shop_session.buy_product——祝福件走 GP 钱包+局内加算区)
|
||||
var runner = load("res://core/level_runner.gd").new(_db, _fresh_rng(80))
|
||||
var lvl_ok: bool = runner.start_level(1, 80)
|
||||
Kit.check("V14 前置:关卡启动(L1 行集)", lvl_ok)
|
||||
if not lvl_ok:
|
||||
return
|
||||
runner.gp = 100
|
||||
Kit.check("V14 前置:攻击祝福受理", runner.shop.buy_product("atk_up"))
|
||||
Kit.check("V14 前置:护甲祝福受理", runner.shop.buy_product("armor_up"))
|
||||
Kit.check("祝福件零泄漏:购买后 forge_state 投影逐位不变(levels/选中/解锁三面)〔断言补齐批 V14〕",
|
||||
sv.meta_state.forge_state() == fs0)
|
||||
Kit.check("祝福件住局内加算区(atk +1 在加算日记——通道分离实证)", runner.journal().bonus_total("atk") == 1)
|
||||
|
||||
|
||||
func _test_default_axes_pre_key() -> void:
|
||||
var sv = _meta_new_save("user://smoke_meta_axesw.json")
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
var ax: Dictionary = mp.hero_type_axes()
|
||||
Kit.check("树钥匙未点亮:hero_type_axes 返回锻造配置默认 normal/light(默认值常驻解耦)〔断言补齐批 V15〕",
|
||||
String(ax["attack"]) == "normal" and String(ax["armor"]) == "light")
|
||||
|
||||
|
||||
func _test_armor_switch_dual_axes() -> void:
|
||||
var sv = _meta_new_save("user://smoke_meta_axesw.json")
|
||||
sv.meta_state._add_keystone(1000)
|
||||
var mp = load("res://core/meta_progression.gd").new(_db, sv)
|
||||
Kit.check("V16 前置:重甲直购解锁+首购+选中(200+20 钥石)",
|
||||
mp.forge_buy_unlock("heavy") and mp.forge_buy_level("heavy") and mp.forge_select("armor", "heavy"))
|
||||
var adr = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(81))
|
||||
var p: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv, mp)
|
||||
var adv_ok: bool = adr.start_adventure(81, p)
|
||||
Kit.check("V16 前置:参数面冒险启动", adv_ok)
|
||||
if not adv_ok:
|
||||
return
|
||||
Kit.check("甲型双轴:heavy 选中 → AC 轴 12 基础+路线 Lv1=13(light 同级 11 的 +2 档差)〔断言补齐批 V16〕",
|
||||
int(adr.runner().effective_ac()) == 13 and String(mp.hero_type_axes()["armor"]) == "heavy")
|
||||
Kit.check("甲型双轴:挨普通攻击修正 −1(克制矩阵 normal→heavy)〔断言补齐批 V16〕",
|
||||
int(_db.combat.settlement.damage_roll.armor_effectiveness["normal"]["heavy"]) == -1)
|
||||
Kit.check("V16 前置:切回 light(初始持有已解锁可切)", mp.forge_select("armor", "light"))
|
||||
var adr2 = load("res://core/adventure_runner.gd").new(_db, _fresh_rng(82))
|
||||
var p2b: Dictionary = load("res://presentation/main.gd").build_adventure_params(sv, mp)
|
||||
var adv2_ok: bool = adr2.start_adventure(82, p2b)
|
||||
Kit.check("V16 前置:切回后新参数面冒险启动(快照冻结契约——新 runner 读新选中)", adv2_ok)
|
||||
if not adv2_ok:
|
||||
return
|
||||
Kit.check("甲型双轴:切回 light 复原(AC 10+0)且 heavy 等级保留(Lv1 不丢)〔断言补齐批 V16〕",
|
||||
int(adr2.runner().effective_ac()) == 10 and sv.meta_state.forge_state().levels.get("heavy", 0) == 1)
|
||||
@@ -40,6 +40,7 @@ func _init() -> void:
|
||||
_test_reveal_advances()
|
||||
_test_seal_place()
|
||||
_test_reject_out_of_catalog()
|
||||
_test_preview_reveal_readonly()
|
||||
quit(Kit.finish("minesweeper_rules_test"))
|
||||
|
||||
|
||||
@@ -110,3 +111,44 @@ func _test_reject_out_of_catalog() -> void:
|
||||
var o: Dictionary = (out[0] as Dictionary) if not out.is_empty() else {}
|
||||
Kit.check("目录外意图被拒(不静默吞掉)——实际载荷 %s" % str(o).substr(0, 80),
|
||||
out.size() == 1 and String(o.get("outcome", o.get("result", ""))) == "rejected")
|
||||
|
||||
|
||||
# ---- V14 棋盘揭示预览只读口(架构整改——preview_reveal 与实挖揭示集同口径、零状态写入零事件)----
|
||||
|
||||
func _test_preview_reveal_readonly() -> void:
|
||||
var events := 0
|
||||
_rules.cell_revealed.connect(func(_evt): events += 1) # 零发射哨兵(preview 全程不触发揭示事件)
|
||||
# 形态 A:封印在场——preview(未开格) 不越封印且零状态写入
|
||||
var before_revealed := _rules.revealed_cells().size()
|
||||
var before_seals := _rules.seal_count()
|
||||
var c_open := _unrevealed_safe()
|
||||
var prev_a: Array = _rules.preview_reveal(c_open)
|
||||
Kit.check("V14 形态A:preview 返回集(真板形态——%d 格)" % prev_a.size(), prev_a.size() >= 0)
|
||||
Kit.check("V14 形态A:调用前后零状态写入(revealed/seal 计数零变化)〔架构整改 V14〕",
|
||||
_rules.revealed_cells().size() == before_revealed and _rules.seal_count() == before_seals)
|
||||
# 形态 B:挖封印格——本格封印隐式退役只入模拟集,真实封印计数不动
|
||||
var seal_c := _unrevealed_safe()
|
||||
_rules.settle_batch([{"action": "seal_place", "cell": seal_c}])
|
||||
var sealed_before := _rules.seal_count()
|
||||
var prev_b: Array = _rules.preview_reveal(seal_c)
|
||||
Kit.check("V14 形态B:preview(封印格) 零状态写入(seal_count 不变——隐式退役只入模拟集)〔架构整改 V14〕",
|
||||
_rules.seal_count() == sealed_before)
|
||||
# 同口径对照:preview(c) = settle_batch(dig c) 后 revealed 差集 ∖ {c}(F2——不含首格与已开格)
|
||||
var dig_c := _unrevealed_safe()
|
||||
var prev_c: Array = _rules.preview_reveal(dig_c)
|
||||
var before := {}
|
||||
for rc in _rules.revealed_cells():
|
||||
before[rc] = true
|
||||
_rules.settle_batch([{"action": "dig", "cell": dig_c}])
|
||||
var after_diff := []
|
||||
for rc in _rules.revealed_cells():
|
||||
if not before.has(rc) and rc != dig_c:
|
||||
after_diff.append(rc)
|
||||
Kit.check("V14 同口径:preview(c) = 实挖揭示差集 ∖ {c}(F2——%d vs %d 格逐位)〔架构整改 V14〕" % [prev_c.size(), after_diff.size()],
|
||||
str(prev_c) == str(after_diff))
|
||||
var events_before_previews := events # 三次 preview 调用前快照(此前增量=夹具自身实挖——与 preview 无关)
|
||||
var _pa: Array = _rules.preview_reveal(_unrevealed_safe())
|
||||
var _pb: Array = _rules.preview_reveal(_unrevealed_safe())
|
||||
var _pc: Array = _rules.preview_reveal(_unrevealed_safe())
|
||||
Kit.check("V14 零事件:preview 全程 cell_revealed 零发射(前后增量 0——实测 %d→%d)〔架构整改 V14〕" % [events_before_previews, events],
|
||||
events == events_before_previews)
|
||||
@@ -29,6 +29,8 @@ func _init() -> void:
|
||||
_test_shop_and_attribution()
|
||||
_test_battle_floor()
|
||||
_test_exit_structure()
|
||||
_test_boss_exit_per_region()
|
||||
_test_gen_params_table_driven()
|
||||
_test_rng_single_point()
|
||||
_test_negative_fixtures()
|
||||
quit(Kit.finish("node_map_generator_test"))
|
||||
@@ -303,6 +305,41 @@ func _terminals_reachable(g: Dictionary, id: int) -> int:
|
||||
return c
|
||||
|
||||
|
||||
# ---- V9 Boss 出场门控(难度曲线与Boss——每区域恰一 Boss 终点)----
|
||||
|
||||
func _test_boss_exit_per_region() -> void:
|
||||
# 真实生成多区域图:全部终点 type=="boss"、boss 终点数==exit_count、深度=L−r(r∈[0,exit_count)
|
||||
# ——每区域恰一 boss、深度互异即分属不同区域)
|
||||
# 节点 depth 字段=入口起最长路径深(生成器校验轴);区域序号 r 的可观测形态=最深 boss(r=0)
|
||||
# 与该 boss 的深度差(同层 boss 深度 = d0 − r——区域段 L−r 的 1 步差)
|
||||
var all_ok := true
|
||||
for L in [1, 2, 3]:
|
||||
for s in [42, 43, 44]:
|
||||
var g := _graph(L, s)
|
||||
var by_id := {}
|
||||
for n in g["nodes"]:
|
||||
by_id[int(n["id"])] = n
|
||||
var exits := int(_params(L)["exit_count"])
|
||||
var d0 := 0
|
||||
for tid in g["terminals"]:
|
||||
d0 = maxi(d0, int(by_id[int(tid)]["depth"]))
|
||||
var per_region := {}
|
||||
for tid in g["terminals"]:
|
||||
var tn: Dictionary = by_id[int(tid)]
|
||||
if String(tn["type"]) != "boss":
|
||||
all_ok = false
|
||||
var r := d0 - int(tn["depth"])
|
||||
if r < 0 or r >= exits:
|
||||
all_ok = false
|
||||
per_region[r] = int(per_region.get(r, 0)) + 1
|
||||
if per_region.size() != exits:
|
||||
all_ok = false
|
||||
for r in per_region:
|
||||
if int(per_region[r]) != 1:
|
||||
all_ok = false
|
||||
Kit.check("每区域恰一 Boss 终点(type=boss × exit_count 个、深度=d0−r 互异分域——区域段 L−r 的可观测形态)〔断言补齐批 V9〕", all_ok)
|
||||
|
||||
|
||||
# ---- V10 RNG 单点(纯流面 counter 不变)----
|
||||
|
||||
func _test_rng_single_point() -> void:
|
||||
@@ -441,3 +478,57 @@ func _has_msg(errs: Array, needle: String) -> bool:
|
||||
if String(e["message"]).contains(needle):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
# ---- V6 节点图三参数表值驱动(架构整改——fork_third_way_pct/fill_rest_first_pct/width_max 入 level_gen_params)----
|
||||
|
||||
func _test_gen_params_table_driven() -> void:
|
||||
# ① 同种子真实表行产图逐位(表值驱动回归锚)
|
||||
var g1 := _graph(2, 42)
|
||||
var g2 := _graph(2, 42)
|
||||
Kit.check("表值驱动:同种子产图逐位复现〔架构整改 V6〕", _sig(g1) == _sig(g2))
|
||||
# ② width_max=2 夹具 → 各深度节点数 ≤2(宽度上限读表可观测)
|
||||
var p2: Dictionary = _params(2).duplicate(true)
|
||||
p2["width_max"] = 2
|
||||
var gw: Dictionary = _gen.generate(p2, 42)
|
||||
var per_depth := {}
|
||||
for n in gw["nodes"]:
|
||||
var d2: int = int(n["depth"])
|
||||
per_depth[d2] = int(per_depth.get(d2, 0)) + 1
|
||||
var width_ok := true
|
||||
for d3 in per_depth:
|
||||
if int(per_depth[d3]) > 2:
|
||||
width_ok = false
|
||||
Kit.check("width_max=2 夹具:各深度节点数 ≤2(宽度上限读表可观测)〔架构整改 V6〕", width_ok)
|
||||
# ③ fork_third_way_pct=0 夹具 → 无节点去路数超 fork_ways_min(三去路概率读表可观测)
|
||||
var p0: Dictionary = _params(2).duplicate(true)
|
||||
p0["fork_third_way_pct"] = 0
|
||||
var g0: Dictionary = _gen.generate(p0, 42)
|
||||
var ways_ok := true
|
||||
for n in g0["nodes"]:
|
||||
if (n["next"] as Array).size() > int(p0["fork_ways_min"]):
|
||||
ways_ok = false
|
||||
Kit.check("fork_third_way_pct=0 夹具:无节点去路数超 fork_ways_min(三去路概率读表可观测)〔架构整改 V6〕", ways_ok)
|
||||
# ④ fill_rest_first_pct=100 vs 0 → 填充交替起点可观测变化(交替起始骰读表)。
|
||||
# 夹具=L1 真实表行改 branch_density=0/battle_floor=0:fill 段只在单出口图跑(e==1——_assign_types)、
|
||||
# 岔口修型段会覆盖侧链头填充位——无岔口+零保底组合让填充差异直达产物(多种子扫过 diff 恒真)
|
||||
var p100: Dictionary = _params(1).duplicate(true)
|
||||
p100["branch_density"] = 0.0
|
||||
p100["battle_floor"] = 0
|
||||
p100["fill_rest_first_pct"] = 100
|
||||
var g100: Dictionary = _gen.generate(p100, 40)
|
||||
var p000: Dictionary = _params(1).duplicate(true)
|
||||
p000["branch_density"] = 0.0
|
||||
p000["battle_floor"] = 0
|
||||
p000["fill_rest_first_pct"] = 0
|
||||
var g000: Dictionary = _gen.generate(p000, 40)
|
||||
var rest100 := 0
|
||||
for n in g100["nodes"]:
|
||||
if String(n["type"]) == "rest":
|
||||
rest100 += 1
|
||||
var rest000 := 0
|
||||
for n in g000["nodes"]:
|
||||
if String(n["type"]) == "rest":
|
||||
rest000 += 1
|
||||
Kit.check("fill_rest_first_pct=100 vs 0 夹具:填充交替起点产物互异(%d vs %d rest)〔架构整改 V6〕" % [rest100, rest000],
|
||||
_sig(g100) != _sig(g000) and rest100 > rest000)
|
||||
@@ -66,11 +66,13 @@ func _test_fail_fast_static(gen, db) -> void:
|
||||
and NodeMapGenerator.check_params(db.run.level_gen_params.row_by_level_index(3)).is_empty())
|
||||
var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2,
|
||||
"rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 3, "battle_floor": 4,
|
||||
"fork_ways_min": 2, "fork_ways_max": 3}
|
||||
"fork_ways_min": 2, "fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4}
|
||||
Kit.check("层深 6 放三出口无解 → check_params 拦截〔fv4〕", not NodeMapGenerator.check_params(unsolvable).is_empty())
|
||||
var overfloor := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2,
|
||||
"rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 1, "battle_floor": 99,
|
||||
"fork_ways_min": 2, "fork_ways_max": 3}
|
||||
"fork_ways_min": 2, "fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4}
|
||||
Kit.check("战斗保底超可放数无解 → check_params 拦截〔fv4〕", not NodeMapGenerator.check_params(overfloor).is_empty())
|
||||
|
||||
|
||||
@@ -96,7 +98,8 @@ func _trigger_fatal() -> void:
|
||||
# 域内合法但深度预算无解:层深 6 放三出口(check_params 拦下 → 致命中止)
|
||||
var unsolvable := {"layers": 6, "branch_density": 0.0, "battle_ratio": 0.5, "reward_ratio": 0.2,
|
||||
"rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 3, "battle_floor": 4,
|
||||
"fork_ways_min": 2, "fork_ways_max": 3}
|
||||
"fork_ways_min": 2, "fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4}
|
||||
gen.generate(unsolvable, 42)
|
||||
# 致命封印未置位=fail-fast 静默化(缺陷)→ 显式退出码 2 与致命停机码 1 区分
|
||||
if gen.is_fatal():
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
# tests/unit/core/playtest_logger_guard_test.gd — 试玩日志器 ap_per_round 必填注入守卫〔架构整改 V5〕
|
||||
# 断言面 = PlaytestLogger.start_session():未注入(0)返回空串+零文件产出;注入 8 返回非空路径。
|
||||
# 真实入口=PlaytestLogger.start_session(R5 必填注入——组合根注入序前置的守卫面)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/core/playtest_logger_guard_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_smoke_required_injection()
|
||||
quit(Kit.finish("playtest_logger_guard_test"))
|
||||
|
||||
|
||||
func _smoke_required_injection() -> void:
|
||||
# 未注入(缺省 0):start_session 返回空串、零文件产出
|
||||
var lg0 = load("res://presentation/playtest_logger.gd").new()
|
||||
var p0: String = lg0.start_session(42)
|
||||
Kit.check("未注入拒绝开会话:start_session 返回空串〔架构整改 V5〕", p0 == "")
|
||||
var f0 := FileAccess.file_exists(String(lg0._path))
|
||||
Kit.check("未注入零文件产出(_path 留空、盘上无会话件)〔架构整改 V5〕", not f0 and String(lg0._path) == "")
|
||||
# 注入 8:返回非空路径(会话开启)
|
||||
var lg8 = load("res://presentation/playtest_logger.gd").new()
|
||||
lg8.ap_per_round = 8
|
||||
var p8: String = lg8.start_session(42)
|
||||
Kit.check("注入 8 返回非空路径(守卫放行——会话开启)〔架构整改 V5〕", p8 != "")
|
||||
if lg8._file != null:
|
||||
lg8._file.close()
|
||||
var f8 := FileAccess.file_exists(p8)
|
||||
Kit.check("注入后会话件在盘〔架构整改 V5〕", f8)
|
||||
if f8:
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(p8))
|
||||
# 越域拒绝(33>32 上界)
|
||||
var lg33 = load("res://presentation/playtest_logger.gd").new()
|
||||
lg33.ap_per_round = 33
|
||||
Kit.check("越域 33 拒绝开会话(守卫上界 [1,32])〔架构整改 V5〕", lg33.start_session(42) == "")
|
||||
@@ -0,0 +1 @@
|
||||
uid://cyaq5yjgwr0ng
|
||||
@@ -318,7 +318,7 @@ func _init() -> void:
|
||||
# enc 带 feats 键+ledger,专长触发经 _judge_seal 真实封印判定管线)
|
||||
var enc_feats = {"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": {},
|
||||
"feats": ["seal_smith"], "tree_rules": [], "battle_index": 1,
|
||||
"feats": ["seal_smith"], "tree_rules": {}, "battle_index": 1,
|
||||
"bonus_ref": {"atk": 0, "max_hp": 0, "armor_class": 0}, "voucher_rap": 0, "voucher_time_sand": 0,
|
||||
"node_type": "battle", "adv_bucket": {}}
|
||||
var rng15 = load("res://core/rng_service.gd").new(); rng15.set_master_seed(341)
|
||||
|
||||
@@ -14,34 +14,45 @@ extends SceneTree
|
||||
func _init() -> void:
|
||||
var eng = load("res://core/effect_engine.gd")
|
||||
var tree_tbl = load("res://data/schemas/meta_tree_table.gd")
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
db.load_tables("res://data/tables", false)
|
||||
var errs: Array = []
|
||||
# 规则键参数字典夹具值源(R1 换代——synth 参数读 meta_tree Fork 行 effects_*,字面量退役)
|
||||
var fork_params := {}
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
for skey in ["effects_a", "effects_b"]:
|
||||
var fx: Dictionary = row.get(skey, {}) as Dictionary
|
||||
for rk in fx.keys():
|
||||
fork_params[String(rk)] = fx[rk]
|
||||
# 四键合成行夹具(与 rebuild 树规则段同构——逐字段对照,实现漂移在此转红)
|
||||
var synth := {
|
||||
"posture_first_strike": [{"on": "round_start",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": int(fork_params["posture_first_strike"]["ap_bonus"])}}],
|
||||
"posture_bulwark": [{"on": "damage_taken",
|
||||
"when": [{"fact": "battle_index", "op": "eq", "value": 1},
|
||||
{"fact": "round_index", "op": "eq", "value": 1}],
|
||||
"do": {"kind": "shield", "value": 1}}],
|
||||
"do": {"kind": "shield", "value": int(fork_params["posture_bulwark"]["shield"])}}],
|
||||
"sense_sharp": [{"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "damage_mult", "value": 50}},
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(fork_params["sense_sharp"]["cap_per_level"])}],
|
||||
"do": {"kind": "damage_mult", "value": int(fork_params["sense_sharp"]["halve_pct"])}},
|
||||
{"on": "mine_hit",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": 2}],
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_sharp_used", "op": "lt", "value": int(fork_params["sense_sharp"]["cap_per_level"])}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_sharp_used", "value": 1}}],
|
||||
"sense_guard": [{"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": 1}},
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(fork_params["sense_guard"]["cap_per_level"])}],
|
||||
"do": {"kind": "grant_ap", "mode": "queue", "value": int(fork_params["sense_guard"]["ap_refund"])}},
|
||||
{"on": "seal_missed",
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": 2}],
|
||||
"when": [{"fact": "counter", "id": "lvl_tree_sense_guard_used", "op": "lt", "value": int(fork_params["sense_guard"]["cap_per_level"])}],
|
||||
"do": {"kind": "counter_add", "counter_id": "lvl_tree_sense_guard_used", "value": 1}}],
|
||||
}
|
||||
var six := ["posture_first_strike", "posture_bulwark", "sense_sharp", "sense_guard", "economy_hoard", "economy_spend"]
|
||||
# rebuild 实装面(db=null→feat/attune 两段空转,_triggers 只含树规则合成行)——行数对照载体
|
||||
var eng_inst = eng.new()
|
||||
eng_inst.rebuild([], [], six)
|
||||
eng_inst.rebuild([], [], fork_params)
|
||||
for key in six:
|
||||
if not tree_tbl.RULE_KEYS.has(key):
|
||||
errs.append("规则键出域 RULE_KEYS 词表: %s" % key)
|
||||
@@ -64,10 +75,58 @@ func _init() -> void:
|
||||
eng.validate_trigger_row("bad_kind", {}, [{"on": "round_start", "do": {"kind": "no_such_kind"}}], bad_errs)
|
||||
if bad_errs.is_empty():
|
||||
errs.append("反例护栏失效:出域事件/出域效果类型未被 validate_trigger_row 拒绝")
|
||||
errs.append_array(_test_rule_key_params(eng, tree_tbl, db))
|
||||
if errs.is_empty():
|
||||
print("[tree_rules_v11] all green(四键合成行零违例 + 聚散两键无行成立 + 反例被拒——契约 5 闭集在册词表承载)")
|
||||
print("[tree_rules_v11] all green(四键合成行零违例 + 聚散两键无行成立 + 反例被拒 + 六键参数域全过——契约 5 闭集在册词表承载)")
|
||||
quit(0)
|
||||
return
|
||||
for e in errs:
|
||||
printerr("[tree_rules_v11] %s" % str(e))
|
||||
quit(1)
|
||||
|
||||
|
||||
# ---- 规则键参数域校验(架构整改批 R1——六键参数字典):正例全过/缺参数拒/多参数拒/越域拒/非字典拒 ----
|
||||
|
||||
func _test_rule_key_params(eng, tree_tbl, db) -> Array:
|
||||
var errs: Array = []
|
||||
var params_tbl: Dictionary = tree_tbl.RULE_KEY_PARAMS
|
||||
# 正例:meta_tree.tres 现行 Fork 行逐键过 validate(加载期已验——此处显式复核)
|
||||
var probe_ok := {"node_id": "probe_rule", "effects": {}, "effects_a": {}, "effects_b": {}}
|
||||
for row in db.crossrun.meta_tree.rows:
|
||||
if String(row.get("kind", "")) != "fork":
|
||||
continue
|
||||
for skey in ["effects_a", "effects_b"]:
|
||||
for rk in (row.get(skey, {}) as Dictionary).keys():
|
||||
probe_ok[skey][String(rk)] = (row.get(skey, {}) as Dictionary)[rk]
|
||||
errs.append_array(db.crossrun.meta_tree._validate_rule_key_params(probe_ok, "probe_ok"))
|
||||
# 负例构造:取 sense_sharp 行(双参数键——缺/多/越域/非字典四形态)
|
||||
var base_params: Dictionary = {"halve_pct": 50, "cap_per_level": 2}
|
||||
var mk := func(mod: Dictionary) -> Dictionary:
|
||||
var row := {"node_id": "probe_neg", "effects_a": {"sense_sharp": base_params.duplicate(true)}, "effects_b": {}}
|
||||
for k in mod.keys():
|
||||
row["effects_a"]["sense_sharp"] = mod[k]
|
||||
return row
|
||||
var cases := [
|
||||
{"name": "缺参数", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": {"halve_pct": 50}}, "effects_b": {}}},
|
||||
{"name": "多参数", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": {"halve_pct": 50, "cap_per_level": 2, "ghost": 1}}, "effects_b": {}}},
|
||||
{"name": "越域", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": {"halve_pct": 50, "cap_per_level": 99}}, "effects_b": {}}},
|
||||
{"name": "非字典", "row": {"node_id": "probe_neg", "effects_a": {"sense_sharp": 1}, "effects_b": {}}},
|
||||
]
|
||||
for c in cases:
|
||||
var got: Array = db.crossrun.meta_tree._validate_rule_key_params(c["row"], "probe_neg")
|
||||
if got.is_empty():
|
||||
errs.append("参数域负例未被拒(%s)" % String(c["name"]))
|
||||
# 越域判别词在 message 内(断言定位面)
|
||||
var oor: Array = db.crossrun.meta_tree._validate_rule_key_params(cases[2]["row"], "probe_neg")
|
||||
var oor_hit := false
|
||||
for e in oor:
|
||||
if str(e).contains("越域"):
|
||||
oor_hit = true
|
||||
if not oor_hit:
|
||||
errs.append("越域 message 缺判别词(断言定位面违约)")
|
||||
# R1 对照表落点:_tree_rule_dict 注入构造面最小断言(sim 单点)
|
||||
var mc = load("res://sim/adventure_monte_carlo.gd")
|
||||
var td: Dictionary = mc._tree_rule_dict(db, ["posture_first_strike"])
|
||||
if not (td.size() == 1 and (td.get("posture_first_strike", {}) as Dictionary).get("ap_bonus", -1) == 1):
|
||||
errs.append("_tree_rule_dict 注入构造面失配(应={posture_first_strike:{ap_bonus:1}}): %s" % str(td))
|
||||
return errs
|
||||
@@ -0,0 +1,69 @@
|
||||
# 致命类用例(独立进程,期望退出码 1)
|
||||
# 甲案词表退役〔架构整改 V3〕——overflow/attack_segments 两死效果通道摘出 EFFECT_KINDS:
|
||||
# 向 feats.tres 注入 do.kind="overflow" 与 do.kind="attack_segments" 探针行 → validate_trigger_row
|
||||
# 须以 EFFECT_KINDS 出域拒绝(词表闭面收窄——误写即加载致命)。任一违规被放行即回归(退出码 2)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/data_config/effect_kind_retired_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const ConfigDBScript := preload("res://core/config_db.gd")
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
|
||||
const SRC_DIR := "res://data/tables"
|
||||
const BAD_OVERFLOW_DIR := "user://effect_kind_fixture_overflow"
|
||||
const BAD_SEGMENTS_DIR := "user://effect_kind_fixture_segments"
|
||||
|
||||
# 注入探针:死通道 do.kind(甲案摘名两族——各一探针行)
|
||||
const PROBE_OVERFLOW := ' {\n "feat_id": "kind_overflow_probe",\n "display_name": "通道探针·溢出",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "overflow", "value": 50 } } ],\n "desc": "夹具探针——甲案退役词 overflow"\n},\n'
|
||||
const PROBE_SEGMENTS := ' {\n "feat_id": "kind_segments_probe",\n "display_name": "通道探针·连珠",\n "rarity": "common",\n "triggers": [ { "on": "dig_reveal", "when": [], "do": { "kind": "attack_segments", "cap": 2, "divisor": 3, "value": 0 } } ],\n "desc": "夹具探针——甲案退役词 attack_segments"\n},\n'
|
||||
|
||||
|
||||
func _inject(dir: String, probe: String) -> bool:
|
||||
var path := dir + "/feats.tres"
|
||||
var f := FileAccess.open(path, FileAccess.READ)
|
||||
if f == null:
|
||||
push_error("注入失败:读不到 %s" % path)
|
||||
return false
|
||||
var s := f.get_as_text()
|
||||
f.close()
|
||||
var anchor := "rows = ["
|
||||
var idx := s.find(anchor)
|
||||
if idx < 0:
|
||||
push_error("注入失败:锚点 '%s' 未找到(len=%d)" % [anchor, s.length()])
|
||||
return false
|
||||
var cut := idx + anchor.length()
|
||||
s = s.substr(0, cut) + "\n" + probe + s.substr(cut)
|
||||
var w := FileAccess.open(path, FileAccess.WRITE)
|
||||
if w == null:
|
||||
push_error("注入失败:写不开 %s" % path)
|
||||
return false
|
||||
w.store_string(s)
|
||||
w.close()
|
||||
return true
|
||||
|
||||
|
||||
func _assert_rejected(bad_dir: String, probe: String, label: String) -> bool:
|
||||
# 返回 true=正确拒绝;false=放行或夹具失败(调用方据此退出 2)
|
||||
if not Kit.copy_tables(SRC_DIR, bad_dir) or not _inject(bad_dir, probe):
|
||||
push_error("[%s] 夹具目录准备失败" % label)
|
||||
return false
|
||||
var db = ConfigDBScript.new()
|
||||
var ok: bool = db.load_tables(bad_dir, false)
|
||||
if ok:
|
||||
push_error("[%s] 违规表被放行——EFFECT_KINDS 出域校验失守(回归)" % label)
|
||||
return false
|
||||
if db.diagnostics_count("error") < 1:
|
||||
push_error("[%s] 加载被拒但零 error 诊断——校验通道静默化(回归)" % label)
|
||||
return false
|
||||
print("[%s] 正确拒绝(error 诊断在案——EFFECT_KINDS 出域)" % label)
|
||||
return true
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
if not _assert_rejected(BAD_OVERFLOW_DIR, PROBE_OVERFLOW, "effect_kind_retired_test/①overflow 出域"):
|
||||
quit(2)
|
||||
return
|
||||
if not _assert_rejected(BAD_SEGMENTS_DIR, PROBE_SEGMENTS, "effect_kind_retired_test/②attack_segments 出域"):
|
||||
quit(2)
|
||||
return
|
||||
print("[effect_kind_retired_test] 双探针均正确拒绝(甲案词表退役——死通道误写加载致命)〔架构整改 V3〕")
|
||||
quit(1)
|
||||
@@ -0,0 +1 @@
|
||||
uid://pgo3ndugo2w1
|
||||
@@ -97,6 +97,21 @@ func _test_price_ladder(db) -> void:
|
||||
prices == [20, 30, 45, 67, 101])
|
||||
Kit.check("已满级(owned=5)再购价照算 151(cap 闸归写口,价格函数纯几何)〔锻造工坊批 V2〕",
|
||||
ForgeTableScript.level_price(row, 5) == 151)
|
||||
# 六路线全量梯(断言补齐批 V11——floor 公式按表值推算不写死表值、逐级严格递增、满级档价=上限档推算值)
|
||||
var all_ladder_ok := true
|
||||
for frow in db.crossrun.forge.routes:
|
||||
var base: int = int(frow["base_price"])
|
||||
var ratio: int = int(frow["price_ratio_pct"])
|
||||
var cap: int = int(frow["cap"])
|
||||
var prev := 0
|
||||
for owned in range(cap + 1):
|
||||
var expect: int = int(floor(float(base) * pow(float(ratio) / 100.0, float(owned))))
|
||||
if ForgeTableScript.level_price(frow, owned) != expect:
|
||||
all_ladder_ok = false
|
||||
if owned > 0 and expect <= prev:
|
||||
all_ladder_ok = false # 逐级严格递增(ratio>100 几何保证)
|
||||
prev = expect
|
||||
Kit.check("六路线全量梯:floor 公式表值推算逐级严格递增、满级档价=上限档推算值〔断言补齐批 V11〕", all_ladder_ok)
|
||||
|
||||
|
||||
# ---- 负向:出域行逐类构造各一行全部被拒(V1)----
|
||||
@@ -199,7 +214,7 @@ func _test_negative_cases() -> void:
|
||||
# 现行 31;本套件零扩签,钉现行数(将来任何加词即转红提示复核)。
|
||||
|
||||
func _test_effect_kinds_closed() -> void:
|
||||
Kit.check("效果引擎效果类型闭集 34 种(迭代 46 换图系扩签 grant_reserve_ap/apply_discount 换代)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EFFECT_KINDS.size() == 34)
|
||||
Kit.check("效果引擎效果类型闭集 32 种(甲案词表退役——overflow/attack_segments 摘名)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EFFECT_KINDS.size() == 32)
|
||||
Kit.check("效果引擎事件词表在册 22 种(迭代 46 换图系扩签 refresh_request/board_refreshed 换代)〔锻造工坊批 V11〕",
|
||||
EffectEngineScript.EVENTS.size() == 22)
|
||||
@@ -26,7 +26,8 @@ func _row(L: int, layers: int, bd: float, ratios: Array, entry: int, exit_c: int
|
||||
return {"level_index": L, "layers": layers, "branch_density": bd,
|
||||
"battle_ratio": ratios[0], "reward_ratio": ratios[1], "rest_ratio": ratios[2], "shop_ratio": ratios[3],
|
||||
"entry_count": entry, "exit_count": exit_c, "battle_floor": floor_n,
|
||||
"fork_ways_min": 2, "fork_ways_max": 3}
|
||||
"fork_ways_min": 2, "fork_ways_max": 3,
|
||||
"fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4}
|
||||
|
||||
|
||||
func _table(rows: Array) -> Variant:
|
||||
|
||||
@@ -42,6 +42,9 @@ func _init() -> void:
|
||||
_smoke_ac_swap_guard()
|
||||
_smoke_enemy_bands()
|
||||
_smoke_variation_framework()
|
||||
_test_level_gen_new_fields()
|
||||
_test_variation_header_domains()
|
||||
_test_feat_cap_required()
|
||||
_smoke_difficulty_table()
|
||||
_smoke_nine_types_gp_stream()
|
||||
_smoke_adventure_table()
|
||||
@@ -529,3 +532,87 @@ func _conversion_row_of(rows: Array, attr: String) -> Dictionary:
|
||||
if String(r.get("attr", "")) == attr:
|
||||
return r
|
||||
return {}
|
||||
|
||||
|
||||
# ---- V7 节点图三新键域校验(架构整改——缺新键行拒/width_max 越域拒/两 pct 越域拒/表值全过)----
|
||||
|
||||
func _test_level_gen_new_fields() -> void:
|
||||
var tbl = _db.run.level_gen_params
|
||||
var mk := func(mod: Dictionary) -> Dictionary:
|
||||
var row := {"level_index": 1, "layers": 6, "branch_density": 0.3, "battle_ratio": 0.5, "reward_ratio": 0.2,
|
||||
"rest_ratio": 0.2, "shop_ratio": 0.1, "entry_count": 1, "exit_count": 1, "battle_floor": 4,
|
||||
"fork_ways_min": 2, "fork_ways_max": 3, "fork_third_way_pct": 35, "fill_rest_first_pct": 50, "width_max": 4}
|
||||
for k in mod.keys():
|
||||
if bool(mod[k]):
|
||||
row.erase(k)
|
||||
else:
|
||||
row[k] = mod[k]
|
||||
return row
|
||||
var inst = load("res://data/schemas/level_gen_params.gd").new()
|
||||
# 表值 35/50/4 全过
|
||||
inst.rows.append(mk.call({}))
|
||||
Kit.check("V7 表值行全过(35/50/4)〔架构整改 V7〕", inst.validate().is_empty())
|
||||
# 缺新键行拒(structural 键集闭集)
|
||||
var missing: Dictionary = mk.call({"fork_third_way_pct": true, "fill_rest_first_pct": true, "width_max": true})
|
||||
inst.rows = [] as Array[Dictionary]
|
||||
inst.rows.append(missing)
|
||||
Kit.check("V7 缺新键行拒(ROW_KEYS 闭集)〔架构整改 V7〕", not inst.validate().is_empty())
|
||||
# width_max 越域拒
|
||||
inst.rows = [] as Array[Dictionary]
|
||||
inst.rows.append(mk.call({"width_max": 7}))
|
||||
Kit.check("V7 width_max=7 越域拒〔架构整改 V7〕", not inst.validate().is_empty())
|
||||
# 两 pct 越域拒
|
||||
inst.rows = [] as Array[Dictionary]
|
||||
inst.rows.append(mk.call({"fork_third_way_pct": 101}))
|
||||
Kit.check("V7 fork_third_way_pct=101 越域拒〔架构整改 V7〕", not inst.validate().is_empty())
|
||||
inst.rows = [] as Array[Dictionary]
|
||||
inst.rows.append(mk.call({"fill_rest_first_pct": -1}))
|
||||
Kit.check("V7 fill_rest_first_pct=-1 越域拒〔架构整改 V7〕", not inst.validate().is_empty())
|
||||
|
||||
|
||||
# ---- 变异表头域校验 / feat cap 必填(架构整改——丁案三件的数据面守卫)----
|
||||
|
||||
func _test_variation_header_domains() -> void:
|
||||
var inst = load("res://data/schemas/variation_table.gd").new()
|
||||
inst.percent_of_cells = 5
|
||||
inst.per_level_bonus = 0
|
||||
inst.pack_max = 4
|
||||
inst.pack_min = 2
|
||||
Kit.check("V11 表值头全过(5/0/4/2)〔架构整改 V11〕", inst.validate().is_empty())
|
||||
inst.pack_min = 5 # > pack_max=4 → 拒
|
||||
Kit.check("V11 pack_min>pack_max 拒〔架构整改 V11〕", not inst.validate().is_empty())
|
||||
inst.pack_min = 2
|
||||
inst.percent_of_cells = 51 # > 50 → 拒
|
||||
Kit.check("V11 percent_of_cells=51 拒〔架构整改 V11〕", not inst.validate().is_empty())
|
||||
inst.percent_of_cells = 5
|
||||
inst.pack_max = 10 # > 9 → 拒
|
||||
Kit.check("V11 pack_max=10 拒〔架构整改 V11〕", not inst.validate().is_empty())
|
||||
|
||||
|
||||
func _test_feat_cap_required() -> void:
|
||||
var inst = load("res://data/schemas/feat_table.gd").new()
|
||||
# 生产表全行 cap 在场(加载期已验——此处显式复核 37 行)
|
||||
var all_cap := true
|
||||
for row in _db.crossrun.feats.rows:
|
||||
if not row.has("cap"):
|
||||
all_cap = false
|
||||
elif int(row["cap"]) < 1 or int(row["cap"]) > 99:
|
||||
all_cap = false
|
||||
Kit.check("V12 生产 feats 表全行 cap 在场且在域 [1,99]〔架构整改 V12〕", all_cap and _db.crossrun.feats.rows.size() > 0)
|
||||
# 缺 cap 行拒
|
||||
var mk := {"feat_id": "probe_cap", "display_name": "探针", "rarity": "common", "triggers": []}
|
||||
var no_cap := mk.duplicate(true)
|
||||
inst.rows.append(no_cap)
|
||||
Kit.check("V12 缺 cap 行拒(必填——零兜底)〔架构整改 V12〕", not inst.validate().is_empty())
|
||||
# 越域拒
|
||||
var oor := mk.duplicate(true)
|
||||
oor["cap"] = 100
|
||||
inst.rows = [] as Array[Dictionary]
|
||||
inst.rows.append(oor)
|
||||
Kit.check("V12 cap=100 越域拒〔架构整改 V12〕", not inst.validate().is_empty())
|
||||
# cap_ok 直索引零兜底:在场行读 cap(relic 面真实入口)
|
||||
var deck = load("res://core/curio_deck.gd").new(_db, load("res://core/rng_service.gd").new())
|
||||
var some_feat: Dictionary = _db.crossrun.feats.rows[0]
|
||||
var cap_v: int = int(some_feat["cap"])
|
||||
Kit.check("V12 cap_ok 在场行直索引读 cap(首行 %s cap=%d——零兜底路径)〔架构整改 V12〕" % [String(some_feat["feat_id"]), cap_v],
|
||||
deck.cap_ok("relic", String(some_feat["feat_id"])) == (int(deck.taken.get("relic:" + String(some_feat["feat_id"]), 0)) < cap_v))
|
||||
@@ -23,6 +23,7 @@ func _init() -> void:
|
||||
_test_phase_text_nonempty()
|
||||
_test_boss_name_uses_boss_rowset()
|
||||
_test_level_clear_wording()
|
||||
_test_snapshot_effects_per_unit()
|
||||
quit(Kit.finish("battle_narrator_test"))
|
||||
|
||||
|
||||
@@ -99,3 +100,39 @@ func _test_level_clear_wording() -> void:
|
||||
{"cleared": false, "result": "failed", "levels_cleared": 1, "stopped_at": 2, "keystone_total": 4, "master_seed": 42})
|
||||
Kit.check("冒险摘要完成关数行(实际「%s」)〔口径换代 V3〕" % summary,
|
||||
summary.contains("完成 1 关 · 止步第 2 层") and not summary.contains("通关"))
|
||||
|
||||
|
||||
# ---- 呈现各读各的(增益状态 v1.5——敌方状态个体化批)----
|
||||
# 真实入口=呈现状态快照函数 presentation_snapshot:A 有削弱 B 无 → 两卡投影各现各的(零共享引用)
|
||||
|
||||
func _test_snapshot_effects_per_unit() -> void:
|
||||
var db = load("res://core/config_db.gd").new()
|
||||
Kit.check("V8 前置:表加载零 error", db.load_tables("res://data/tables", true) and db.diagnostics_count("error") == 0)
|
||||
var rng = load("res://core/rng_service.gd").new()
|
||||
rng.set_master_seed(20261103)
|
||||
var settlement = load("res://core/battle_orchestrator.gd").new(db, rng)
|
||||
# depth 3 = hobgoblin 行集 2-3 只——两卡对照面
|
||||
var open_ok: bool = settlement.begin_battle({"level_index": 1, "node_depth": 3, "hero_hp": int(db.combat.hero.max_hp)})
|
||||
Kit.check("V8 前置:开局(L1 depth3 双敌行集)", open_ok)
|
||||
if not open_ok:
|
||||
return
|
||||
var enemies: Array = settlement._enemy_system.all_enemies()
|
||||
var dual_ok: bool = enemies.size() >= 2
|
||||
Kit.check("V8 前置:≥2 敌(两卡对照面)", dual_ok)
|
||||
if not dual_ok:
|
||||
return
|
||||
var sc = load("res://core/status_container.gd")
|
||||
var src := {"source_class": "seal_hit", "origin_cell": Vector2i(-1, -1), "round_index": 0}
|
||||
Kit.check("V8 前置:A 敌受理削弱(B 敌不授)", settlement._status_effects.apply_effect("weaken", sc.owner_key_for(enemies[0]), src))
|
||||
var snap: Dictionary = settlement.presentation_snapshot()
|
||||
var e1_effects: Array = snap.enemies[0].effects
|
||||
var e2_effects: Array = snap.enemies[1].effects
|
||||
var e1_has := false
|
||||
for e in e1_effects:
|
||||
if String(e.effect_id) == "weaken":
|
||||
e1_has = true
|
||||
var e2_has := false
|
||||
for e in e2_effects:
|
||||
if String(e.effect_id) == "weaken":
|
||||
e2_has = true
|
||||
Kit.check("A 有削弱 B 无 → 两卡投影各现各的(A 卡现削弱、B 卡无同款)〔个体化批 V8〕", e1_has and not e2_has)
|
||||
@@ -36,6 +36,7 @@ func _init() -> void:
|
||||
_sweep_source_names()
|
||||
_sweep_board_glyph()
|
||||
_sweep_token_scans()
|
||||
_sweep_refresh_toast_no_literals()
|
||||
quit(Kit.finish("player_copy_sweep_test"))
|
||||
|
||||
|
||||
@@ -156,6 +157,27 @@ func _sweep_token_scans() -> void:
|
||||
orch_hits == 0)
|
||||
|
||||
|
||||
# 刷新两 toast 文案零数字字面量(架构整改 R4——格式串零数字、插值位与表字段一一对应;
|
||||
# 静态判据+评审注记观测面局限:真机文案随表值由实机目检兜——T10 清单两项)
|
||||
func _sweep_refresh_toast_no_literals() -> void:
|
||||
var main_src := FileAccess.get_file_as_string("res://presentation/main.gd")
|
||||
var rap_toast := ""
|
||||
var cap_toast := ""
|
||||
for line in main_src.split("
|
||||
"):
|
||||
if "弃图刷新需" in line:
|
||||
rap_toast = line
|
||||
if "本轮刷新次数已用完" in line:
|
||||
cap_toast = line
|
||||
Kit.check("V4 前置:两 toast 行在场(源锚)", rap_toast != "" and cap_toast != "")
|
||||
# RAP 不足 toast:格式串零数字字面量(「需 %d」插值读 rap_refresh_cost 表值)
|
||||
var rap_fmt_bad := "(弃图刷新需 8)" in rap_toast or "%d 点(弃图刷新需 %d)" not in rap_toast
|
||||
Kit.check("RAP 不足 toast:格式串零数字字面量、插值位=cost 表值拼接〔架构整改 V4〕", not rap_fmt_bad)
|
||||
# 轮上限 toast:文案零数字字面量(「每轮 %d 次」插值读 refresh_cap_per_round 表值)
|
||||
Kit.check("轮上限 toast:零数字字面量、插值位=refresh_cap_per_round 表值拼接〔架构整改 V4〕",
|
||||
"(每轮 2 次)" not in cap_toast and "每轮 %d 次" in cap_toast)
|
||||
|
||||
|
||||
func _first_label(node: Node) -> Label:
|
||||
for child in node.get_children():
|
||||
if child is Label:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# tests/unit/presentation/settings_store_test.gd — 设置存储单元契约(回读/缺省/应用链)
|
||||
# 断言面 = SettingsStore 公开方法:save_settings/load_settings 持久化回读、缺文件出厂默认、
|
||||
# apply() 引擎档应用(UiKit.motion_scale 双态)。隔离卫生:一律经 _init path 注入指向临时 cfg,
|
||||
# 禁碰真实 user://settings.cfg(ui_click_probe 设置面段同款纪律)。
|
||||
# 运行:godot --headless --path game --script res://tests/unit/presentation/settings_store_test.gd
|
||||
extends SceneTree
|
||||
|
||||
const Kit := preload("res://tests/unit/test_kit.gd")
|
||||
const StoreScript := preload("res://presentation/settings_store.gd")
|
||||
|
||||
const ISO_PATH := "user://settings_store_unit_test.cfg" # 隔离注路径(临时件——测毕清理)
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_test_settings_roundtrip()
|
||||
_test_settings_default()
|
||||
_test_settings_apply_chain()
|
||||
_cleanup()
|
||||
quit(Kit.finish("settings_store_test"))
|
||||
|
||||
|
||||
func _cleanup() -> void:
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(ISO_PATH))
|
||||
|
||||
|
||||
# ---- 持久化回读(写 true→存→新实例回读 true;节/键 motion/reduced)----
|
||||
|
||||
func _test_settings_roundtrip() -> void:
|
||||
var w = StoreScript.new(ISO_PATH)
|
||||
w.reduced_motion = true
|
||||
Kit.check("前置:save_settings 落盘成功", w.save_settings())
|
||||
var r = StoreScript.new(ISO_PATH)
|
||||
r.load_settings()
|
||||
Kit.check("持久化回读:新实例回读 reduced_motion=true(motion/reduced 节键)〔断言补齐批 V6〕", r.reduced_motion == true)
|
||||
w.reduced_motion = false
|
||||
Kit.check("回写往返:改 false 再存再读得 false〔断言补齐批 V6〕", w.save_settings() and (func():
|
||||
var r2 = StoreScript.new(ISO_PATH)
|
||||
r2.load_settings()
|
||||
return r2.reduced_motion == false).call())
|
||||
|
||||
|
||||
# ---- 缺省值(无文件缺省 false)----
|
||||
|
||||
func _test_settings_default() -> void:
|
||||
var absent_path := "user://settings_store_unit_absent_%d.cfg" % (Time.get_ticks_msec())
|
||||
DirAccess.remove_absolute(ProjectSettings.globalize_path(absent_path)) # 确保无文件
|
||||
var d = StoreScript.new(absent_path)
|
||||
d.load_settings()
|
||||
Kit.check("无文件缺省:reduced_motion=false(缺文件=出厂默认合法态)〔断言补齐批 V7〕", d.reduced_motion == false)
|
||||
|
||||
|
||||
# ---- 引擎档应用链(reduced→apply()→UiKit.motion_scale==0.5,false→1.0;测毕还原静态初值)----
|
||||
|
||||
func _test_settings_apply_chain() -> void:
|
||||
var saved_scale: float = UiKit.motion_scale # 静态初值留底——测毕还原
|
||||
var a = StoreScript.new(ISO_PATH)
|
||||
a.reduced_motion = true
|
||||
a.apply()
|
||||
Kit.check("应用链:reduced=true → UiKit.motion_scale==0.5〔断言补齐批 V8〕", UiKit.motion_scale == 0.5)
|
||||
a.reduced_motion = false
|
||||
a.apply()
|
||||
Kit.check("应用链:reduced=false → UiKit.motion_scale==1.0〔断言补齐批 V8〕", UiKit.motion_scale == 1.0)
|
||||
UiKit.motion_scale = saved_scale # 还原(静态共享面——不渗入其他套件)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bqscskbjj026u
|
||||
@@ -3,6 +3,7 @@
|
||||
> **性质**:设计讨论段过程稿(审计读数)。`game/` 全目录只读架构审查的发现清单与证据,作为写卡段、工程改动批与文档设计批的输入材料;结论被吸收进权威文档与修复批后按「吸收即死」退役。
|
||||
> **范围**:core 44 件、presentation 71 件、data 24 schema + 19 表、sim 15 件、probe 19 件、tests 69 件、`tools/` 闸件。判据双轨=项目自有规范(`design/engineering/architecture/architecture.md` v1.24 + 五份工程规约)+ Godot 4.7 最佳实践。
|
||||
> **方式**:分层依赖方向/数据管线/测试覆盖三路并行静态取证+机械 grep 复核。**零测试执行、零引擎运行、零闸执行**(测试两入口铁规),全部结论来自静态读盘;下文每条命中均给 文件:行号,抽查原文已核。
|
||||
> **处置状态(2026-11-03 更新)**:§三 P0(3.1/3.2)已随迭代 50 三卡合回(合并提交 8d3a12ec)。**§四 P1 四条全部闭合**,含三处子项一并落笔:4.1 表 presentation 行(零动作,豁免通道限定用途已在册)、4.2 表 S7 行的私有成员直取面(缺口在册、移交 §5.1,不属 §四)、4.4 末「低覆盖另计四件」(零动作,能力清单已承接)——裁决、读数订正与已落地闸件见各节「2026-11-03」标注。**§五 P2 四项未处置**、§六待裁点⑦未落文,**本报告禁退役**(「吸收即死」判据未满足:结论未全吸收进权威文档)。登记位现不在原「架构审查优化讨论」条目(该条随迭代 50 收尾按状态机出口删除),改在待办清单「架构审查余项——闸面缺口、豁免存废、零测试点名与架构文档漂移」(2026-11-03 批次移交新立)。
|
||||
|
||||
---
|
||||
|
||||
@@ -102,12 +103,18 @@ core 内干净,绕过点全在 core 之外:
|
||||
| 层 | 位置 | 形态 | 是否被闸扫到 |
|
||||
|---|---|---|---|
|
||||
| presentation | `main.gd:914` | `randi()` 建种子,行内标 `rng-seed-exempt` | 扫到但被豁免标记排除 |
|
||||
|
||||
**上表 presentation 行的处置(2026-11-03)**:零动作、维持现状。该处在产品码扫描面**内**,已被行内注记在册豁免,且豁免通道的限定用途已由 code-conventions §4 第 10 条写死=非玩法 RNG 消费面的引擎熵(组合根建种子不进玩法封闭流),与本节「三目录定种自建属合法测量口径」同一条理,不需再动。
|
||||
| probe | `ui_click_probe.gd:20` 等 8 处 | `RandomNumberGenerator.new()` | **不扫** |
|
||||
| sim | `calibrate_boards.gd:54`、`experiment_density.gd:27`、`damage_probe.gd:274`、`level_autoplay.gd:388` | 自建 RNG + 显式定种 | **不扫** |
|
||||
| tests | 3 处测试夹具 | 自建 RNG | **不扫** |
|
||||
|
||||
全部绕过点均显式定种、无 `randomize()`(全库零命中),确定性尚存;问题在于**没有任何闸防它扩散**——S1/S2 扫描面只有 `DIR_CORE` + `DIR_PRES`(`static_scan.sh:20-21, 88-91`)。
|
||||
|
||||
**2026-11-03 裁(不扩扫描面,改为写说明)**:
|
||||
- **裁决**:S1/S2 扫描面**保持产品码两目录**,不扩 sim/probe/tests。这三目录自建且显式定种的随机源是**合法测量口径**(改前改后读数要用同种子对拍,属全项目测量前提),把它们判进「玩法代码禁自建 RNG」的闸面,等于给每处合法用法挂 `rng-seed-exempt` 注记、只拦得住新增——不值其成本。落文=`design/engineering/code-conventions.md` §4 新增第 13 条(按来源分档:三目录各由哪条运行期通道承接、允许自建的条件是不定种禁止、以及该禁止目前无机器闸只由文字纪律+批末独立验收审计人工评审承载),架构文档 S 表 S1 行加同口径注。
|
||||
- **读数订正(2026-11-03 复核)**:①自建点由本节记的「8 处」降到**7 处**——`game/sim/experiment_density.gd` 已随迭代 50 卡片 T9 删除(sim 现 3 处:calibrate_boards/damage_probe/level_autoplay;probe 1 处 ui_click_probe;tests 3 处 board_gen_reward/player_copy_sweep/theme_projection)。②「无 `randomize()`」要加限定:`randomize()` 在 `game/` 字面命中 2 处,两处均在注释里(`core/rng_service.gd:3`、`presentation/main.gd:756`),**实际调用零命中**,结论不变。③按 S1 原判据(裸 `randi()`/`randf()`/`randf_range()`)扫这三目录,非注释命中 **0 处**。
|
||||
|
||||
### 4.2 静态闸扫描面结构性缺口
|
||||
|
||||
`game/tests/static_scan.sh` 的 S1–S13 扫描面实况:
|
||||
@@ -127,6 +134,17 @@ core 内干净,绕过点全在 core 之外:
|
||||
|
||||
净效果:**sim / probe / tests 三层除 S11/S12 外,零类型闸、零质量闸、零 RNG 闸、零 await 闸。**
|
||||
|
||||
**2026-11-03 裁(三面不扩;两处机制修与两处本轮新查)**:
|
||||
- **S9/S10/S13 不扩到三层**(裁决)。2026-11-03 只读数实跑(三个静态扫描脚本对三层,未修文件、未写基线)给出现存总量 **≈2162 处**:S9 类型 1070(sim 158/probe 130/tests 782,其中无类型容器 859)、S10 文件头 117、S13 质量 975(其中「禁调试打印」274+「循环内 await」70——这两类在测试与探针码里是功能需要,不是债)。产品码面 S9/S10 现行**归零**,扩面即把「归零」这一执法前提换成 1187 项存量红;为三层另建欠账基线会把账从 1 份变 3 份。落文=code-conventions §4 第 13 条写明适用域止于产品码并给出三目录的运行期承接通道。
|
||||
- **读数是**:S8 现行 **14 文件**显式清单(`static_scan.sh:317`),非本节所记 9 文件;同口径 14 文件清单在 S7 另抄一份(`:361`),两份靠人工同步;绿文案仍报「B1 文件集 10 个 .gd」(`:337`)与头注 `:311` 的「共 14 文件」自相矛盾;S8 自检只防「清单内文件缺位」(`:319-324`)不防「该进的没进」——纯目录域方案已证伪:装板/授予/效果端口/掷格四件结算码在 `game/core` 根下而非 `settlement/` 目录内。
|
||||
- **S7 行缺口的尾巴(本表 §4.2 里唯一指向 §五的一格)**:该行写「S7 不管私有成员直取面(见 5.1)」。§四 对 S7 只裁它自己的扫描面与清单形态——扫描面维持 `$DIR_CORE`、豁免名单与 S8 并成单点 `B1_SET`(已落地);私有成员直取面**不在本节处置范围**:它无静态判据可造(GDScript 无编译期访问检查,误报面大),且真要做是改 `core/settlement/` 的协作契约=游戏代码=需求改动通道,归 §5.1 与待办清单「架构审查余项」条目之后的讨论段。本节对此格的结论=**缺口在册、移交 §5.1,不在 §四 闭合**。
|
||||
- **本轮新查两处**(审计未列):①S9/S10 **无扫描面空转自检**(`tools/audit_type_hints.py:82-83`、`tools/audit_file_headers.py:64-65` 目录不存在即静默 PASS),违背本文「三层承载与判定原则」的零命中自检要求与脚本前言自立的防空转纪律(`static_scan.sh:18-19`);②S3 是半闸(命中只打 WARNING、不置 fail,`static_scan.sh:110`)——**但这条不是洞**:架构文档 S 表 S3 行早已登记「warning 级——class_name 直引误报需人工复核」,属已在册状态。2026-11-03 裁**维持 warning 不升硬**(升硬前提是先有可靠的直引误报排除判据,该判据未立)。
|
||||
- **待执行清单(2026-11-03 同日执行完毕,凭据见下)**:
|
||||
1. `game/tests/static_scan.sh`:S8 新增「`game/core/settlement/` 实有 `.gd` ⊆ 清单」反向对账(新段类未登记即红)+S7 的 `S7_B1_SET` 与 `S8_FILES` 两份 14 文件抄本合一为单点 `B1_SET`+绿文案「10 个 .gd」改为派生计数(现打「14 个 .gd+settlement/ 目录域已反向对账」)——**已落地**;
|
||||
2. `tools/audit_type_hints.py`/`tools/audit_file_headers.py`:补扫描面空转自检(缺目录或零个 `.gd` 即红,绿时打印扫描面件数供核)——**已落地**(负证:把面清单改成不存在目录,两件各返回退出码 1;正证:真实面 158 个 `.gd` 归零 PASS);
|
||||
3. 架构文档 S 表同步——S8 行登记单点与反向对账判据、原「S9/S10 缺自检」偏差句改判为已清偿——**已落地**(凭据=`design/engineering/architecture/architecture.md` v1.34)。
|
||||
- **验证口径**:`bash game/tests/static_scan.sh` 全量 13 条规则绿、退出码 0;车道=会话内独立 worktree 直改+合回远端 CI(零需求卡片,纯闸件面属工程改动)。§四 P1 至此四条全部闭合。
|
||||
|
||||
### 4.3 S13 豁免常态化
|
||||
|
||||
三处豁免全落在体量最大、风险最高的文件上:
|
||||
@@ -137,6 +155,8 @@ core 内干净,绕过点全在 core 之外:
|
||||
| `presentation/main.gd` | 46 个句柄成员 | 同上 |
|
||||
| `presentation/tune_sim_controller.gd` | await 使用 | 同上 |
|
||||
|
||||
**2026-11-03 结案(待裁点⑤已无对象,读数过期)**:本节所记三处豁免已于 **2026-10-31 迭代 44 批后收尾**清偿(commit 9eedf896「gdstyle 基线 refresh 收账 16→5 键:消失=main.gd49/orchestrator38+文件长 1035/tune_sim await…五键清偿」)。现行基线只剩 5 条键,全在 `adventure_runner`/`level_runner`/`board_view`(`tools/gdstyle_baseline.txt:8-12`),判定机制是「基线外新键红、同键数值恶化红、只准因还债下降」(同文件 :5)。零动作,不再叠清偿期限——「只降不升+refresh 骤降/抬升双守卫」已在位,加期限属重复建设。
|
||||
|
||||
### 4.4 四个 core 文件零测试点名
|
||||
|
||||
判定口径:文件名与 `class_name` 在 `game/tests/` 全域 grep 均零命中。
|
||||
@@ -152,6 +172,14 @@ core 内干净,绕过点全在 core 之外:
|
||||
|
||||
低覆盖(仅 1 个测试文件点名)另计:`hero_stats`、`skill_lottery`、`shop_session`、`curio_deck`。
|
||||
|
||||
**2026-11-03 裁(定性改判+读数订正为 11 件;落文=规约登记义务,不建闸不补空套件)**:
|
||||
- **定性改判**:本节「零测试点名」的风险叙述不成立——四件都有有针对性断言,只是不经文件名的通道:MetaStateStore 经 `meta_state.` 键散布 14 个测试文件、SlotStore 经 `.slots.attach_path/attach_slot_template` 在 system_test 与 save_slot_isolation/summary/boot_no_activate 四套件、BattleFxHost 经 `fx_host()` 在 adventure_feats_test.gd:584-661、AttuneLoadout 经 `_loadout.set_attunement` 在 feat_vouchers_test.gd:80-83。真缺口是**没有「按文件登记覆盖」的惯例**,不是没测。
|
||||
- **读数订正**:按本节同口径(`game/tests/**/*.gd` 全域 grep basename 与 class_name)复跑,core 现 52 件里零点名是 **11 件**,不是 4 件。分三档:纯零 6 件(attune_loadout 117 行/slot_store 189 行/config_domain_combat·crossrun·presentation·run 四件 7–12 行纯持有容器);只在闸清单出现 4 件(battle_fx_host 232 行〔本节记 179,已涨〕/reward_roller 245/round_cycle 106/battle_refresh_flow 43);只在能力清单文字出现 1 件(meta_state_store 176)。**成因读数**:后一档三件全是迭代 44/46/49 的拆件产物——「拆件产新件不登记」是复发模式,不是一次性欠账。
|
||||
- **裁否两案**:①「立每个 core 件必须被点名一次的覆盖闸」——与本文档族的 `test-conventions.md` §7「覆盖率不是目标:不引入行覆盖率度量;现行补洞=能力清单覆盖域表+人工判定」正面冲突,且会把为点名而点名的空套件逼出来;②「给 11 件补点名套件」——同既有断言重复,属为登记而写码。
|
||||
- **落文**:`design/engineering/test-conventions.md` §8.3 新增「新增 core 件的覆盖登记义务」(新 core 件要么被 tests 面点名、要么在能力行申报「经宿主链覆盖(宿主=<件>)」,两者都无=收尾核对补登;明示本条不是覆盖率要求),`design/engineering/process-conventions.md` §3 第 6 条加承接指向(拆件套的覆盖登记归该义务,核对动作归收尾提交)。
|
||||
- **落笔约束**:能力清单 `game/tests/README.md` 本体归写卡段维护、实现批禁写(`test-conventions.md` §8.3),所以申报行由写卡段入表,本轮规约文字不代写清单内容。
|
||||
- **「低覆盖另计四件」的处置(本本节最后一句未落笔项)**:`hero_stats`/`skill_lottery`/`shop_session`/`curio_deck` 四件**零动作**。判据:低覆盖≠无覆盖,四件在能力清单各有代表用例承接(现值:`hero_stats`→hero_ledger_test 直构断言 rest_heal;`skill_lottery` 已由审计时点的 1 件升到 2 件——round_flow_skill_pool_test:798/830 与 trigger_cond_test:85/90;`shop_session`→level_shop_economy_test 经 `LevelRunner.shop.` 真实入口;`curio_deck`→pool_case_d 经 AdventureRunner),已满足新落的 §8.3 登记义务(点名或申报经宿主链覆盖二者有一即可)。要提覆盖得由写卡段在后续批次按能力行增补,不属本节闸件范围。
|
||||
|
||||
---
|
||||
|
||||
## 五、P2 — 封装与漂移
|
||||
@@ -190,6 +218,8 @@ GDScript 无编译期访问检查,私有成员改名不会有任何闸报错
|
||||
|
||||
以下均需用户裁定,Agent 不代裁:
|
||||
|
||||
**2026-11-03 状态**:①②③ 已裁、已写卡、已随迭代 50 三卡合回(凭据=`reports/处置-架构审查3.1+3.2整改-2026-11-02.md` 与合并提交 8d3a12ec);④⑤⑥ 已裁,裁决、读数订正与待执行清单见 §四 的 4.1/4.2/4.3/4.4 各节「2026-11-03」标注;⑦ **未处置**——属 §五 P2,讨论段未开工,因此本报告整体尚未闭合。
|
||||
|
||||
1. **A 类硬编码 18 处怎么修**——按改动四类归属,其中 `core/` 与 `presentation/` 的玩法数值入表属**需求改动**(写卡 + 编排),`sim/` 两处模拟口径常量属**工程改动**。两类不能混一批。
|
||||
2. **B 类结构常量要不要入表**——`MAX_WIDTH` / `CHARGE_LAYER_CAP` / `SPAWN_AREA_CELLS` / `node_map_generator` 的域字面量四组,代码自标「非玩法表值」,但 `SPAWN_AREA_CELLS` 已被 data 侧校验反向引用,是否算玩法数值需裁。
|
||||
3. **sim 平行实现的处置口径**——是收敛(sim 调 core 的推理与掷点)还是承认双口径并显式登记(避免读数混用)。四组里①②影响数值验证可信度,③④影响可维护性。
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# 需求卡片:敌方状态个体化批(2026-11-02)
|
||||
|
||||
> 一句话:把增益/减益状态实例从「阵营共享一条」改为「每只敌人各自持有」——全体授予=逐只各发一条实例,敌方增益技只强化施放者自己,个体死亡即清名下实例。
|
||||
> 来源:待办清单条目「敌方状态个体化——状态实例归属到敌人个体」(2026-11-02 登记,来源=会话提及)。
|
||||
> 审裁决:godot-master 终审通过(2026-11-02——架构四查全过,P3 一处 S9 类型标注句折入 T0-1);grilling 零待裁点(断言换代申报照卡执行、估时归迭代行)。
|
||||
|
||||
## 设计事实源
|
||||
|
||||
| 文档 | 依据 | 版本 |
|
||||
|---|---|---|
|
||||
| `design/gdd/systems/战斗循环/增益状态.md` | 硬规矩「实例按个体归属」+〈目标规则与个体归属〉节(词表四词/合并认个体/死亡即清/呈现各读各的) | v1.5(2026-11-02) |
|
||||
| `design/gdd/systems/战斗循环/敌人系统.md` | 增益技落点=施放者自己;警觉授予随动 | v1.5(2026-11-02) |
|
||||
| `design/gdd/systems/战斗循环/战斗结算.md` | 警觉按个体;AC 合流=该敌自己名下的 ac 型状态 | v1.6(2026-11-02) |
|
||||
|
||||
与待办清单条目凭据一致。
|
||||
|
||||
## 目标与完成定义
|
||||
|
||||
状态容器从「按阵营两桶」换为「按个体多桶」(主角一桶+每只敌各一桶),授予/合并/衰减/清除/兑付/呈现全链随动;敌方增益技从「全体受益」纠正为「施放者自己」。完成定义=全部断言当轮跑批执行并通过+代码域闸全绿+合回后本地 CI 全量域闸全绿+主角侧行为零变化(回归全绿)。
|
||||
|
||||
## 现状事实(代码级考证 2026-11-02,全仓审计)
|
||||
|
||||
- **容器本体**:`StatusContainer` 全战斗单例(battle_orchestrator.gd:217),实例按 `target_side` 两桶;`apply_effect` :28-49、`query_active(side)` :98-118、`decay_round` :121-134(全池统一 −1)、`notify_battle_end` :137-143。
|
||||
- **授予写点 3 处**:敌方技能增益(combat_segment.gd:84-90,落点由状态行 target_side 路由到敌方侧——现行全体共享的根);奖励格(reward_grant.gd:226-239,含 enemy_alert);直伤附带(damage_controller.gd:451 fx_apply_status)。
|
||||
- **读点**:乘区池合计 `clamped_pool` damage_controller.gd:629-637(两 side 实例混算);defender_ac 敌分支 :601-606(side 级 ac 型加到每一只敌);nest_count battle_orchestrator.gd:117-121 与 damage_controller.gd:93-98;池兑付 _pool_effective :521-527;结算快照 buffs :525-535;呈现投影 :670-692(**同一数组引用塞进每张敌卡**,standee_body.gd:80 自认「各立绘同值」)。
|
||||
- **死亡**:unit_died → damage_controller._on_unit_died :192-198(未结单据作废);状态实例不随死亡清,活到衰减或战斗终局。
|
||||
- **已正确的个体面(不动)**:技能冷却/半血闩/保底计数(unit_container.gd:99-153、:259-272);hp/armor/intent;死亡清脸呈现;顺位取靶(:358-362)。
|
||||
- **主角侧**:单单位,全链语义不受影响。
|
||||
- **中途入场**:不存在该路径(敌人开战一次性实例化,unit_container._spawn_rowset :81-118),「后援继承」问题消解。
|
||||
|
||||
## 设计契约红线
|
||||
|
||||
- 🔴 **GDD v1.5 语义逐条照搬**:实例按个体归属/全体授予=逐只各发一条/敌方增益技归施放者自己/个体死亡即清/呈现各读各的/合并只在同体内——任何偏离=L3 停卡。
|
||||
- 🔴 **status_effects.tres 行值零触碰**:合并策略、词表、时长、幅度全照旧——本批只改归属结构,不改任何状态定义。
|
||||
- 🔴 **断言换代申报制**:下方「测试削弱申报」清单之外,禁删禁改任何既有断言;实现轮撞红的侧级共享读数断言须挂对照表行换代,静默放宽=独立审计判红。
|
||||
- 🔴 **主角侧行为零变化**:attack_up/ac_up 全套既有断言回归绿;无类型键改动泄漏到主角调用面以外的公开接口。
|
||||
- 🔴 RNG 单点/Core 零 UI 依赖/fail-fast 照旧;全程零随机(禁随机摇目标)。
|
||||
- 🔴 **实现照单落名**:README 代表用例名=测试函数名唯一依据;断言尾缀〔个体化批 Vn〕全仓可 grep。
|
||||
|
||||
## 测试削弱申报(唯一允许的换代清单)
|
||||
|
||||
| 既有断言 | 换代方式 | 依据 |
|
||||
|---|---|---|
|
||||
| `roll_multiplier_test.gd:_smoke_armor_roll` V1/V2/V3(迭代 48 卡①削弱累加三断言) | 重写为个体键读数——累加/续时/覆盖时长的语义断言原样保留,只把「敌方侧查询」换成「目标个体查询」 | GDD v1.5 合并匹配认个体 |
|
||||
| `round_flow_skill_pool_test.gd:_smoke_settlement_segments` 衰减相位断言(:413-421、:449) | 全池衰减改逐桶衰减(每个存活个体恰 −1、归 0 移除),断言语义等价换代 | GDD v1.5 实例按个体归属 |
|
||||
|
||||
## 范围边界
|
||||
|
||||
**做**:容器个体化+3 写点+全部读点随动+呈现投影换代+上表申报断言换代+新断言 V1-V8。
|
||||
|
||||
**不做**:种类级目标词(不预先建面);伤害侧新目标规则(现行三词够用);主角侧任何语义变化;status_effects.tres 行值;多语言/音频(P1);mid-battle 增援(路径不存在)。
|
||||
|
||||
## 关联文档同步面
|
||||
|
||||
| 文档 | 关联点 | 同步义务 |
|
||||
|---|---|---|
|
||||
| `design/gdd/systems/战斗循环/增益状态.md` v1.5 | 〈目标规则与个体归属〉节「实装随该批」规矩行 | 实装合入后该行已过时——反哺点清单登记,用户裁决后文档批删除(实现批禁写 design/) |
|
||||
| `game/tests/README.md` | 能力行增改 | 写卡段当场落(本批卡内已列),实现照单落名 |
|
||||
|
||||
## 任务卡 T0..T3
|
||||
|
||||
**T0 容器个体化+授予路径换代(≈1 工作日)**
|
||||
|
||||
- [ ] T0-1 `status_container.gd`:实例桶键从 side 换个体——个体键约定单点定义(常量:主角=`"hero"`,敌人=`"e<生成序>"`,生成序=开战逐只编号沿用顺位轴);新桶键结构按 S9 全类型标注(godot-master 终审折入);`apply_effect(effect_id, owner_key, duration_override)`、`query_active(owner_key)`、`decay_round()`(逐桶各 −1)、`purge_owner(owner_key)` 新增(死亡清)、`notify_battle_end` 照旧全清
|
||||
- [ ] T0-2 授予写点 3 处换代:combat_segment.gd 增益技落点=施放者自己;reward_grant.gd 全体规则=循环存活敌逐只发(封印削弱/踩雷/警觉);damage_controller.fx_apply_status=被命中者个体
|
||||
- [ ] T0-3 死亡清接线:unit_died → purge_owner(死亡个体名下实例全清)
|
||||
- [ ] T0-4 断言落地 `system_contracts_test.gd`:`_test_status_individual_isolation`〔V1〕(两敌各授同名实例,层数/时长互不干扰);`_test_status_grant_all_per_unit`〔V2〕(全体授予=每只各一条、各自衰减);`_test_status_death_clears`〔V3〕(死体实例清零、他体不受影响);`_test_status_merge_per_unit`〔V4〕(同体合并照旧:累加/叠加/满层拒)
|
||||
- [ ] T0-5 换代申报落地:armor_roll V1-V3 重写(个体键读数,语义保留);`round_flow_skill_pool_test.gd` 新增 `_test_enemy_buff_targets_self`〔V5〕(蓄力只强化施放者,他敌 output 池读数不变)
|
||||
- [ ] T0-6 单元套全绿——里程碑步出口跑一次全套闸;波内修复只做出红最小单元的最窄确认
|
||||
|
||||
**T1 消费面随动(≈0.5 工作日)**
|
||||
|
||||
- [ ] T1-1 `damage_controller.gd`:`clamped_pool` 带 owner_key(主角打敌=该敌防御池;敌打主角=施放敌自己的输出池);defender_ac 敌分支只读被打者名下 ac 型实例;nest_count 两处(battle_orchestrator.gd:117-121、damage_controller.gd:93-98)按个体;_pool_effective 与结算快照 buffs 逐个体
|
||||
- [ ] T1-2 断言落地 `damage_controller_test.gd`:`_test_pool_consumption_per_unit`〔V6〕(B 敌的削弱不进 A 敌的账);`_test_defender_ac_per_unit`〔V7〕(alert 各算各的)
|
||||
- [ ] T1-3 单元套全绿(同 T0-6 措辞)
|
||||
|
||||
**T2 呈现投影+主角侧回归(≈0.5 工作日)**
|
||||
|
||||
- [ ] T2-1 battle_orchestrator.gd:670-692:enemies[].effects 每卡读自己桶(删同引用共享);结算明细归因照旧
|
||||
- [ ] T2-2 断言落地 `battle_narrator_test.gd`:`_test_snapshot_effects_per_unit`〔V8〕(A 有削弱 B 无 → 两卡投影各现各的)
|
||||
- [ ] T2-3 主角侧回归:attack_up/ac_up 全套既有断言绿;冒烟 15 项 0 失败
|
||||
- [ ] T2-4 单元套全绿(同 T0-6 措辞)
|
||||
|
||||
**T3 产出物落位卡(≈0.25 工作日)**
|
||||
|
||||
- [ ] T3-1 合并前写齐 `reports/iterN-实现报告-<日期>.md`(各卡状态+实机目检总清单〔本批玩家可见面=敌方增益图标各现各的、蓄力不再全体受益——两项入目检清单,登记不阻塞〕+实现决定记录)
|
||||
- [ ] T3-2 合并前写齐 `reports/iterN-反哺点-<日期>.md`(增益状态.md「实装随该批」行删除义务,逐条落点与依据;实现批禁写 design/ 与根规则文件)
|
||||
|
||||
## 验证闸(改动—验收对照表)
|
||||
|
||||
| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 实例按个体归属 | status_container.gd 键换代 | `_test_status_individual_isolation`〔V1〕 | 单元 | StatusContainer.apply_effect()/query_active() | 各体投影读数互不影响 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 |
|
||||
| 全体授予逐体发 | reward_grant.gd 循环授予 | `_test_status_grant_all_per_unit`〔V2〕 | 单元 | 同上 | 逐体实例数与各自衰减 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 |
|
||||
| 个体死亡即清 | unit_died→purge_owner | `_test_status_death_clears`〔V3〕 | 单元 | 同上+purge_owner() | 死体桶清零、他体不变 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 |
|
||||
| 同体合并照旧 | 合并匹配认个体 | `_test_status_merge_per_unit`〔V4〕 | 单元 | StatusContainer.apply_effect() | 同体累加/叠加/满层拒读数 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 |
|
||||
| 增益技归施放者自己 | combat_segment.gd 落点换代 | `_test_enemy_buff_targets_self`〔V5〕 | 单元 | 战斗结算编排链(技能施放) | 施放者与被他敌的池读数差 | 新增:敌方增益自体落点与乘区池按个体 |
|
||||
| 乘区池按个体 | clamped_pool 带 owner_key | `_test_pool_consumption_per_unit`〔V6〕 | 单元 | DamageController 兑付 | 被击者专属池合计 | 新增:敌方增益自体落点与乘区池按个体 |
|
||||
| AC 合流按个体 | defender_ac 敌分支 | `_test_defender_ac_per_unit`〔V7〕 | 单元 | DamageController 兑付 | 被击者自身 ac 型合计 | 新增:敌方增益自体落点与乘区池按个体 |
|
||||
| 呈现各读各的 | 投影换代 battle_orchestrator.gd | `_test_snapshot_effects_per_unit`〔V8〕 | 单元 | 呈现状态快照函数 | 两卡 effects 投影差 | 新增:状态实例按个体归属:个体隔离/全体授予逐体发/死亡即清/同体合并照旧 |
|
||||
| 申报换代两件 | armor_roll/settlement_segments 重写 | 原函数名保留(语义等价换代,尾缀随原批) | 单元 | 原真实入口 | 原判据个体化 | 既有:同名状态合并策略(叠加/累加/并存)与剩余次数语义 |
|
||||
|
||||
## 独立验收审计
|
||||
|
||||
执行体=迭代 loop 内启动的未参与实现子代理(2026-11-02 用户裁,test-conventions §8.7 默认通道);报告落 `reports/iterN-验收审计-<日期>.md`,逐行写「通过/缺漏/例外及依据」。审计四件事:逐行核对对照表(测试存在、当轮执行、尾缀可 grep、真实入口未绕改动点);抽查判据绑定面;检查测试削弱(对照本卡「测试削弱申报」两件清单);核对内部字段直查受控例外(本卡零直查行)。未过不得合并。
|
||||
|
||||
## 异常分级
|
||||
|
||||
- **L1 自救**:断言消息、局部夹具命名、循环授予的实现次序(入列顺序稳定可读照 GDD)。
|
||||
- **L2 记录继续**:撞红的其余侧级共享读数断言——按申报制挂对照表行换代,记实现决定记录。
|
||||
- **L3 停机呈报**:呈现快照无个体键承载(投影改不动=上游缺陷);purge 与断点续玩时序冲突;GDD 语义与现役观测面失配;机检键与本卡契约互斥——一律停卡退回写卡段。
|
||||
|
||||
## 代拍板白名单
|
||||
|
||||
- 测试内局部命名、常量、断言消息文本。
|
||||
- 循环授予的实现编排(按调用方给定次序入列,保证稳定可读)。
|
||||
- 个体键字符串格式的内部拼写(约定语义不变)。
|
||||
|
||||
## 待裁点
|
||||
|
||||
零待裁点,开工门=U0。
|
||||
|
||||
## 落位与可复用性预检
|
||||
|
||||
**零新增文件**:全部改造/扩展现有件——status_container.gd(本体)、combat_segment.gd/reward_grant.gd/damage_controller.gd(写点)、unit_container.gd(死亡信号既有)、battle_orchestrator.gd(读点/投影);测试扩展 system_contracts_test.gd/round_flow_skill_pool_test.gd/damage_controller_test.gd/battle_narrator_test.gd 四个既有宿主。
|
||||
|
||||
**唯一面**:个体键约定单点定义于 StatusContainer 头部(禁散落第二处拼写);purge 单点(死亡清唯一通道);无 helper 外散。
|
||||
|
||||
**分层与交互**:全链在 core 层,零 UI 依赖(headless 可跑通);呈现只经呈现状态快照函数读取,不直捅容器;RNG 零新增(本批零摇号)。
|
||||
|
||||
## 实现规范预算
|
||||
|
||||
- status_container.gd:公开方法 +1(purge_owner)、成员 +1(桶键结构)——公开方法 ≤20、成员 ≤15 内。
|
||||
- 存量触面 S13 预期:battle 域触碰文件(combat_segment/damage_controller/battle_orchestrator/reward_grant)键较登记值只降不升(clamped_pool 加参不增分支路径,投影换代删共享引用行);T0 读数步骤核对。
|
||||
|
||||
## 拆分边界纪律
|
||||
|
||||
本卡为行为重构(归属结构换代),非拆件批:禁为迎合机检保留或新增无效成员/别名;机检键与本卡契约互斥=停卡呈报,不自造第三条路。
|
||||
@@ -1,174 +0,0 @@
|
||||
# 需求卡片:断言补齐批(2026-11-02)
|
||||
|
||||
> 一句话:为①增益状态结算链、②减动效开关存储、⑤Boss 出场门控、⑥锻造工坊结构四域补自动化断言——零行为改动、纯测试补齐,同批刷新能力清单缺口口径。
|
||||
> 来源:待办清单条目「能力清单覆盖缺口补齐——六类无断言面或设计未定项」(2026-10-29 登记,来源=会话提及)。
|
||||
|
||||
## 设计事实源
|
||||
|
||||
| 域 | 设计依据(唯一事实源) | 版本凭据 |
|
||||
|---|---|---|
|
||||
| ⑥ 锻造工坊 | `design/gdd/systems/跨局循环/锻造工坊.md` 验收标准节九条判据 | v0.11(2026-11-02) |
|
||||
| ① 增益状态 | `design/gdd/systems/战斗循环/增益状态.md` 结算链规则 | v1.4 |
|
||||
| ⑤ 难度曲线与Boss | `design/gdd/systems/冒险循环/难度曲线与Boss.md` 核心规则(机制面) | v1.2 |
|
||||
| ② 减动效开关 | `design/gdd/systems/表现与支撑/设置与本地化.md`(开关承诺)+ `design/gdd/ux/battle.md`(减动效双态承诺:延迟归零同帧直落+横幅静态化、可加速不可整体跳过) | v1.1 |
|
||||
|
||||
与待办清单条目 8 凭据(实况关联文档清单)一致;增益状态.md 以本表 v1.4 为准,待办条目实况中的 v1.1 记法由本批订正。
|
||||
|
||||
## 目标与完成定义
|
||||
|
||||
为条目 8 收窄后的四域剩余真缺口补 **13 个新断言函数+2 处既有函数扩展**;断言全数通过;能力清单(`game/tests/README.md`)同步刷新(5 行新增能力行+5 行覆盖域表缺口句订正)。
|
||||
完成定义=全部断言在当轮真实跑批中执行并通过+代码域闸全绿+合回后本地 CI 全量域闸全绿。
|
||||
|
||||
## 现状事实(代码级考证,2026-11-02)
|
||||
|
||||
### ① 增益状态结算链(game/core/status_container.gd)
|
||||
|
||||
- 授予路由:`apply_effect` :28-49 按 stack_policy 三臂路由(independent :41-43/accumulate :44-45/stack :46-47)。
|
||||
- 叠层/续时:`_apply_accumulate` :60-67(命中只加时长、层数恒 1、快照沿首授);`_apply_stack` :70-80(满层拒返 false、层数与时长零写入)。
|
||||
- 衰减:`decay_round` :121-134(先兑付后衰减、每实例每结算恰 −1、归 0 移除、返回移除数 k)。
|
||||
- 清除:`notify_battle_end` :137-143(战斗结束全清);`notify_battle_start` :16-22(开域+清残留)。
|
||||
- 可观测面:返回值 bool/int+只读投影 `query_active(side)`(effect_id/layers/remaining_settlements/effective_value=L×m)。
|
||||
- 已盖面(不动):迭代 48 卡①(git 1e35ab94)已盖叠层续时 V1/V2/V3(`roll_multiplier_test.gd:_smoke_armor_roll` :160-167);五段全序与衰减相位断言(`round_flow_skill_pool_test.gd:_smoke_settlement_segments` :382-449)。
|
||||
- 剩余缺口:衰减逐值与到期移除数、战斗结束全清、满层拒绝、independent 分支、快照沿首授——全部零断言(notify_battle_end 全测试目录零调用,grep 证实)。
|
||||
|
||||
### ② 减动效开关存储(game/presentation/settings_store.gd)
|
||||
|
||||
- 独立设置存储(不进存档):user://settings.cfg 节/键 motion/reduced(读 :26、写 :33);`reduced_motion` 默认 false(:12)。
|
||||
- 引擎档应用:`apply()` :38-45(reduced → UiKit.motion_scale=0.5,否则 1.0)。
|
||||
- 已盖面(不动):探针全链(`ui_click_probe.gd` 设置面段 :310-327:开→0.5、持久化回读、关→1.0);系统测试 V2b(`system_test.gd` :166-168);单元侧排播双态(`pacing_queue_test.gd` :120-125,直拨 UiKit.motion_scale 不经 store)。
|
||||
- 剩余缺口:store 本体零单元断言(持久化回读/缺省值/应用链);README :194 缺口句仍写「仅系统测试与冒烟覆盖」,与现役探针覆盖不符——本批订正。
|
||||
|
||||
### ⑤ Boss 出场门控(机制面)
|
||||
|
||||
- 门控链:`node_map_generator.gd` `_build_region` :338-352(每区域 boss 深度=L−r、恰一终点)→ `level_runner.gd:546` node_type 入参 → `battle_orchestrator.gd:283` row_kind 传参 → `config_db_resolver.gd:33-67` 行类路由(越域 fatal :41-43)→ boss 行集选出。
|
||||
- Σ×r_boss 无运行时承载(Boss hp 行值直填 enemy.tres;difficulty_table.gd:2-3 头注成文口径「Boss 数值走行值直填不经本表」)——本批不造运行时公式断言。
|
||||
- 已盖面(不动):resolver 路由与 clamp(`config_db_resolver_test.gd` 四函数);表侧 Boss 行集带键覆盖(`table_structure_guard_test.gd:_smoke_enemy_bands` :242-259);半血门与闩(`skill_unlock_test.gd`)。
|
||||
- 剩余缺口:每区域恰一 Boss 终点无专项断言(`node_map_generator_test.gd` 只断终点数与入口数 :79-89);「Boss 节点开战必选 boss 行集」端到端无直接断言。
|
||||
|
||||
### ⑥ 锻造工坊(meta_progression.gd+forge_table.gd+shop_session.gd)
|
||||
|
||||
- 解锁双通道单点:`_forge_route_unlocked` meta_progression.gd:126-139(初始持有∨直购∨练满前一档);直购写口 `forge_buy_unlock` :160-170(已解锁拒绝零收费),解锁价 200(forge.tres)。
|
||||
- 定价:`forge_table.gd:24-28` level_price = floor(基价 × (公比/100)^已购级数);表值基价 20/公比 150(forge.tres)。
|
||||
- 注入:`hero_override` :270-293(树淬炼 :280-288、分层相加 :289-293、树攻防恒 0);`_forge_axis_bonus` :297-302(只认选中路线、异常回退 0);参数面下传 main.gd:191-201;入账 adventure_runner.gd:89-97。
|
||||
- 默认值常驻:`hero_type_axes` :188-198(未点亮照常返回 normal/light)+ forge.tres:7-8 默认轴。
|
||||
- 拒购:`forge_buy_level` :144-155;余额闸零副作用(`meta_state_store.gd:18-22` spend_keystone 余额不足 return false 零写入)。
|
||||
- 存档:save_manager v7 forge_routes 域 :267-272(默认 weapon=sword/armor=light)+normalize 补齐 :295-311。
|
||||
- 祝福件通道:局内商店购买(shop_session.gd:71-91 legacy/:272-293 pool)写局内加算区(GP 币),与 forge_routes 代码零交集;绑定负断言零测试。
|
||||
- 已盖面(不动):双通道/锁死/公式对拍/树零攻防(`meta_progression_forge_test.gd` :432-455、:476-541);锁定态/两区三行/钮态(`forge_workbench_test.gd` :42-99);长剑价格梯纯函数(`forge_table_test.gd:_test_price_ladder` :91-99);购买链首两级(:411-420)。
|
||||
- 剩余缺口:全路线价格梯(非长剑四路线零梯断言)、购买链 45 档、拒购零副作用、祝福件零泄漏、默认值解耦、甲型双轴切换。
|
||||
|
||||
## 设计契约红线
|
||||
|
||||
- 🔴 **零行为改动**:本批只增测试断言与 README 能力行;禁触产品码、禁触 data/tables 行值——断言需要观测面而现役无 → 停卡呈报,禁为可测性改产品码(需求改动另走卡)。
|
||||
- 🔴 **判据绑可观测提交面**(test-conventions §8.4);禁变异类测试、禁撤销类取证。
|
||||
- 🔴 **RNG 单点**:⑤ 集成断言用固定种子经 RngService 封闭流,禁 randomize()/自建 RNG。
|
||||
- 🔴 **实现照单落名**:README 代表用例名=测试函数名唯一依据,名实不符=停卡退回写卡段;断言失败消息尾缀〔断言补齐批 Vn〕全仓可 grep。
|
||||
- 🔴 **测试削弱申报**:本批不删不改任何既有断言(迭代 48 V1-V3 零触碰);无允许削弱项。
|
||||
- 🔴 **Forge 表行值零触碰**(provisional 形态归 M4 数值批)。
|
||||
|
||||
## 范围边界
|
||||
|
||||
**做**:13 个新断言函数+2 处既有函数扩展+README 能力行增行与覆盖域表缺口句订正。
|
||||
|
||||
**不做**:②多语言(P1 功能缺口);③音频(P1 后置);⑤数值落带断言(provisional 归 M4);④死亡灰化/碎裂断言(2026-11-02 用户裁除名,探针+人工目检为终态);Σ×r_boss 运行时公式断言(无运行时承载,不造);满级特效(本期不做);为可测性改产品码(需求改动另走卡)。
|
||||
|
||||
## 关联文档同步面
|
||||
|
||||
| 文档 | 关联点 | 同步义务 |
|
||||
|---|---|---|
|
||||
| `game/tests/README.md` 覆盖域表 | 断言落地后覆盖状态变化(:180/:182/:184/:194/:200 五行) | 状态列翻转登记进反哺点清单,由用户裁决后归文档批落文(实现批禁写 README);缺口句订正属写卡段维护面,本批当场落 |
|
||||
| 两份 GDD(难度曲线与Boss/锻造工坊) | 断言绑定其验收标准节/规则节 | 无义务——零触面 |
|
||||
|
||||
## 任务卡 T0..T4
|
||||
|
||||
**T0 ①增益状态结算链断言(≈0.5 工作日)**
|
||||
|
||||
- [ ] T0-1 `game/tests/unit/contracts/system_contracts_test.gd` 新增四函数:`_test_status_decay_and_clear`〔V1/V2〕(衰减每结算恰 −1、归 0 移除并返回移除数 k;notify_battle_end 后 query_active 零行);`_test_status_stack_full_reject`〔V3〕(满层再授返 false、层数与 remaining 零写入);`_test_status_independent_branch`〔V4〕(直改已加载表 rows 追加 independent 试验行→授予两实例→并存独立结算,测毕还原);`_test_status_snapshot_first_grant`〔V5〕(accumulate 再授只加时长,origin_cell/round_index 沿首授)
|
||||
- [ ] T0-2 单元套全绿——里程碑步出口跑一次全套闸;波内修复只做出红最小单元的最窄确认
|
||||
|
||||
**T1 ②设置存储契约断言(≈0.5 工作日)**
|
||||
|
||||
- [ ] T1-1 新建 `game/tests/unit/presentation/settings_store_test.gd` 三函数:`_test_settings_roundtrip`〔V6〕(写 true→存→新实例回读 true,节/键 motion/reduced);`_test_settings_default`〔V7〕(无文件缺省 false);`_test_settings_apply_chain`〔V8〕(reduced→apply()→UiKit.motion_scale==0.5,false→1.0,测毕还原静态初值);隔离卫生:经 retarget() 指向临时 cfg,禁碰真实 user://settings.cfg(ui_click_probe 同款纪律)
|
||||
- [ ] T1-2 单元套全绿(同 T0-2 措辞)
|
||||
|
||||
**T2 ⑤Boss 门控断言(≈0.5 工作日)**
|
||||
|
||||
- [ ] T2-1 `node_map_generator_test.gd` 新增 `_test_boss_exit_per_region`〔V9〕(真实生成多区域图,每区域终点集恰一 type=="boss"、深度=区域深度 L−r)
|
||||
- [ ] T2-2 新建 `game/tests/unit/battle_settlement/boss_gate_test.gd`:`_test_boss_node_spawns_boss_rowset`〔V10〕(begin_battle(node_type="boss")→实例化敌人行键全部 row_kind=="boss";对照入参 node_type="battle" 预解析行集零 boss 行;固定种子经 RngService 流口)
|
||||
- [ ] T2-3 单元套全绿(同 T0-2 措辞)
|
||||
|
||||
**T3 ⑥锻造工坊断言(≈1 工作日)**
|
||||
|
||||
- [ ] T3-1 扩展 `forge_table_test.gd:_test_price_ladder`〔V11〕:六路线全量梯(逐级严格递增、floor 公式按表值推算不写死表值、满级档价=上限档)
|
||||
- [ ] T3-2 扩展 `meta_progression_forge_test.gd:_meta_pricing_and_buy`〔V12〕:长剑购买链走至 3 级(45 档扣款与余额递减断言)
|
||||
- [ ] T3-3 新增 `_test_forge_buy_reject`〔V13〕(余额不足与未解锁两路→false,等级与余额零变化)
|
||||
- [ ] T3-4 新增 `_test_blessing_no_forge_leak`〔V14〕(商店买攻击祝福+护甲祝福→forge_state 投影逐位不变)
|
||||
- [ ] T3-5 新增 `_test_default_axes_pre_key`〔V15〕(树钥匙未点亮→hero_type_axes 返回 normal/light)
|
||||
- [ ] T3-6 新增 `_test_armor_switch_dual_axes`〔V16〕(forge_select("heavy")→AC 轴 +2 且挨普通攻击修正 −1;切回 light 复原且等级保留)
|
||||
- [ ] T3-7 单元套全绿(同 T0-2 措辞)——里程碑步出口跑一次全套闸;波内修复只做出红最小单元的最窄确认
|
||||
|
||||
**T4 产出物落位卡(≈0.25 工作日)**
|
||||
|
||||
- [ ] T4-1 合并前写齐 `reports/iterN-实现报告-<日期>.md`(各卡状态+遗留读数义务+实现决定记录;本批零实机目检项——纯断言批无玩家可见面)
|
||||
- [ ] T4-2 合并前写齐 `reports/iterN-反哺点-<日期>.md`(覆盖域表五行状态列翻转与缺口句收敛,逐条写明落点与依据;实现批禁写 design/ 与根规则文件)
|
||||
|
||||
## 验证闸(改动—验收对照表)
|
||||
|
||||
| 需求点 | 改动(文件:行/行为) | 验收测试(名称+落点+尾缀) | 层级 | 真实入口 | 判据绑定面 | 能力行 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| 增益衰减逐值与到期移除+战斗结束全清 | system_contracts_test.gd 新增一函数 | `_test_status_decay_and_clear`〔V1/V2〕 | 单元 | StatusContainer.apply_effect()/decay_round()/notify_battle_end() | 返回值+query_active 投影 | 新增:状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) |
|
||||
| 增益满层拒绝 | 同上文件新增一函数 | `_test_status_stack_full_reject`〔V3〕 | 单元 | StatusContainer.apply_effect() | 返回 false+投影层数/时长零变化 | 新增:状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) |
|
||||
| 增益独立并存+首授快照 | 同上文件新增两函数 | `_test_status_independent_branch`〔V4〕/`_test_status_snapshot_first_grant`〔V5〕 | 单元 | StatusContainer.apply_effect()+直改已加载表行(试验行) | 返回值+投影快照域 | 新增:状态效果结算链(逐值衰减/到期移除/战斗结束全清/满层拒绝/独立并存/首授快照) |
|
||||
| 减动效存储回读/缺省/应用链 | settings_store_test.gd 新文件三函数 | `_test_settings_roundtrip`〔V6〕/`_test_settings_default`〔V7〕/`_test_settings_apply_chain`〔V8〕 | 单元 | SettingsStore.load_settings()/save_settings()/apply() | ConfigFile 回读值+UiKit.motion_scale | 新增:减动效开关存储契约(持久化回读/缺省值/引擎动效档应用) |
|
||||
| 每区域恰一 Boss 终点 | node_map_generator_test.gd 新增一函数 | `_test_boss_exit_per_region`〔V9〕 | 单元 | NodeMapGenerator.generate() | 生成图节点 type/depth 投影 | 新增:Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) |
|
||||
| Boss 节点开战必选 Boss 行集 | boss_gate_test.gd 新文件一函数 | `_test_boss_node_spawns_boss_rowset`〔V10〕 | 集成 | BattleOrchestrator.begin_battle() | 实例化敌人行键 row_kind 投影 | 新增:Boss 出场门控(每区域恰一 Boss 终点/Boss 节点开战必选 Boss 行集) |
|
||||
| 全路线价格梯+多级购买扣款 | forge_table_test 扩展+meta_progression_forge_test 扩展 | `_test_price_ladder`(扩)〔V11〕/`_meta_pricing_and_buy`(扩)〔V12〕 | 单元 | ForgeTable.level_price()/MetaProgression.forge_buy_level() | 表值推算序列+扣款与余额递减 | 新增:锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) |
|
||||
| 拒购零副作用 | meta_progression_forge_test 新增一函数 | `_test_forge_buy_reject`〔V13〕 | 单元 | MetaProgression.forge_buy_level() | 返回 false+余额/等级零变化 | 新增:锻造定价与购买闭环(全路线价格梯/多级购买扣款/拒购零副作用) |
|
||||
| 祝福件零泄漏 | meta_progression_forge_test 新增一函数 | `_test_blessing_no_forge_leak`〔V14〕 | 单元 | 商店购买口(shop_session 两条购买路径) | forge_state 投影逐位不变 | 新增:锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) |
|
||||
| 默认值解耦+甲型双轴 | meta_progression_forge_test 新增两函数 | `_test_default_axes_pre_key`〔V15〕/`_test_armor_switch_dual_axes`〔V16〕 | 单元 | MetaProgression.hero_type_axes()/forge_select() | 返回值+hero_override 投影(AC 轴+matchup 修正) | 新增:锻造与相邻通道隔离(祝福件购买锻造零变化/默认轴与钥匙解耦/甲型切换双轴可感) |
|
||||
|
||||
表外备注:`game/tests/README.md` 五行缺口句订正=非行为改动(写卡段维护面,本批当场落,见「关联文档同步面」节)。
|
||||
|
||||
## 独立验收审计
|
||||
|
||||
执行体=迭代 loop 内启动的未参与实现子代理(2026-11-02 用户裁,test-conventions §8.7 默认通道);报告落 `reports/iterN-验收审计-<日期>.md`,逐行写「通过/缺漏/例外及依据」。审计四件事:逐行核对对照表(测试存在、当轮执行、尾缀可 grep、真实入口未绕改动点);抽查判据绑定面;检查测试削弱(本卡申报=无允许削弱项);核对内部字段直查受控例外(本卡零直查行)。未过不得合并。
|
||||
|
||||
## 异常分级
|
||||
|
||||
- **L1 自救**:断言消息文本、局部夹具命名、扩展函数内步序。
|
||||
- **L2 记录继续**:README 新行在旅程段内的插入行位(系统归属相邻处)——记入实现决定记录。
|
||||
- **L3 停机呈报**:①independent 试验行注入不可行→停卡呈报;⑤boss_gate 集成夹具不可行→停卡呈报;⑥甲型双轴观测面不足(AC 修正不随 select 变化)→停卡呈报——观测面不足=上游缺陷,禁自造注入破坏;机检键与本卡契约互斥→停卡退回写卡段。
|
||||
|
||||
## 代拍板白名单
|
||||
|
||||
- 测试内局部命名、常量、断言消息文本。
|
||||
- 扩展函数内步序编排。
|
||||
- README 新行的旅程段内插入行位。
|
||||
|
||||
## 待裁点
|
||||
|
||||
零待裁点,开工门=U0。
|
||||
|
||||
## 落位与可复用性预检
|
||||
|
||||
**新增文件 2 个:**
|
||||
|
||||
| 文件 | 职责 | 为什么不能复用既有 |
|
||||
|---|---|---|
|
||||
| `game/tests/unit/presentation/settings_store_test.gd` | SettingsStore 单元契约(回读/缺省/应用链) | pacing_queue_test 主题是排播节奏(直拨 UiKit 不经 store);ui_click_probe 是探针承载非单元套件(不入 run_all 单元登记);store 单元契约无既有唯一落点 |
|
||||
| `game/tests/unit/battle_settlement/boss_gate_test.gd` | Boss 节点开战必选 Boss 行集端到端 | node_map_generator_test 只测图结构;config_db_resolver_test/table_structure_guard_test 只测表面路由与带键覆盖;battle_narrator_test 只测呈现投影命名——端到端链(generator→begin_battle→spawn)无既有承载 |
|
||||
|
||||
**复用既有(全部扩展,不新增文件):** system_contracts_test.gd(增益状态单元契约既有宿主)、forge_table_test.gd/meta_progression_forge_test.gd(锻造既有宿主)、node_map_generator_test.gd(图结构既有宿主)。
|
||||
|
||||
**唯一面**:每条断言唯一函数承载;README 新行唯一;各新函数复用所在文件既有夹具(harness 不外散);零产品码触面。
|
||||
|
||||
**分层与交互**:全部新件在测试层(probe/sim/tests 不在 S9 扫描面,测量面直构惯例);Core 域断言经领域类公开方法进出(StatusContainer/NodeMapGenerator/MetaProgression/ForgeTable);集成断言实例化真实宿主链(battle_orchestrator),禁裸替身。
|
||||
|
||||
## 实现规范预算
|
||||
|
||||
- 新增两文件均为单元测试宿主:公开面仅 ` _init`(各文件 ≤6 个私有断言函数、零公开方法增长、成员 ≤5)——远低于 gdstyle 默认阈值(公开方法 ≤20/成员 ≤15)。
|
||||
- 存量触面:产品码零触面,S13 基线键零新增;扩展的四份存量测试件不入 gdstyle 基线(测试面直构惯例),无对账键。
|
||||
|
||||
## 拆分边界纪律
|
||||
|
||||
本卡非拆分/重构类(纯测试增补),不适用拆出单元条款;零删码,无「为机检保留无效成员」面。
|
||||
Loaded 100 of 112 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user