docs: 订正归航手感论证的运动学错误 + 记录定价越权与用户决策

【spec §2.0】原论证只算转弯半径就断言「急转的敌人能甩掉」,从未拿它对照敌人
实际速度 —— 明写此错,并补上缺的那一步运动学核算:跟踪横向速度 v、距离 d 的
目标所需角速度 ω≈v/d,故跟丢条件是 d < v/ω_max(近距离判据)。代入实际数值
(敌 45~140 px/s、弹 ~350 px/s) 重列强度表,标出发版值 1.5、给出各档对最快敌人
的跟丢距离。说明中远距离不是区分点(1.5 与 3.0 余量分别近 5 倍/10 倍,都能可靠
命中),真实失手方式是【近距离过冲】——目标坐进转弯圈内、几何上无法收敛,
而这正是 1.5 想要的手感。叠加表按 1.5 递增重列,堆到必中仍是合法 build 收益。

【spec §2.8】承认原「与 pierce/bounce 对齐」的论证绕过了权威:那两个词条本身
就不在定价表里,拿两个未定价条目互相对齐等于自建平行标准。而
numerical_design.md 确实给 homing 定过价(Tier 3 / Mana 40),且该表是活的权威——
其 Mana 列与 spells.json 每个已实现条目精确吻合,仅 homing 一行偏离。记录用户
决策及其取舍:这个词条能卖同族价,是因为它被调到了同族强度。

【numerical_design.md】homing 行按 P6-N2/N25 同样风格标注:原行删除线保留可
追溯(不抹掉「曾判定为 Tier 3」这个记录),表下补注说明 Homing Force 语义已废
(实现为最大转向角速度 rad/s,与 5.0 不同量纲不可比)、Mana 40 被同族标准取代、
实际发版 homing 1.5 / Mana 8 / 商店 18,并链接 spec。

【architecture_design.md】§4.2 冷数据注释的示例值 3.0 → 1.5(含转弯半径 233px)。

