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:
@@ -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 超预算 |
|
||||
|
||||
Reference in New Issue
Block a user