docs(plan): 记录 CACHE_MODE_IGNORE 为验证 class_name 脚本的首选手段 + 加回填步骤

Task 1 实测发现:改完源码后经全局类名直调会拿到**过时结果**(返回修复前的
数值),是假通过陷阱——编辑器持有的已编译类是旧的。正确手段是
ResourceLoader.load(path, "GDScript", CACHE_MODE_IGNORE),它保留 class_name
原样且走引擎自己的编译器,故顺带证明静态类型标注真能被引擎编译。

另记 var x := <Variant 方法调用> 在 4.7.1 是无行号的编译错误而非警告。

新增 Task5 Step8:回填计划为实际执行版并勾复选框——归航那期踩过
「计划里留着过时代码和失效断言」的坑。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 11:04:44 +08:00
co-authored by Claude Opus 5
parent 9e3cbf8e5a
commit f86d6cd20e
+21 -7
View File
@@ -11,12 +11,16 @@
> **规范**:提交中文;纯数据驱动,文件缺失 `push_error` 不静默回退。spec`docs_dev/specs/2026-07-31-player-attributes-design.md`。 > **规范**:提交中文;纯数据驱动,文件缺失 `push_error` 不静默回退。spec`docs_dev/specs/2026-07-31-player-attributes-design.md`。
> **已知工具坑** > **已知工具坑**
> ① `validate_script` 对任何带 `class_name` 的脚本必然假阴性(报 `hides a global script class`)—— `attribute_formula.gd` 属此类;`player_stats.gd` / `player_manager.gd` / `spell_evaluator.gd` / `combat_manager.gd` / `attribute_tab.gd` / `designer_panel.gd` 无 `class_name`,可正常 `validate_script`。 > ① `validate_script` 对任何带 `class_name` 的脚本必然假阴性(报 `hides a global script class`)—— `attribute_formula.gd` 属此类;`player_stats.gd` / `player_manager.gd` / `spell_evaluator.gd` / `combat_manager.gd` / `attribute_tab.gd` / `designer_panel.gd` 无 `class_name`,可正常 `validate_script`。
> ② **`GDScript.new()` + `source_code` + `reload()` 这个绕法,在该类被引擎注册为全局类之后同样会失败**(同一条 `hides a global script class`)。Task 1 首次运行时因尚未注册而侥幸通过,**后续任务不能照抄**。正确做法二选一 > ② **验证 `class_name` 脚本的首选手段(Task 1 实测得出,优于下面两种绕法)**
> - **剥离 `class_name` 行再 `reload()`**`S.source_code = src.replace("class_name AttributeFormula\n", "")``@tool` 编辑器类同理剥自己的 `class_name`); > ```gdscript
> - 或**直接调用已注册的全局类**`AttributeFormula.compute(...)`(只验证已落盘版本时更省事)。 > var S = ResourceLoader.load(path, "GDScript", ResourceLoader.CACHE_MODE_IGNORE)
> ③ `@tool` 编辑器类在 dock 里被缓存,`load(...).new()` 拿到旧类,绕法同 ②。 > ```
> ④ `execute_editor_script` 静默阻止运行时 `FileAccess.WRITE` > 强制绕过引擎缓存、从磁盘重新编译,**保留 `class_name` 原样**且走引擎自己的编译器 —— 因此它顺带证明了静态类型标注(如 `Dictionary[String, Combine]`、`for m: Dictionary`)真能被引擎编译,而不只是能被动态副本解析。无需 `restart_editor`,也不必改源码
> ⑤ 新增 `.gd` 后 `.gd.uid` 不会自动出现,需 `EditorInterface.get_resource_filesystem().scan()` 触发扫描才生成;项目约定二者一并提交 > ③ **⚠️ 直接调用已注册的全局类名会拿到过时结果,是假通过陷阱。** Task 1 修完文件后经 `AttributeFormula.compute(...)` 直调,返回的仍是**修复前**的数值 —— 编辑器持有的已编译类是旧的。**改完源码后不要用全局类名验证**,用 ② 的 `CACHE_MODE_IGNORE`
> ④ `GDScript.new()` + `source_code` + `reload()` 这个绕法,在该类被注册为全局类之后会失败(同一条 `hides a global script class`);若要用必须先 `src.replace("class_name AttributeFormula\n", "")` 剥掉那行。`@tool` 编辑器类在 dock 里被缓存,同理。**优先用 ②。**
> ⑤ **`var x := <Variant 方法调用>` 在 4.7.1 是编译错误而非警告**,且报错无行号(`Script compilation failed`),极难定位。断言夹具里若持有动态 `GDScript` 引用 `F`,必须写 `var r: float = F.compute(...)` 而非 `var r := F.compute(...)`。
> ⑥ `execute_editor_script` 静默阻止运行时 `FileAccess.WRITE`。
> ⑦ 新增 `.gd` 后 `.gd.uid` 不会自动出现,需 `EditorInterface.get_resource_filesystem().scan()` 触发扫描才生成;项目约定二者一并提交。
> **关键既有事实**`spell_evaluator.gd:332` `var max_ops: int = MAX_OPS_PER_CPU * 5``MAX_OPS_PER_CPU = 40``:8`)。`player_manager.gd:7` `const MOVE_SPEED: float = 200.0``:45` 消费,`:88` `_cast_interval = core.cast_interval if core else 0.5`。`combat_manager.gd:257` `_rebuild_wand()``:267-271` 为 `mana_leech` 重算段。`player_stats.gd``hp_max` :10、`cpu_limit` :21、`armor` :22、`resistance` :23、`_ready()` :33、`reset_for_run()` :125、`get_save_data()` :135、`load_save_data()` :138。`CompiledDeck` **没有** `cpu_limit` 字段(只有 `feature_tags`),故 `spell_evaluator` 侧不需要新增字段。 > **关键既有事实**`spell_evaluator.gd:332` `var max_ops: int = MAX_OPS_PER_CPU * 5``MAX_OPS_PER_CPU = 40``:8`)。`player_manager.gd:7` `const MOVE_SPEED: float = 200.0``:45` 消费,`:88` `_cast_interval = core.cast_interval if core else 0.5`。`combat_manager.gd:257` `_rebuild_wand()``:267-271` 为 `mana_leech` 重算段。`player_stats.gd``hp_max` :10、`cpu_limit` :21、`armor` :22、`resistance` :23、`_ready()` :33、`reset_for_run()` :125、`get_save_data()` :135、`load_save_data()` :138。`CompiledDeck` **没有** `cpu_limit` 字段(只有 `feature_tags`),故 `spell_evaluator` 侧不需要新增字段。
--- ---
@@ -703,7 +707,17 @@ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
EOF EOF
``` ```
- [ ] **Step 8: 合并决策** - [ ] **Step 8: 回填本计划为实际执行版**
实现过程中若因评审而改动了代码或断言,**本计划里对应的代码块与脚本必须回填为实际落地的版本**,并勾上各任务的复选框。否则任何人按这份计划复跑,得到的是过时代码和可能已失效的断言 —— 归航那期就踩过这个坑。
已知必须回填的(Task 1 评审后代码有变):
- Task 1 Step 2 的实现代码块 → 加上 `pct` 越界守卫、`_COMBINE_NAMES``_COMBINE_BY_NAME``ri``floored``Dictionary[String, Combine]``for m: Dictionary` 的静态类型、以及 `hard` 语义差异的注释。
- Task 1 Step 3 的夹具 → 改用工具坑 ② 的 `ResourceLoader.load(..., CACHE_MODE_IGNORE)``cases` 补上 ⑦/⑦b 两条越界断言(共 12 条);注意工具坑 ⑤(`var r: float =` 而非 `var r :=`)。
其余任务同理:**以实际跑通的为准**,并在每处回填旁注明「原写法为何不可用」。
- [ ] **Step 9: 合并决策**
`superpowers:finishing-a-development-branch` 决定分支去向。前四个已完成子项(i-frames / 抗性 / 弹跳 / 归航)均合并进 `master` 并删分支。 `superpowers:finishing-a-development-branch` 决定分支去向。前四个已完成子项(i-frames / 抗性 / 弹跳 / 归航)均合并进 `master` 并删分支。