docs(research): 效果引擎无状态化提案用词对齐系统资源定名——指计数器容器的「账/账本」35 处改为「计数器容器」

2026-10-19 用户裁:GP/AP/钥石/RAP/刷新次数/生命/充能这一层统一叫「系统资源」,
「账/账本/独立资源/系统数值」退役。提案里的「账/账本」指的是 RunLedger 持有的
计数器容器,不属系统资源,沿用旧词会在读者端与新定名相撞。
本次只换词不改内容:容器一律写「计数器容器」或直接写标识符,动词「记账/落账」
改「写入计数」,另订正「文档欠账」→「文档缺口」。提案自身的判断与结论一字未动。
This commit is contained in:
DKChan committed 2026-10-19 19:27:28 +08:00
1 parent b206120b8e
commit b99a46beed
1 file changed
+32 -32
@@ -11,7 +11,7 @@
## 1. 目标与完成定义
**目标**:把 `EffectEngine` 从「解释器 + 自带状态账本」改为「**纯计算引擎(零运行时状态)**」;所有效果侧的值改由**各生命周期的宿主对象**持有,事件触发时按参数传入,引擎只做判定。
**目标**:把 `EffectEngine` 从「解释器 + 自带状态计数器容器」改为「**纯计算引擎(零运行时状态)**」;所有效果侧的值改由**各生命周期的宿主对象**持有,事件触发时按参数传入,引擎只做判定。
**完成定义**(全部满足才算完):
@@ -20,7 +20,7 @@
3. `counters_export` / `counters_seed` 两条搬运链**整段删除**,跨战持久不再靠副本同步;
4. `adventure_runner.gd:840-842` 硬编码清除名单删除,改为「关卡桶整体清空」;
5. 计数器归属机器可读且加载期可校验(写错即加载致命);
6. **同种子复放对拍**与改前一致(确定性零漂移)。对拍=**同种子重放已有模拟锚、零局新采集**(`sim-data/README.md` 配套纪律③:对拍断言 = 0 新跑,出口闸复验用锚内子集 ≤20)。🔴 **2026-10-19 复核:本条与「缺陷自动消解」互斥**——分桶会消解 `run_ledger.gd:42` 判空早退(自锁,实际是**整局**不落账,非仅首战),adventure 域四枚从「每战清零」变「跨战持久」,是行为变更 ⇒ 对拍中相关项必然出现差异。最终口径取决于 §7.7 待裁项 B。
6. **同种子复放对拍**与改前一致(确定性零漂移)。对拍=**同种子重放已有模拟锚、零局新采集**(`sim-data/README.md` 配套纪律③:对拍断言 = 0 新跑,出口闸复验用锚内子集 ≤20)。🔴 **2026-10-19 复核:本条与「缺陷自动消解」互斥**——分桶会消解 `run_ledger.gd:42` 判空早退(自锁,实际是**整局**不写入,非仅首战),adventure 域四枚从「每战清零」变「跨战持久」,是行为变更 ⇒ 对拍中相关项必然出现差异。最终口径取决于 §7.7 待裁项 B。
> **用语订正(2026-10-19)**:草案前几版对读者直用「MC」缩写而未展开,违反 doc-conventions 第 4 节用语红线。**「MC」=蒙特卡洛(Monte Carlo)**,在本项目特指「跑大量随机冒险的模拟试玩」,入口 `res://smoke/adventure_monte_carlo.gd`(全冒险期望),产物缓存层=`sim-data/`(该目录 README 首行即「MC 缓存层」)。虽系项目既有简称(`AGENTS.md:34/43/62` 全篇在用),但**首次出现必须展开**,本草案后续一律写「全冒险模拟(蒙特卡洛)」。
@@ -35,7 +35,7 @@
| 成员 | 行 | 性质 | 处置 |
|---|---|---|---|
| `_triggers` | :86 | 编译产物(装载期定死) | **保留**(=程序,非状态) |
| `_counters` | :87 | **运行时账本**(counter_id→int,混装四档寿命) | **搬走** |
| `_counters` | :87 | **运行时计数器容器**(counter_id→int,混装四档寿命) | **搬走** |
| `_reset_on_round_start` | :88 | 编译产物(装载期收集) | 随分桶删除 |
| `_adventure_counter_ids` | :89 | 编译产物(装载期收集) | 随分桶删除 |
| `db` | :91 | 只读依赖 | 保留 |
@@ -52,13 +52,13 @@
| `counters_export()` / `counters_seed()` | :249 / :258 | `battle_orchestrator.gd:1127` / `:393` |
| `validate_trigger_row()` | :354(静态纯函数) | `feat_table.gd:44`、`treasure_item_table.gd:104` |
🔴 **草案初版遗漏两处**:①第 6 个写口 `grant_segment.gd:327`(`_apply_counter_hit`——含卡② `insurance_used` 记账,见 §6-3);②三个 `counter()` 读口 `damage_controller.gd:345` / `grant_segment.gd:103` / `grant_segment.gd:205`。
🔴 **草案初版遗漏两处**:①第 6 个写口 `grant_segment.gd:327`(`_apply_counter_hit`——含卡② `insurance_used` 写入计数,见 §6-3);②三个 `counter()` 读口 `damage_controller.gd:345` / `grant_segment.gd:103` / `grant_segment.gd:205`。
### 2.3 宿主链与寿命的物理来源
- `level_runner.gd:452` `_start_battle` → `BattleOrchestrator.new` → `battle_orchestrator.gd:274` `_fx = EffectEngine.new(db_ref)`。
- ⇒ **域内账本的物理寿命恒等于一场战斗**;所谓「跨战持久」= 战末 `counters_export` 拷进 `run_ledger.adventure_counters`、战首 `counters_seed` 拷回,**两份副本手工搬运**。
- 搬运链的既有缺陷:`run_ledger.gd:42` `if adventure_counters.is_empty(): return` ⇒ 空账时导出被整批丢弃;因账本只有断点回填 `:206-208` 才能变非空,**形成自锁:全新开局连玩则 adventure 域四枚整局不落账**(自迭代 20 T6 即有,无整局级测试覆盖)。**本提案落地后该缺陷随宿主消失(不再有副本)**,代价=行为变更,详见 §7.8 / 待裁项 B。
- ⇒ **域内计数器容器的物理寿命恒等于一场战斗**;所谓「跨战持久」= 战末 `counters_export` 拷进 `run_ledger.adventure_counters`、战首 `counters_seed` 拷回,**两份副本手工搬运**。
- 搬运链的既有缺陷:`run_ledger.gd:42` `if adventure_counters.is_empty(): return` ⇒ 容器为空时导出被整批丢弃;因该容器只有断点回填 `:206-208` 才能变非空,**形成自锁:全新开局连玩则 adventure 域四枚整局不写入**(自迭代 20 T6 即有,无整局级测试覆盖)。**本提案落地后该缺陷随宿主消失(不再有副本)**,代价=行为变更,详见 §7.8 / 待裁项 B。
- 🔴 草案初版把「按关重置」清零点写成 `adventure_runner.gd:876-878`,实为 **`_reset_tree_limited_counters()` :840-842**(硬编码两键 `tree_sense_sharp_used` / `tree_sense_guard_used`),调用点 **`_start_current_level` :847**;新冒险置空在 **:255**(非 :258);断点写键在 **:1035**(非 :1072,该文件仅 1059 行)。
### 2.4 效果侧还有一批「记忆」住在编排器
@@ -92,16 +92,16 @@
| 现在 | 之后 |
|---|---|
| `dispatch(event, payload, facts)` | 增 ctx 参数(携带本战可读写账本引用集) |
| `dispatch(event, payload, facts)` | 增 ctx 参数(携带本战可读写计数器容器引用集) |
| `counter()` | 改从 ctx 读(纯查询,不存) |
| `counter_add()` / `counter_spend()` | **删除**——写由发射点落到传入账本 |
| `counter_add()` / `counter_spend()` | **删除**——写由发射点落到传入计数器容器 |
| `counters_export()` / `counters_seed()` | **删除** |
| `has_effect_on()` / `validate_trigger_row()` | 保留(纯函数) |
| — | 新增纯函数 `bucket_of(counter_id)`(按前缀判桶) |
写口迁移(**共 6 处,非 5 处**):`action_economy.gd:110/113/173`、`damage_controller.gd:351/464`、**`grant_segment.gd:327`** 由 `_fx.counter_add(...)` 改为对 ctx 账本写入;`battle_orchestrator.gd:717` 的 `counter_spend` 同理。
写口迁移(**共 6 处,非 5 处**):`action_economy.gd:110/113/173`、`damage_controller.gd:351/464`、**`grant_segment.gd:327`** 由 `_fx.counter_add(...)` 改为对 ctx 计数器容器写入;`battle_orchestrator.gd:717` 的 `counter_spend` 同理。
🔴 `grant_segment.gd:327` 是**卡② 完美保险的记账唯一通路**(`:315` `_apply_counter_hit(h)` 落 `insurance_used`),漏掉它=保险「每战限次一次」失效(恒 0 → 每次标错都豁免)。
🔴 `grant_segment.gd:327` 是**卡② 完美保险的写入计数唯一通路**(`:315` `_apply_counter_hit(h)` 落 `insurance_used`),漏掉它=保险「每战限次一次」失效(恒 0 → 每次标错都豁免)。
### 3.3 归属表达(零运行时机制)
@@ -176,7 +176,7 @@
| **C2** 保险限次一次 | 单测 `feat_vouchers_test.gd` | ✅ **已闭合**——V25 现成「第 1 战用满 → 第 2 战重新豁免」双编排器骨架;`orch2.begin_battle({"battle_index":2,…})` + `orch2._grant_seg._judge_seal(...)` 即真实 `seal_missed` | `:143-186` |
| **E1/E2** 前缀致命 | 新增负面套件 | ✅ **已闭合**——现役 fail-fast 测试模式:`config_db_fatal_oob_test.gd`(独立进程,`expect:1` + `negative:true`),manifest 协议在役 | `tests/run_all.gd:37`/`:48` |
| **F1** 旧件零残留 | `static_scan.sh` 新增 S 段 | 🟡 **半闭合**——S1–S10 分检制在位;本批新增 S 段承载,按纪律「闸件增减只改该脚本+`run_all_gates.sh` 登记」,卡里须写明改哪两个文件 | `static_scan.sh:38-82` |
| **A1/A2/A3** adventure 桶 | 冒烟/单测新段 | 🟡 **半闭合**——RunLedger 经 `enc["ledger"]` 下传(`battle_orchestrator.gd:375`),冒烟可自建共享账喂两个 orch;`AdventureRunner.run_ledger` 公开字段可达(`smoke_all.gd:6131` 先例)。**卡在 U2** | `battle_orchestrator.gd:375` |
| **A1/A2/A3** adventure 桶 | 冒烟/单测新段 | 🟡 **半闭合**——RunLedger 经 `enc["ledger"]` 下传(`battle_orchestrator.gd:375`),冒烟可自建共享计数器容器喂两个 orch;`AdventureRunner.run_ledger` 公开字段可达(`smoke_all.gd:6131` 先例)。**卡在 U2** | `battle_orchestrator.gd:375` |
| **C1** battle 桶 | 单测/冒烟 | 🟡 **半闭合**——编排器可 new + begin_battle(`smoke_all.gd:2680-2687` 先例);桶须为公开字段或 API,测试才读写得了 | `smoke_all.gd:2680-2687` |
| **B1/B2** level 桶 | 冒烟新段或探针 | 🟡 **半闭合**——LevelRunner 真实驱动有先例(`board_shot_probe.gd:5`);但现役 level 桶**寄居在 `run_ledger.adventure_counters` 字段内**(`adventure_runner.gd:840-842` erase 的就是该字段),搬去 LevelRunner 后须新定公开接口。**卡在 U3** | `adventure_runner.gd:840-842` |
| **D1** round 桶 | 冒烟 | 🔴 **未闭合**——`round_start` 的真实发射入口未定位 | — |
@@ -206,7 +206,7 @@
| **A5** | `round_start` 真实发射入口(同 U4) | 未定位 ⇒ D1 可能只能走裸 `dispatch`,弱于真实路径 | 🟡 |
| **A6** | `counter_spend` 施加面漏列 | §3.2 只列了写口 `orchestrator.gd:717`;**漏了读施加面 `action_economy.gd:106`**(按 hits 的 `kind == "counter_spend"` 分支走加收对价)——ctx 改动后这条路径怎么走未写 | 🟡 |
| **A7** | counter_id 改名传播面未清点 | 已知:`feats.tres` 数据行、`effect_engine.gd` 树合成行硬编码、`smoke_all.gd` 断言字符串、`tests/` 断言字符串。**未查:呈现层/HUD 是否显示 counter_id**。T6 估时 0.25d 可能不够 | 🟡 |
| **A8** | 保险双账一致性(N6 遗留) | `_perfect_forgiven`(编排器自有账,N6 定「留编排器」)与 `insurance_used`(battle 桶,`grant_segment.gd:327` 写)是两份。V25 对两者都断言;`when counter lt 1` 读的是桶内那份 ⇒ 需厘清 `_perfect_forgiven` 是镜像还是另有语义,否则同一次标错可能被两处各记一次 | 🟡 |
| **A8** | 保险双计数一致性(N6 遗留) | `_perfect_forgiven`(编排器自持计数,N6 定「留编排器」)与 `insurance_used`(battle 桶,`grant_segment.gd:327` 写)是两份。V25 对两者都断言;`when counter lt 1` 读的是桶内那份 ⇒ 需厘清 `_perfect_forgiven` 是镜像还是另有语义,否则同一次标错可能被两处各写一次 | 🟡 |
### 4.5 需求澄清未对齐(不影响本批动工,但需登记或后续裁)
@@ -234,7 +234,7 @@
| `feat_vouchers_test.gd` | **`:114`/`:119`/`:128`**(V16 断点往返 `pain_layers`/`twin_digs`;初版写 `:92-103` 系错行)+ `:132+` 卡② V25 完美保险验收组 |
| `settle_segments_test.gd` | `:32-33` `_apply_counter_hit` + `_fx.counter("dig_combo")` |
| `treasure_effects_test.gd` | `:34/:45/:53-56` 裸 `dispatch` 面(无 ctx 参数则签名不兼容) |
| `smoke_all.gd` | `:6083/:6085/:6088` `counter_add`+`counters_export`+`counters_seed`/`:6131-6134` 直呼 `_reset_tree_limited_counters`(删函数须同步)/**另 8 处 `fx.counter_add` 直调 `:1427/:1436/:1445/:1466/:1478/:1525/:1566/:1583`(初版全漏)**/`:1410` 断点 `adventure_counters` 键断言/`:1612` `_fx.rebuild`/`:6096/:6124/:6148/:6157` enc 携带 `adventure_counters` 空账 |
| `smoke_all.gd` | `:6083/:6085/:6088` `counter_add`+`counters_export`+`counters_seed`/`:6131-6134` 直呼 `_reset_tree_limited_counters`(删函数须同步)/**另 8 处 `fx.counter_add` 直调 `:1427/:1436/:1445/:1466/:1478/:1525/:1566/:1583`(初版全漏)**/`:1410` 断点 `adventure_counters` 键断言/`:1612` `_fx.rebuild`/`:6096/:6124/:6148/:6157` enc 携带 `adventure_counters` 空容器 |
| `run_all.gd` | 套件表(增改套件须登记) |
**存档/断点**:`adventure` 桶已在断点(写键 `adventure_runner.gd:1035`,续玩回填 `:206-208`);`level` 桶是否进断点见 N3。仅改归属不改 id → 旧档零影响;**若采纳 N2 前缀改名(`tree_sense_*` → `lvl_*`)则旧档断点内旧键失效**(Alpha 阶段可接受,有先例:代码术语刷新批 2026-10-18 用户裁不迁移)。
@@ -246,7 +246,7 @@
1. **确定性风险**:读写时序若与改前不一致,同种子复放会漂移 → T7 必须做同种子复放对拍(`adventure_monte_carlo.gd`,零局新采集)。
2. **旧档**:`counter_id` 若改名(如 `tree_sense_*`),断点里旧键失效。Alpha 阶段可接受,有先例(代码术语刷新批 2026-10-18 用户裁:不迁移)。
3. **与迭代 31 卡②的时序(🔴 2026-10-19 复核:前提已翻转)**:草案初版写「卡②**将落**完美保险、建议本批排在卡②之后」——但 **卡② 已合入 master**(`3e076e22` merge 迭代 31 卡② 凭证件裁削批),完美保险**已按「引擎持状态 + `domain=battle`」形态在役**:`effect_engine.gd:42-44` `forgive_seal_miss` 已扩签、`feats.tres:875-877` `insurance_used` 行已落(feats.tres 首例 `domain=battle`)、`battle_orchestrator.gd:267/472/1299` `_perfect_forgiven` 已落地、`grant_segment.gd:311-317` 消费点已接线、`feat_vouchers_test.gd:132+` V25 验收组已在册。
⇒ **本批不再是「排在卡②之后」,而是必须把已落地的保险链路一并迁移**:`insurance_used` 归 battle 桶(无前缀缺省)、`grant_segment.gd:327` 写口改 ctx、`_perfect_forgiven`(编排器自有账)是否并入 battle 桶见 N6。若不迁移即删 `counter_add`,保险**每战限次一次**的闸门失效(变每场无限豁免),且 V25 验收组红。
⇒ **本批不再是「排在卡②之后」,而是必须把已落地的保险链路一并迁移**:`insurance_used` 归 battle 桶(无前缀缺省)、`grant_segment.gd:327` 写口改 ctx、`_perfect_forgiven`(编排器自持计数)是否并入 battle 桶见 N6。若不迁移即删 `counter_add`,保险**每战限次一次**的闸门失效(变每场无限豁免),且 V25 验收组红。
4. **`_triggers` 仍留在引擎内**——本提案的判断是「编译产物=程序,不算状态」;若此前提被否,则需另立 `Program` 对象,工作量 +0.5d。
---
@@ -307,16 +307,16 @@ func counters_merge_export(exported: Dictionary) -> void:
| 维度 | 内容 |
|---|---|
| **所属领域** | 核心域 `game/core/`(非呈现层、非数据层) |
| **所属系统/类** | `RunLedger`——共享四本账承载类(迭代 27 T3 从「四账散在 encounter 字典」收口为单类,审计 P1-4) |
| **所属链路** | EffectEngine adventure 域计数器「**战末导出 → 冒险账本 → 战首回灌**」搬运链(迭代 20 T6 建立) |
| **所属系统/类** | `RunLedger`——四件共享容器的承载类(迭代 27 T3 从「四件共享容器散在 encounter 字典」收口为单类,审计 P1-4) |
| **所属链路** | EffectEngine adventure 域计数器「**战末导出 → 冒险级计数器容器 → 战首回灌**」搬运链(迭代 20 T6 建立) |
| **设计作用** | 让 `domain=adventure` 的计数器跨战斗持久:战末 `orchestrator.gd:1127` 导出 → 覆写 `run_ledger.adventure_counters`;战首 `:393` `counters_seed` 回灌引擎 |
| **实际作用** | 恒等空操作(见下「自锁」) |
**自锁机制(不是「首战丢一次」,是整局不落账)**:
**自锁机制(不是「首战丢一次」,是整局不写入)**:
1. 新冒险开局 `adventure_runner.gd:255` → `run_ledger.adventure_counters = {}`(空);
2. 战末导出 → `:42` 判空 → 整批 `return`,四枚值全丢;
3. 账本**仍为空** → 下一战末继续丢 → ……循环不变;
3. 容器**仍为空** → 下一战末继续丢 → ……循环不变;
4. 唯一解锁路径=断点回填 `:206-208`(`bp_counters is Dictionary` 才赋非空)。
⇒ 同一份存档,**读档前每战清零、读档后跨战累积**,行为不一致且难复现。
@@ -336,10 +336,10 @@ func counters_merge_export(exported: Dictionary) -> void:
| 时点 | 性质 | 说明 |
|---|---|---|
| **T0 开局** | **缺初始化**(该做没做) | 应播种四枚零值账(账本初始非空),实际只给 `{}` 空字典 ⇒ 为 T3 守卫的误判提供了触发条件 |
| **T0 开局** | **缺初始化**(该做没做) | 应播种四枚零值计数(容器初始非空),实际只给 `{}` 空字典 ⇒ 为 T3 守卫的误判提供了触发条件 |
| **T3 战末** | **守卫误判**(做了但被跳过) | 应无条件覆写四枚,实际被 `is_empty()` 早退闸整批丢弃 ⇒ **病根** |
两者**任一修好即可解开自锁**:只修 T0(播种零账)→ 账本非空 → T3 守卫放行;只修 T3(删守卫)→ 无条件覆写 → 即使 T0 空也通。T1/T4 的偏差是这两处的**后果**,不是独立缺陷。
两者**任一修好即可解开自锁**:只修 T0(播种零值计数)→ 容器非空 → T3 守卫放行;只修 T3(删守卫)→ 无条件覆写 → 即使 T0 空也通。T1/T4 的偏差是这两处的**后果**,不是独立缺陷。
⇒ 本提案走的是「修 T3」这条路,且连同整条搬运链一起删(无副本即无守卫可附),T0 的初始化缺口随之失去意义。
@@ -356,9 +356,9 @@ if _fx != null and not _fx_adventure_counters_ref.is_empty(): # ← 守卫
_fx_adventure_counters_ref[cid] = int(exported[cid])
```
**旧形态下这个守卫是合理的**:它判的不是「有没有数据」,而是**「这个引用是不是真的指向共享账本」**——`is_empty()` 只是当时成立的**代理判据**。enc 没带 `adventure_counters` 时引用退化为临时 `{}`,写进去没人看得到,所以跳过。
**旧形态下这个守卫是合理的**:它判的不是「有没有数据」,而是**「这个引用是不是真的指向共享计数器容器」**——`is_empty()` 只是当时成立的**代理判据**。enc 没带 `adventure_counters` 时引用退化为临时 `{}`,写进去没人看得到,所以跳过。
**收口后判据与被防风险脱钩**:账本变成 `RunLedger` 的**成员字段**(`run_ledger.gd:12`),永远存在、永远有效,不再有「引用无效」这回事;此时 `is_empty()` 只剩「还没写入过任何值」的含义。**判据没改,但它要防的东西已经不存在了** ⇒ 这就是「误判」的准确含义——不是写错了条件,是**迁移时把一个已失效的代理判据原样保留**。
**收口后判据与被防风险脱钩**:该容器变成 `RunLedger` 的**成员字段**(`run_ledger.gd:12`),永远存在、永远有效,不再有「引用无效」这回事;此时 `is_empty()` 只剩「还没写入过任何值」的含义。**判据没改,但它要防的东西已经不存在了** ⇒ 这就是「误判」的准确含义——不是写错了条件,是**迁移时把一个已失效的代理判据原样保留**。
**修法对比(伪代码)**:
@@ -368,7 +368,7 @@ func counters_merge_export(exported):
for cid in exported.keys():
adventure_counters[cid] = int(exported[cid])
# 乙 · 改判据(判"本次有没有东西可导",而非"目标账本空不空")
# 乙 · 改判据(判"本次有没有东西可导",而非"目标容器空不空")
func counters_merge_export(exported):
if exported.is_empty(): # 注:counters_export 对名单全量输出,恒非空 ⇒ 本分支恒不触发
return # ⇒ 乙 实际等价于甲,但保留了一个恒真的死条件
@@ -376,7 +376,7 @@ func counters_merge_export(exported):
adventure_counters[cid] = int(exported[cid])
# 丙 · 补 T0 初始化(不改守卫,改上游)
# adventure_runner.gd:255 由 {} 改为播种四枚 0 ⇒ 账本恒非空 ⇒ 守卫恒放行
# adventure_runner.gd:255 由 {} 改为播种四枚 0 ⇒ 容器恒非空 ⇒ 守卫恒放行
# 可解自锁,但保留了一个语义已错的条件,后人仍会踩 ⇒ 不推荐
# 丁 · 本提案:整个函数连同搬运链删除
@@ -387,11 +387,11 @@ func counters_merge_export(exported):
先澄清一个误解——**旧形态不是「写不进去」**。GDScript 的 `Dictionary` 是引用类型,写外部字典毫无技术障碍;旧守卫防的不是可写性,而是**有效性**:写进一个无人可见的临时副本等于白写。
真正的结构性根因是「**引擎持内部账本 + 战末一次性搬运**」这个两段式结构本身,它同时制造了三类风险:
真正的结构性根因是「**引擎持内部计数器容器 + 战末一次性搬运**」这个两段式结构本身,它同时制造了三类风险:
| 风险 | 来源 |
|---|---|
| 引用可能无效(临时 `{}`) | 内部账本需要「导出到某处」,而"某处"可能不存在 |
| 引用可能无效(临时 `{}`) | 内部计数器容器需要「导出到某处」,而"某处"可能不存在 |
| 需要额外时点(战末/战首) | 搬运只能在战斗边界发生 ⇒ 时点错了就丢数据 |
| 副本与真身的一致性 | 两份数据 ⇒ 谁先谁后、谁覆写谁 |
@@ -417,7 +417,7 @@ func counters_merge_export(exported):
| # | 定值 | 推导依据 |
|---|---|---|
| 1 | **立项判据=用户 2026-10-19 给出的 ECS 架构依据**(系统无状态/数据随宿主生灭/状态按参数传入),不依赖 architecture.md 现有条文 | ECS 依据即已生效的用户裁定,判据充分。`architecture.md`/`architecture.yaml`/`entities.yaml` 检索「无状态」「纯计算」零命中属**文档欠账**,处理方式=登记 06(来源=会话提及,设计面=文档轨)另立文档批补条目,**不阻塞本批** |
| 1 | **立项判据=用户 2026-10-19 给出的 ECS 架构依据**(系统无状态/数据随宿主生灭/状态按参数传入),不依赖 architecture.md 现有条文 | ECS 依据即已生效的用户裁定,判据充分。`architecture.md`/`architecture.yaml`/`entities.yaml` 检索「无状态」「纯计算」零命中属**文档缺口**,处理方式=登记 06(来源=会话提及,设计面=文档轨)另立文档批补条目,**不阻塞本批** |
| 2 | 分桶后数据行 `domain` 与 `reset_at` 两个字段**删除** | 前缀是归属的唯一声明;保留即**双事实源**(红线)。`reset_at` 由「round 桶轮始整体清空」取代,`domain` 由 `adv_`/`lvl_` 前缀取代 |
| 3 | 容器键名 `run_ledger.adventure_counters` **保留不改** | 改名会破坏旧档断点键;最小改动+单一事实源(桶的容器名不是归属声明) |
| 4 | counter_id 改名**不迁移旧档** | 仅 `tree_sense_*`→`lvl_*`、`twin_digs`→`adv_twin_digs`、`instinct_atk`→`round_instinct_atk` 三处会废掉旧档断点内旧键;Alpha 阶段可接受,**有先例**(2026-10-18 代码术语刷新批用户裁不迁移) |
@@ -431,7 +431,7 @@ func counters_merge_export(exported):
2026-10-19 前半场提出的「**寿命域统一口径**」方案(五档/四档/两档枚举、`domain` 键扩词表、`reset_at` 退役、D1–D9 裁决单)**整体作废**。作废理由:该方案的隐含前提是「引擎持有状态、需要一个字段说明它活多久」;改为分桶后,**寿命由宿主对象天然决定,不需要寿命字段**。
原议题中仍然成立的部分只剩两条:①`feats.tres` 名实不符(`pain_layers` / `twin_digs`;**`smith_seals` 已于 2026-10-19 复核剔除**——它未声明 `domain`,默认 battle 随战清零,不属名实不符)→ 已在 §7 N4 定值(`pain_layers` 归 battle/`twin_digs` 保 adventure,依据 `奇物.md:58` 与 v3.16 追认⑥);②`run_ledger.gd:42` 空账早退缺陷 → 本提案落地后自动消解,无需单独修(但消解=行为变更,触发 §7.7 待裁项 B)。
原议题中仍然成立的部分只剩两条:①`feats.tres` 名实不符(`pain_layers` / `twin_digs`;**`smith_seals` 已于 2026-10-19 复核剔除**——它未声明 `domain`,默认 battle 随战清零,不属名实不符)→ 已在 §7 N4 定值(`pain_layers` 归 battle/`twin_digs` 保 adventure,依据 `奇物.md:58` 与 v3.16 追认⑥);②`run_ledger.gd:42` 空容器早退缺陷 → 本提案落地后自动消解,无需单独修(但消解=行为变更,触发 §7.7 待裁项 B)。
---
@@ -460,7 +460,7 @@ func counters_merge_export(exported):
### 10.2 遗漏面订正
1. **第 6 个写口** `settlement/grant_segment.gd:327`(`_apply_counter_hit`)——卡② `insurance_used` 记账唯一通路,漏=保险限次失效。
1. **第 6 个写口** `settlement/grant_segment.gd:327`(`_apply_counter_hit`)——卡② `insurance_used` 写入计数唯一通路,漏=保险限次失效。
2. **3 个读口** `damage_controller.gd:345`、`grant_segment.gd:103`(`value_counter` 层值:安全帽 `helm_layers`/疼痛记忆 `pain_layers`/陷阱本能 `instinct_atk`/弃图兑换 `stop_swap`)、`grant_segment.gd:205`(`per` 反打层值)。
3. **测试迁移面漏 8+3+2 处**:`smoke_all.gd` 另 8 处 `fx.counter_add` 直调(:1427/1436/1445/1466/1478/1525/1566/1583)、`settle_segments_test.gd:32-33`、`treasure_effects_test.gd:34/45/53-56` 裸 `dispatch`。T7 估时(0.5–1d)应上修。
4. **`smoke_all.gd:1612`** `orch0._fx.rebuild([], [], [])`——`rebuild` 签名若改须同步。
@@ -496,7 +496,7 @@ func counters_merge_export(exported):
| 候选 | 内容 | 不选它的理由 |
|---|---|---|
| **甲** 挂 `run_ledger`(与 `adventure_counters` 同处) | 断点零新增键(复用同字典);`_reset_tree_limited_counters` 改成「清 level 前缀键」 | RunLedger 名义=冒险级共享四账,装关卡桶=名实不符;且它随快照走 ⇒ **N3「不进断点」选项被物理排除**(选甲即强制选「进」) |
| **甲** 挂 `run_ledger`(与 `adventure_counters` 同处) | 断点零新增键(复用同字典);`_reset_tree_limited_counters` 改成「清 level 前缀键」 | RunLedger 名义=冒险级共享容器(四件),装关卡桶=名实不符;且它随快照走 ⇒ **N3「不进断点」选项被物理排除**(选甲即强制选「进」) |
| **乙** 挂 `LevelRunner`(每关 new,随宿主生灭=天然按关清零) | 寿命天然正确;硬编码名单自动消失 | 需新增一条 `enc` 下传路径(现役只下传 `enc["ledger"]`);N3 若选「进断点」须新增存档键 |
### N2 · 归属表达
@@ -520,14 +520,14 @@ func counters_merge_export(exported):
| 计数器 | 候选 | 不选它的理由 |
|---|---|---|
| `twin_digs`(现役声明 adventure) | ①真 adventure(跨战累积,保现役)②归 battle(每第 2 次按战重算,每战首次付费开格不返) | ①=承认现役,但 `run_ledger.gd:42` 空账早退使**首战导出全丢**=首战攒的次数丢失(现役真 bug);②=强度实质变化(每战少返一次),属数值改动,须另走数值裁 |
| `twin_digs`(现役声明 adventure) | ①真 adventure(跨战累积,保现役)②归 battle(每第 2 次按战重算,每战首次付费开格不返) | ①=承认现役,但 `run_ledger.gd:42` 空容器早退使**首战导出全丢**=首战攒的次数丢失(现役真 bug);②=强度实质变化(每战少返一次),属数值改动,须另走数值裁 |
| `pain_layers`(现役声明 adventure,但 `:407` 行 `set:0` 用后清零) | ①归 battle(与「用后清零」自洽,跨战持久本就不成立)②保留 adventure 但删 `set:0` 行(变真跨战累积层)③归 level | ①最省事且语义自洽,但**改变了「上一战残留层可带进下一战」这一现役行为**(要确认是否为设计意图);②=明确的内容加强,属数值裁;③两不靠,无现役依据 |
### N5 · 引擎是否保留 `counter()` 只读
| 候选 | 内容 | 不选它的理由 |
|---|---|---|
| **甲 保留**(纯查询,读 ctx) | `_fact_value` 的 `fact counter`(:328)在 `dispatch` 内部必须能读到——现役 4 枚条件面(`twin_digs`/`pain_layers`/`insurance_used`/`tree_sense_*`)全走此路;外部 4 读口也走 ctx | 「引擎接触账本对象」——严格说是持**只读引用**,是否算「状态」取决于 §1 完成定义 1 的严格度 |
| **甲 保留**(纯查询,读 ctx) | `_fact_value` 的 `fact counter`(:328)在 `dispatch` 内部必须能读到——现役 4 枚条件面(`twin_digs`/`pain_layers`/`insurance_used`/`tree_sense_*`)全走此路;外部 4 读口也走 ctx | 「引擎接触计数器容器对象」——严格说是持**只读引用**,是否算「状态」取决于 §1 完成定义 1 的严格度 |
| **乙 连读也外置** | 外部 4 读口直读桶,不经引擎 | **引擎内部仍必须读**:`when` 条件的 `fact counter` 判定在 `_cond_ok` 内发生,无法外置到调用点,除非改成「payload 预携带全部计数器现值」——那等于每次发射都要快照整桶,成本与复杂度都更高。⇒ 乙实质只能做到「外部调用点不再经引擎」,引擎内部读不可消除 |
补充事实:`gte_ref` / `lt_ref` 两算子现役**零数据行使用**(仅登记在 `OPS` 词表),不构成 N5 约束。