【plan】Task 1 Step 5/6 的 JSON 与断言期望值同步为 1.5;验收⑧(Step 6/6b)重跑
并更新为 folded=3.00 / e2e_2stack=3.000 / e2e_1stack=1.500;Step 8b 补设计器
往返与 SpinBox 格点确认输出。新增顶部醒目段落说明【机制测试①②③④⑦里的 3.0
是测试局部值、与发版值刻意解耦】——平衡调整不应导致机制测试失败,不要为了
数字一致去改它们;唯一应随发版值走的是⑧。Step 7 注明平衡调整不影响性能守卫
(强度只进转向数学,不参与决定 query_circle 频率的任何分支),未重跑。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 18:13:58 +08:00
co-authored by Claude Opus 5
parent 4b12ffbfb1
commit e651072b9b
4 changed files with 87 additions and 27 deletions
+15 -1
View File
@@ -99,10 +99,24 @@
| ID | 名称 | 类型 | Mana | Delay | 伤害类型 | 效果/修正 | 备注 |
| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
| `nuke` | 战术核弹 | Projectile | 200 | +3.00s | PHYSICAL | Dmg: 500, Radius: 300 | 屏幕清空,可能会炸死自己 |
| `homing` | 自动追踪 | Modifier | 40 | +0.10s | — | Homing Force: 5.0 | 只有这一个修正就够改变玩法 |
| `homing` | 自动追踪 | Modifier | ~~40~~ | +0.10s | — | ~~Homing Force: 5.0~~ | ~~只有这一个修正就够改变玩法~~ ⚠️ **本行已被取代(2026-07-30**,见下方注 |
| `heavy_cost` | 鲜血献祭 | Modifier | 0 | -0.50s | — | Dmg: +50, Cost: 10 HP | 卖血流 |
| `chain_bolt` | 连锁闪电 | Projectile | 60 | +0.80s | LIGHTNING | 弹射 5 次 | 清怪神器 |
> **⚠️ `homing` 行注(2026-07-30E1-③ 归航实装)** —— 原行**保留不改写**,以留存「曾判定其为 Tier 3」这一判断记录。实际发版取代如下:
>
> | | 原表(Tier 3 | 实际发版(`data/spells.json` `modifier_homing` |
> | :-- | :-- | :-- |
> | 强度 | `Homing Force: 5.0` | `homing: 1.5` |
> | Mana | 40 | 8 |
> | 商店价 | — | 18 |
>
> 1. **`Homing Force` 这一语义已废。** 实现的量是**最大转向角速度(弧度/秒)**,不是「转向力」—— 每帧把速度矢量朝锁定目标旋转,单帧转角上限 `strength × delta`,速率不变。`5.0` 与 `1.5` 不是同一量纲的数,不可直接比较。
> 2. **Mana 40 / Tier 3 的定价被同族标准取代**:与 `modifier_pierce_plus`、`modifier_bounce` 同为 8 蓝 / 18 金。**代价是把强度降到 1.5 rad/s 使其名副其实** —— 运动学核算表明原设想的 3.0 rad/s 对本工程敌人速度(45~140 px/s)而言接近「稳定命中」,确实配得上原表 Tier 3 的判断;1.5 rad/s 才是「中度制导」(对最快敌人在 93 px 内会跟丢)。
> 3. 该表其余行的 Mana 列与 `spells.json` 已实现条目逐一吻合,**仅此行曾出现偏离**,现予记录消解。
>
> 权威细节见 spec [`docs_dev/specs/2026-07-30-bullet-homing-design.md`](../../docs_dev/specs/2026-07-30-bullet-homing-design.md) §2.0(运动学核算)与 §2.8(定价决策)。
---
## 3. 敌人与战斗数值 (Combat Metrics)
+2 -1
View File
@@ -632,7 +632,8 @@ func _node_has_tag(node: SpellNode, tag_pattern: String) -> bool:
```gdscript
# 冷数据字段(_bullet_contexts[bullet_idx]):
# homing_strength : float —— 最大转向角速度,单位 弧度/秒(3.0 ≈ 350 速度下转弯半径 117px)
# homing_strength : float —— 最大转向角速度,单位 弧度/秒
# (单层词条发版值 1.5 ≈ 350 速度下转弯半径 233px)
# homing_range : float —— 选目标搜索半径,默认 400.0
# homing_target_id: int —— 锁定的 entity_id-1 = 未锁定/待重选
# homing_retry_at : int —— 重选失败后的退避到期时刻(ms 墙钟),成功即 erase
+35 -13
View File
@@ -82,8 +82,9 @@ var homing_add: float = 0.0 # MODIFIER 累加的归航角速度(弧度/
`data/spells.json``modifier_bounce` 行之后(同为 type 1 修饰器区)新增一行(注意逗号):
```json
"modifier_homing": { "type": 1, "display_name": "Homing", "description": "后续法术获得归航(飞行中以受限角速度转向锁定的敌人)。", "element_tags": [], "meta": { "homing": 3.0, "mana_cost": 8, "shop_cost": 18 } },
"modifier_homing": { "type": 1, "display_name": "Homing", "description": "后续法术获得归航(飞行中以受限角速度转向锁定的敌人)。", "element_tags": [], "meta": { "homing": 1.5, "mana_cost": 8, "shop_cost": 18 } },
```
> **`homing` 值于 2026-07-30 发版前平衡复核由 `3.0` 降为 `1.5`**`mana_cost` / `shop_cost` 不变)。原 3.0 经运动学核算实为「接近稳定命中」,配不上 8 蓝同族价;1.5 才兑现 spec §2.0 的「中度制导」。理由与定价决策见 spec §2.0 / §2.8 与 `docs/design/numerical_design.md` Tier 3 表下的 `homing` 行注。
- [x] **Step 6: 校验语法 + 数据**
@@ -93,11 +94,11 @@ Run: godot-mcp-pro `execute_editor_script`
```gdscript
var d = JSON.parse_string(FileAccess.get_file_as_string("res://data/spells.json"))
var ok: bool = d is Dictionary and d.has("modifier_homing") \
and abs(float(d["modifier_homing"]["meta"].get("homing", 0.0)) - 3.0) < 0.001 \
and abs(float(d["modifier_homing"]["meta"].get("homing", 0.0)) - 1.5) < 0.001 \
and int(d["modifier_homing"]["meta"].get("shop_cost", 0)) == 18
_mcp_print("FAILS=%d" % (0 if ok else 1))
```
Expected: `FAILS=0`
Expected: `FAILS=0``1.5` 为 2026-07-30 平衡复核后的发版值,原 3.0)
- [x] **Step 7: 提交**
@@ -303,6 +304,16 @@ EOF
**Files:** 无(运行时断言)+ 收尾文档
> ### ⚠️ 关于脚本里的 `homing_strength: 3.0`(读 Step 2~5 之前先看这段)
>
> **机制测试(①②③④⑦,Step 2 / 2b / 3 / 4 / 5)里的 `3.0` 是测试局部值,与 `modifier_homing` 的发版值刻意解耦。** 这些脚本**直接构造冷数据字典**传给 `spawn_bullet`**不读 `data/spells.json`** —— 它们验证的是转向数学(角速度上限、速率守恒、最短转向、目标重选、bounce 协同),与词条卖多少钱、单层给多少强度无关。
>
> 选 `3.0` 是因为 `3.0 / 60 = 0.05` 是个干净的判据数,断言里 `cap=0.050000` 一眼可读。
>
> **2026-07-30 平衡调整把发版值从 3.0 降到 1.5(见 spec §2.0/§2.8),这些脚本刻意不跟着改。** 平衡调整**不应该**导致机制测试失败 —— 若把它们改成 1.5,单帧上限变成 0.025,判据数变难读,而且下次调平衡又得再改一轮。**这是有意的设计,不要为了「数字一致」去改。**
>
> 唯一**应当**跟随发版值的是 **⑧(Step 6 / 6b)** —— 它测的正是「JSON → `_apply_modifier` 折叠 → 冷数据」这条数据驱动链路,期望值必须等于发版值。
- [x] **Step 1: 启动战斗场景**
Run: godot-mcp-pro `play_scene``res://scenes/main/combat_s2.tscn`。确认运行、`get_editor_errors` count=0。
@@ -544,7 +555,7 @@ ctx.stats.reset()
SpellEvaluator._apply_modifier(sp, ctx)
SpellEvaluator._apply_modifier(sp, ctx)
var folded: float = ctx.stats.homing_add
if abs(folded - 6.0) > 0.001: fails += 1 # ⑧ 两层 → 6.0
if abs(folded - 3.0) > 0.001: fails += 1 # ⑧ 两层 → 3.0= 2 × 发版值 1.5
ctx.stats.reset()
if abs(ctx.stats.homing_add) > 0.001: fails += 1 # reset 归零
# ⑨ 前半:走真实商店可抽池(而非复刻谓词)
@@ -555,10 +566,11 @@ _mcp_print("FAILS=%d folded=%.2f after_reset=%.2f in_pool=%s pool_size=%d shop_c
[fails, folded, ctx.stats.homing_add, str(in_pool), pool.size(),
int(sp.meta.get("shop_cost", 0)), int(sp.meta.get("unlock_cost", 0)), int(sp.meta.get("mana_cost", 0))])
```
实测输出:
实测输出2026-07-30 平衡调整 `homing 3.0 → 1.5` 后重跑)
```
FAILS=0 folded=6.00 after_reset=0.00 in_pool=true pool_size=17 shop_cost=18 unlock_cost=0 mana_cost=8
FAILS=0 per_stack=1.50 folded_2x=3.00 after_reset=0.00 in_pool=true shop_cost=18 unlock_cost=0 mana_cost=8
```
> **⑧ 的期望值随发版值走**:单层 = `spells.json` 的 `meta.homing`(现为 **1.5**),两层折叠 = 3.0。若日后再调平衡,这条断言的期望值须同步更新 —— 它**有意**耦合发版值,测的正是「JSON → 折叠 → 冷数据」这条数据驱动链路。
- [x] **Step 6b: 整条 VM 管线端到端(验收⑧ 补强)**
@@ -579,7 +591,7 @@ var fails := 0
if BulletManager.get_active_count() < 1: fails += 1
var cold: Dictionary = BulletManager._bullet_contexts.get(0, {})
var hs: float = float(cold.get("homing_strength", -1.0))
if abs(hs - 6.0) > 0.001: fails += 1
if abs(hs - 3.0) > 0.001: fails += 1 # 2 × 发版值 1.5
if int(cold.get("homing_target_id", -99)) != -1: fails += 1 # 发射时不解析目标
if abs(float(cold.get("homing_range", -1.0)) - 400.0) > 0.001: fails += 1
# 对照:不带修饰器的同一 ACTION 不应写任何 homing 冷数据
@@ -592,14 +604,17 @@ if cold2.has("homing_strength"): fails += 1
_mcp_print("FAILS=%d homing_strength=%.3f cold=%s" % [fails, hs, str(cold)])
_mcp_print("control_cold=%s" % str(cold2))
```
实测输出:
实测输出2026-07-30 平衡调整后重跑,含单层对照)
```
FAILS=0 homing_strength=6.000 cold={ "homing_strength": 6.0, "homing_range": 400.0, "homing_target_id": -1 }
FAILS=0 per_stack=1.50 folded_2x=3.00 e2e_2stack=3.000 e2e_1stack=1.500 mana=8 shop=18
cold_2stack={ "homing_strength": 3.0, "homing_range": 400.0, "homing_target_id": -1 }
control_cold={ }
```
- [x] **Step 7: 性能回归守卫 —— R-H1 两种病态场景的单帧峰值**
> **2026-07-30 平衡调整(`homing 3.0 → 1.5`)不影响本步,未重跑。** 依据:`homing_strength` 只在 `_apply_homing` 末尾进入转向数学(`clampf(diff, ±strength*delta)`),**不参与**任何决定 `query_circle` 频率的分支 —— 查询频率只由「目标是否有效」与 `homing_retry_at` 退避窗决定,二者与强度无关(`strength <= 0.0` 的提前 return 是唯一例外,1.5 不触发)。且本步脚本用的是自己的测试局部值 3.0,字面上也未被改动。
> **本步性质已变更(2026-07-30,Task 2 代码质量评审后)**:原本这是「决策关卡」—— 实测后再定要不要启用 spec §5 的退避方案。评审在 Task 2 阶段就直接实测并**证实触发**(300 弹规模即越过 2.0 ms 线),故退避 + 抖动**已随 Task 2 落地**。本步因此降级为**回归守卫**:确认退避与抖动仍生效、峰值未回弹。
>
> **两种病态场景都要测**(原计划只写了第一种,漏了更糟的第二种):
@@ -739,17 +754,24 @@ for id in d.keys():
if not src.contains('"k": "%s"' % k): missing.append("%s.%s" % [id, k])
var m = d.get("modifier_homing", {}).get("meta", {})
var ok: bool = d.has("modifier_homing") \
and absf(float(m.get("homing", 0.0)) - 3.0) < 0.001 \
and absf(float(m.get("homing", 0.0)) - 1.5) < 0.001 \
and int(m.get("shop_cost", 0)) == 18 and int(m.get("mana_cost", 0)) == 8 \
and int(d["modifier_homing"].get("type", -1)) == 1
_mcp_print("schema_FAILS=%d missing=%s" % [missing.size(), str(missing)])
_mcp_print("json_FAILS=%d modifier_homing.meta=%s" % [(0 if ok else 1), str(m)])
```
实测输出:
实测输出2026-07-30 平衡调整后重跑)
```
schema_FAILS=0 missing=[]
json_FAILS=0 modifier_homing.meta={ "homing": 3.0, "mana_cost": 8.0, "shop_cost": 18.0 }
JSON_FAILS=0 spells_total=24 modifier_homing.meta={ "homing": 1.5, "mana_cost": 8.0, "shop_cost": 18.0 }
spinbox_grid: 1.5/0.05 = 30.000000 -> 整数格点=true
modifier_pierce_plus mana=8 shop=18
modifier_bounce mana=8 shop=18
modifier_homing mana=8 shop=18
ROUNDTRIP_FAILS=0 type1_checked=8
modifier_homing 往返后 meta={ "mana_cost": 8, "shop_cost": 18, "homing": 1.5 } 逃生舱=(空)
```
> 设计器 `homing` 字段的 `"d": 0.0` 是「未设置」默认,**不是发版值,不随平衡调整而改**。已确认 `1.5` 正落在 SpinBox `step=0.05` 的整数格点上(1.5/0.05 = 30),往返无损。
`validate_script``bullet_manager.gd` / `enemy_manager.gd` / `spell_evaluator.gd` / `spell_tab.gd``Script compiles successfully``cast_stats.gd` 唯一报错是已知假阴性 `Class "CastStats" hides a global script class`(带 `class_name` 的脚本必然如此)。`get_editor_errors` count=0。
- [x] **Step 9: 更新路线图 + memory + 提交**
@@ -787,7 +809,7 @@ EOF
| 5 | 目标失效后重选 | Step 4 (`locked=1 → relocked=2`) | 直接 | ✅ |
| 6 | 空场直行且**不因归航**查询 | Step 4 (方向 6 帧恒定;`tid` 未变 + 无 `homing_retry_at`) | **状态推断**(唯一一条非直接观测,论证见 spec §4⑥) | ✅ |
| 7 | bounce 协同改写 `homing_target_id` | Step 5 (`tgt == e2`,且过期退避键被 erase) | 直接 | ✅ |
| 8 | 两层修饰器 → `homing_add == 6.0` | Step 6(真实 `_apply_modifier`)、Step 6b(端到端施法) | 直接 | ✅ |
| 8 | 两层修饰器 → `homing_add == 2 × 发版值`(现 `1.5×2 = 3.0` | Step 6(真实 `_apply_modifier`)、Step 6b(端到端施法) | 直接 | ✅ |
| 9 | 商店可购 + 设计器字段往返 | Step 6(真实 `_available_pool()`)/ Step 8(真实实例往返 8 条) | 直接 | ✅ |
| 10 | `validate_script` 通过 / 无目标不崩 | Task 1/2/3 各校验步 + Step 8b;无目标不崩由 Step 4 与 Step 7 数千次失败重选覆盖 | 直接 | ✅ |
| — | R-H1 退避+抖动的性能回归守卫(两种病态场景 × 两规模 × 单帧峰值,spec §5) | Step 7 | 直接 | ✅ 稳态 0/39 超预算 |
@@ -20,19 +20,29 @@
### 2.0 手感定位
**中度制导**:明显画弧追踪,但有最大转向角速度上限 —— 追不上贴脸急转的目标,会「甩尾」绕过去再回头。
**中度制导**:明显画弧追踪,但有最大转向角速度上限 —— 贴脸时会过冲、绕过去再回头。
反面:不做「轻度辅助瞄准」(玩家感知不到花了 8 蓝买了什么),也不做「每帧速度矢量直指目标」的强锁定(近乎必中,会让手瞄与闪避设计同时失效)。
转弯半径 = 速度 ÷ 角速度。主流子弹速度 ~350(`spark_bolt` 350 / `fire_bolt` 320 / `frost_bolt` 360):
> **⚠️ 原论证有错,已订正(2026-07-30,发版前平衡复核)**:本节初稿只算了转弯半径,就断言 3.0 rad/s「急转的敌人能甩掉」—— **算了转弯半径,却从未拿它对照敌人的实际速度**。补做运动学核算后结论反转:3.0 rad/s 实际接近「稳定命中」。单层词条值因此由 **3.0 降为 1.5 rad/s**,详见下方核算与 §2.8 定价说明。
| 角速度 | 350 速度下转弯半径 | 手感 |
| :-- | :-- | :-- |
| 2 rad/s | 175 px | 偏弱,大圆弧 |
| **3 rad/s(单层词条)** | **117 px** | **明显制导,急转的敌人能甩掉** |
| 6 rad/s(叠 2 层) | 58 px | 很难甩掉 |
| 12 rad/s(叠 4 层) | 29 px | 接近必中 |
### 运动学核算(原论证缺的那一步)
不设叠加上限 —— 堆到必中是合法 build 收益,与 bounce 堆叠同理
跟踪一个相对子弹横向速度为 `v`、距离为 `d` 的目标,所需角速度约为 **`ω ≈ v / d`**。子弹能提供的上限是 `homing_strength`,故**跟丢的条件是 `d < v / ω_max`** —— 注意这是个**近距离**判据:距离越近,同样的横向速度要求越高的角速度
代入本工程实际数值(`data/enemies.json` 敌人速度 **45 ~ 140 px/s**;主流子弹速度 **~350 px/s**`spark_bolt` 350 / `fire_bolt` 320 / `frost_bolt` 360):
| 角速度 | 转弯半径(350 速度) | 对最快敌人(140 px/s)的跟丢距离 `v/ω` | 手感 |
| :-- | --: | --: | :-- |
| 1.0 rad/s | 350 px | 140 px | 偏弱,大圆弧 |
| **1.5 rad/s(单层词条·发版值)** | **233 px** | **93 px** | **中度制导:中远距可靠修正,贴脸急转能甩掉** |
| 3.0 rad/s(叠 2 层) | 117 px | 47 px | 很难甩掉,接近稳定命中 |
| 6.0 rad/s(叠 4 层) | 58 px | 23 px | 已在碰撞半径量级,接近必中 |
**远距离拦截不是区分点。** 400 px 接近过程耗时约 `400/350 ≈ 1.14 s`,其间最快敌人横移约 160 px,所需航向修正仅约 `atan(160/400) ≈ 0.38 rad`;而 1.5 rad/s 在同样时间内可转 **1.71 rad**,余量近 5 倍。3.0 rad/s 的余量则接近 10 倍 —— 两者在中远距**都**能可靠命中,差别根本体现不出来。这正是原论证失效的地方:它比较的是两个都远超需求的数字。
**真实的失手方式是近距离过冲**,不是「被甩掉」。目标进入子弹的转弯圈(半径 233 px)以内时,子弹的最小转弯半径大于它到目标的距离,几何上无法收敛 —— 会从目标旁掠过、绕一个大弧再回头。1.5 rad/s 把这个「过冲区」放大到有意义的尺度(93 px 内对最快敌人必然跟丢,233 px 转弯圈使贴身缠斗时反复过冲),**这正是本节想要的手感**:远处放心开火,近身仍需走位与手瞄。3.0 rad/s 下过冲区只有 47 px,已小到玩家感知不到。
不设叠加上限 —— 堆到必中是合法 build 收益,与 bounce 堆叠同理;叠 2 层即回到 3.0、叠 4 层 6.0,正是玩家用词条位换来的强度。
### 2.1 冷数据字段(子弹 `_bullet_contexts[idx]`
@@ -133,11 +143,23 @@ bounce 现有行为一行不改(仍是命中瞬间硬重定向),homing 只
### 2.8 修饰器数据(`data/spells.json`
```json
"modifier_homing": { "type": 1, "display_name": "Homing", "description": "后续法术获得归航(飞行中以受限角速度转向锁定的敌人)。", "element_tags": [], "meta": { "homing": 3.0, "mana_cost": 8, "shop_cost": 18 } }
"modifier_homing": { "type": 1, "display_name": "Homing", "description": "后续法术获得归航(飞行中以受限角速度转向锁定的敌人)。", "element_tags": [], "meta": { "homing": 1.5, "mana_cost": 8, "shop_cost": 18 } }
```
`mana_cost` / `shop_cost``modifier_pierce_plus``modifier_bounce` 完全对齐。`shop_cost>0` → 自动进商店池。
> **⚠️ 定价论证的订正与用户决策(2026-07-30,发版前平衡复核)**
>
> **原论证绕过了权威。** 本节初稿的理由是「与 pierce/bounce 对齐」,但那两个词条**根本不在定价权威表里** —— 拿两个同样未被该表定过价的条目互相对齐,等于自建了一套平行标准。而 `docs/design/numerical_design.md` 的法术数据库**确实给 homing 定过价****Tier 3(史诗/质变)· Mana 40**,备注「只有这一个修正就够改变玩法」。
>
> 该表是**活的定价权威**,不是过时草案 —— 复核确认它的 Mana 列与 `spells.json` 里**每一个**已实现条目精确吻合(`double_cast` 2 / `spread_mod` 0 / `damage_plus` 15 / `trigger_hit` 10 / `heavy_cost` 0 / `chain_bolt` 60 / `energy_orb` 20),**只有 homing 这一行对不上**。所以这是一处真实的越权定价,不是表本身失准。
>
> **用户决策:降强度到 1.5 rad/s,保持 8 蓝 / 18 金。** 三个选项中(① 抬价到 40 蓝 Tier 3;② 保持 8 蓝但承认越权;③ 降强度使 8 蓝名副其实),选 ③。理由:它是唯一**同时**兑现 §2.0 原批准的设计意图(中度制导、可被甩掉)又**不需要连带复核蓝耗曲线**的选项 —— 抬到 40 蓝会牵动整条 Tier 3 定价与玩家蓝池预算,属另一次立项。
>
> 换言之:**这个词条之所以能卖同族价,是因为它被调到了同族强度**,而不是因为「和 pierce/bounce 对齐」这条论证本身成立。§2.0 的运动学核算证明 3.0 rad/s 确实值 Tier 3 的评价(接近稳定命中),原表的判断是对的。
>
> `numerical_design.md` 对应行已在同日标注:原 `Homing Force: 5.0` / Mana 40 保留可追溯,并注明实际发版为 `homing 1.5` / Mana 8。
**不新增自带归航的 ACTION 法术** —— 只出 `modifier_homing` 一个词条,与 bounce 当期只加修饰器的做法一致。
### 2.9 设计器字段(`addons/game_designer/spell_tab.gd`
@@ -166,7 +188,8 @@ MODIFIER`type 1`)的 meta schema 加:
全部经 Godot MCP 运行时实测,断言确定性数值而非目测:
1. **归航生效**`homing=3.0` 子弹朝偏移目标画弧并命中;`homing=0` 对照组直线飞过。断言速度矢量方向随帧变化(对照组不变)。
1. **归航生效**归航子弹朝偏移目标画弧并命中;`homing=0` 对照组直线飞过。断言速度矢量方向随帧变化(对照组不变)。
> 机制类验收(①②③④⑦)的测试脚本直接构造冷数据、**不读 `spells.json`**,固定用**测试局部值 `homing_strength = 3.0`**`3.0/60 = 0.05` 是个干净的单帧判据数)。该值与 `modifier_homing` 的**发版值(现 1.5,见 §2.0)刻意解耦** —— 平衡调整不应导致机制测试失败。唯一随发版值走的是 ⑧。
2. **角速度上限**:单帧转角 ≤ `homing_strength * delta`(浮点容差内)。
3. **速率守恒**:转向前后 `Vector2(vx,vy).length()` 不变。
4. **最短转向**:目标位于子弹正后方偏一侧时,转向方向为夹角较小的一侧(验证 `wrapf`)。
@@ -182,7 +205,7 @@ MODIFIER`type 1`)的 meta schema 加:
>
> 论证对被限定后的命题成立,但**它是状态推断而非对调用次数的直接观测** —— 如实标注,不宣称为直接测量。
7. **bounce 协同**`homing + bounce` 子弹弹跳后 `homing_target_id == ` bounce 选中的 `next_id`
8. **修饰器折叠**:两层 `modifier_homing``ctx.stats.homing_add == 6.0`
8. **修饰器折叠**:两层 `modifier_homing``ctx.stats.homing_add == 2 ×` 发版值(现 `1.5 × 2 = 3.0`)。**本条有意耦合发版值**,测的正是「JSON → 折叠 → 冷数据」这条数据驱动链路,平衡调整时须同步更新期望值
9. **商店与设计器**`modifier_homing` 可在商店买到;设计器 `homing`/`bounce` 字段回读往返一致(对真实 `data/spells.json`)。
10. `validate_script` 通过;纯数据驱动;无目标时不崩。