docs: 计划回填实际执行的验收脚本 + 订正权威文档自相矛盾处 + 性能数字改区间

【计划 Task 4】原稿四个脚本有缺陷,照它复跑会得到假失败与空通过,现已替换为
实际执行并通过的版本,每处旁注「原脚本为何有缺陷」,并勾上完成的复选框:
- Step 2 敌人摆位 (400,200) 距原点 447px 已出 homing_range=400,实测 FAILS=1;
  这是测试摆位错误而非实现缺陷(精确距离过滤行为正确)。改到 (300,150)。
- Step 6 内联复刻了折叠逻辑与商店谓词——在测自己;改调真实 _apply_modifier
  与 ShopManager._available_pool(),另补 compile_wand→execute_compiled 端到端。
- Step 7 无墙钟起搏,40 帧全停在退避窗内、守卫沦为空断言;且几何让子弹直穿
  敌人簇,多计约 1.5ms 碰撞开销。改为 60fps 起搏 + spec §5.2 环形摆位,
  frame1 与稳态峰值分开报。
- Step 8 只对源码 grep 字符串,不是往返;改为真实实例化 spell_tab 对真实
  spells.json 做 8 条 type-1 的回读→写回比对。
回溯表补「观测方式」列,标出⑥是唯一一条状态推断而非直接观测。

【architecture_design.md】同一文档内自相矛盾:
- CastStats 块 homing_force(「归航强度,向最近敌人偏转」)与 §4.2 新写的
  「最大转向角速度 + 锁定式目标」冲突 → 订正为 homing_add(弧度/秒);
  顺带订正确证过的 pierce_add / bounce_add(原写 *_count)。该块其余字段
  仍有既有漂移,加 ⚠️ 注明未核对、以 cast_stats.gd 为准,不扩大改动范围。
- ProjectileDef 的 homing_force 删除——projectile_def.gd 无任何 homing 字段,
  归航冷数据由 _push_projectile 直接写入 BulletManager。
- §4.2 bounce 协同散文补上实际存在的 if cold.has("homing_strength") 前置守卫。
- get_nearest_pos 代码片段:_enemy_count → _active_count(前者不存在)、
  未命中返回值 Vector2.ZERO → origin、删除不存在的 _visible_flags 过滤。

【spec】
- §4 第⑥条措辞「不触发 query_circle」字面为假(_check_collision 每帧无条件
  发一次),改为「不因归航触发」,并写明这是唯一一条间接验证及其封闭性论证。
- §5.5 峰值改为区间表述(① 4~5ms / ② 5~7ms @1500),注明编辑器/调试构建、
  运行间离散(同场景三次得 5.61/5.09/7.29ms),判定回归看是否出现周期性复发
  尖峰与是否越过 16.67ms,勿拿单值比对;并记录几何对测量的影响。
- §5.4 勘误:「1493 个互异到期值」不可能成立(jitter 只能产出约 200 个整数值,
  同帧失败又共用同一 now),复测为跨度 223ms / 222 个互异值,结论不变。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 17:33:16 +08:00
co-authored by Claude Opus 5
parent 4b09cd1064
commit 3e45ef0059
3 changed files with 399 additions and 139 deletions
+23 -11
View File
@@ -73,6 +73,9 @@ graph TD
#### A. 基础单元 (SpellNode)
这是所有"部件"的基类。
> **⚠️ 本块与实现存在既有漂移(2026-07-30 部分订正)**`pierce_add` / `bounce_add` / `homing_add` 三项已按 `scripts/domain/spell_system/cast_stats.gd` 实测订正(原写 `pierce_count` / `bounce_count` / `homing_force`)。**其余字段仍未核对**:实现里没有 `projectile_speed` / `spread_angle` / `projectile_size` / `range_mult`,实际为 `spread_count` / `speed_mult` / `radius_mult` / `lifetime` / `multicast_count`。此漂移早于本次归航改动,未一并订正以免超出改动范围 —— **以 `cast_stats.gd` 为准**。
```gdscript
# cast_stats.gd - MODIFIER 节点的修改目标;通过 SpellContext.stats 访问
class_name CastStats
@@ -82,17 +85,18 @@ var damage_add: float = 0.0 # 累加伤害加成(所有 Dmg+ 修正
var damage_mult: float = 1.0 # 乘算伤害系数(暴击、元素弱点等乘入)
var projectile_speed: float = 600.0 # 弹道飞行速度(像素/秒)
var spread_angle: float = 0.0 # 散射角度(度,0 = 直线)
var pierce_count: int = 0 # 穿透次数(0 = 首次命中即销毁
var bounce_count: int = 0 # 弹射次数(0 = 不弹射
var homing_force: float = 0.0 # 归航强度(0 = 直线,>0 向最近敌人偏转)
var pierce_add: int = 0 # MODIFIER 累加的穿透次数(叠加到 ACTION 自带 pierce
var bounce_add: int = 0 # MODIFIER 累加的弹跳次数(叠加到 ACTION 自带 bounce
var homing_add: float = 0.0 # MODIFIER 累加的归航【最大转向角速度】,单位 弧度/秒
# (叠加到 ACTION 自带 homing;语义见 §4.2 Homing 受限角速度制导)
var projectile_size: float = 1.0 # 弹道大小系数(影响碰撞半径与贴图缩放)
var range_mult: float = 1.0 # 射程乘算系数(弹道消失前飞行距离倍率)
var crit_chance: float = 0.0 # 本次施法暴击率追加量(0.0~1.0,叠加而非覆盖)
func reset() -> void:
damage_add = 0.0; damage_mult = 1.0; projectile_speed = 600.0
spread_angle = 0.0; pierce_count = 0; bounce_count = 0
homing_force = 0.0; projectile_size = 1.0; range_mult = 1.0; crit_chance = 0.0
spread_angle = 0.0; pierce_add = 0; bounce_add = 0
homing_add = 0.0; projectile_size = 1.0; range_mult = 1.0; crit_chance = 0.0
# spell_context.gd - 使用 RefCounted 避免 GC 压力
class_name SpellContext
@@ -192,7 +196,10 @@ var damage_type: int = DamageType.PHYSICAL # DamageType 枚举(见上方定
var speed: float = 600.0 # 初速度(像素/秒)
var direction: Vector2 = Vector2.RIGHT
var spread_angle_rad: float = 0.0 # 在 direction 基础上的随机偏转幅度
var homing_force: float = 0.0 # 每帧转向力(0 = 直线)
# 注:归航【不】经 ProjectileDef 传递。`projectile_def.gd` 无任何 homing 字段 ——
# SpellEvaluator._push_projectile 直接把 homing_strength / homing_range /
# homing_target_id 写进 BulletManager 的冷数据字典(详见 §4.2)。
# 原此处的 `homing_force`(「每帧转向力」)语义与实现均不存在,2026-07-30 删除。
var acceleration: float = 0.0 # 速度加速度(可为负,做减速球)
# ── 生命周期 ──────────────────────────────────────────────
@@ -675,7 +682,13 @@ func _node_has_tag(node: SpellNode, tag_pattern: String) -> bool:
> **⚠️ 失败退避必须带抖动**(spec §5.4,本次最值得记住的一条):重选**失败**时若不留记录,该子弹余生每帧都会重跑 `query_circle(r=400)`(半径 400 覆盖约 196 格 + 一次 `PackedInt32Array` 分配 ≈ 一百多次碰撞查询)。但**朴素固定退避会把子弹锁进同相、而非打散** —— 同帧失败的子弹拿到同一个 `now`,一个退避周期后又整齐地一起重试,尖峰每 12 帧永久复发。故抖动到期时刻,用「`bullet_idx` 在活跃弹数中的占比」铺满恰好一个退避周期(`HOMING_RETRY_MS` 一值两用:既是基础退避时长也是抖动窗口宽度)。实测数据见 spec §5.2 / §5.5,此处不复述以免两套数字分叉。
> **与 bounce 协同**:bounce 在命中瞬间硬重定向到「最近未访问敌」,若不协调,下一帧 homing 就会覆盖它、使 bounce 失效。语义定为**bounce 负责选目标、homing 负责追上去**bounce 重定向块内改写 `cold["homing_target_id"] = next_id` 并 `cold.erase("homing_retry_at")`(新目标有效,须撤销过期退避)。
> **与 bounce 协同**:bounce 在命中瞬间硬重定向到「最近未访问敌」,若不协调,下一帧 homing 就会覆盖它、使 bounce 失效。语义定为**bounce 负责选目标、homing 负责追上去**bounce 重定向块内
> ```gdscript
> if cold.has("homing_strength"): # 前置守卫:纯 bounce 弹不写 homing 键
> cold["homing_target_id"] = next_id
> cold.erase("homing_retry_at") # 新目标有效,须撤销过期退避
> ```
> `has("homing_strength")` 这个守卫不可省 —— 没有它,纯 bounce 子弹会被塞进一个永不被消费的 `homing_target_id`;而 `erase` 不可省是因为退避键若残留,子弹会对着 bounce 刚给的有效目标拒绝转向(该缺陷在实现期评审中实测复现并修复)。
* **EnemyManager 对外查询接口**:以下函数供 BulletManager、DropManager、BossManager、AudioManager 调用,均为 GDScript 侧接口(轻量读操作,非热路径):
@@ -713,17 +726,16 @@ func _node_has_tag(node: SpellNode, tag_pattern: String) -> bool:
func get_nearest_pos(origin: Vector2, max_dist: float = 9999.0) -> Vector2:
# BulletManager.get_nearest_enemy_pos() 调用;直接线性扫描 SoA _dataO(M)
# 仅在发射时调用,非每帧热路径(homing 走 get_pos_by_id 锁定目标,不经此函数)
var best_pos := Vector2.ZERO
var best_pos := origin # 无敌人时返回 origin(非 Vector2.ZERO
var best_dist := max_dist * max_dist # 比较平方距离,避免 sqrt
for i in range(_enemy_count):
if _visible_flags[i] == 0: continue # 跳过屏外(可选:也可全扫)
for i in _active_count: # _active_count,不存在 _enemy_count
var px := _data[i * ENEMY_STRIDE + 0]
var py := _data[i * ENEMY_STRIDE + 1]
var d2 := (px - origin.x) * (px - origin.x) + (py - origin.y) * (py - origin.y)
if d2 < best_dist:
best_dist = d2
best_pos = Vector2(px, py)
return best_pos
return best_pos # 实现全扫存活敌人,不按 _visible_flags 过滤
```
> **EnemyManager SoA 完整槽位布局**(权威来源:`implementation_plan.md §2.3.C`P6-N45):