docs: 订正 wand_fast 饱和点 0.05→0.0667;roadmap 关闭已决定项并澄清移速表述

【订正】wand_fast(0.25s) 的饱和点按公式 (1/60)/0.25 = 0.0667,不是 0.05。
numerical_design:21 的标注与它自己上一句的公式直接矛盾,spec/plan 另有三处同源
错误标注——均派生自一句口头转述,实测表里记的 0.0667 一直是对的。

结论也随之改对,不只是改数字:hard=0.05 使 wand_basic(饱和 0.0333) 与
circuit_fork(0.0278) 无无效区间,但 wand_fast 仍剩 [0.05, 0.0667) 这段买不到东西
(较原 hard:0.01 的 6.7 倍缩至 1.33 倍)。仍取 0.05 的理由:饱和点 = (1/60)/法杖
基准间隔,随杖而异,不存在对所有杖都最优的单一 hard——取 0.0667 让 wand_fast 干净
会使另两把杖够不到自己的饱和点,反而制造新的不可达区间。hard 数值不变。

顺带说明实测表里 wand_fast@0.0667 读到 2 帧的原因:采样点比真实边界 1/15 高 3.3e-5
(T 上高 8.3e-6 s),再叠加整帧倍数多花一帧的浮点余量效应,故实际 1 帧上界略低于
0.0667。数据没错,是标签错了。

【roadmap】E3-① 下的 cast_delay_mod 条目仍记为「待人定」并指向两个选项,而该决定
已在 ba7c885 关闭——roadmap 是查「E3 还剩什么」的索引,留着会把已关闭的决定重新打开。
改为已处理,并注明彻底消除需让 hard 逐杖化(挪进 cores.json),属独立立项。

【roadmap】「实测 200→300 px/s」读起来像发版移速由 200 改成 300,与同批改动里
numerical_design 的订正(发版是 200,300 是从未被读过的纸面值)直接冲突。改写为
「发版基准仍是 200;实测手段是临时挂 +50% 加成使生效值变 300」。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 13:36:53 +08:00
co-authored by Claude Opus 5
parent ba7c885291
commit 84c10725e2
4 changed files with 52 additions and 16 deletions
@@ -89,13 +89,13 @@ E6-a 玩家无敌帧(i-frames) ← 先行小项:Boss 弹幕公平性前置,
**依赖**:货架 C 依赖「属性词条系统」先落地;核心抽取复用已有 `cores.json`/`make_core_by_id`
**切分(建议 4 个子计划,属性词条优先)**
1. ~~**属性词条系统**~~**完成**2026-07-31`feat/player-attributes`spec/plan `docs_dev/{specs,plans}/2026-07-31-player-attributes*`):`AttributeFormula` 纯静态公式模块(`hybrid` 连乘 / `inverse` 反向下限 / `add_int``pct` + `pct` 越界钳 0);`PlayerStats``_attr_def` + `_modifiers` 并把公式结果写回静态类型裸字段(读取端零开销);`add_modifier` / `remove_modifiers_from(source)` 按来源增撤;`data/attributes.json` + 设计器「属性」页。**接线三条死数据**:`spell_evaluator` MAX_OPS 改读生效 `cpu_limit`(法杖份额以 `source="core"` 加成接入,运行时实测 3→120 / 5→200 / 8→320 步)、`player_manager``const MOVE_SPEED` 改读 `PlayerStats.move_speed`实测 200300 px/s)、`_handle_auto_cast` 开火时乘 `cast_delay_mod`。删孤儿字段 `armor`/`resistance``hp_max`/`cpu_limit` 移出存档(改为派生值)。
1. ~~**属性词条系统**~~**完成**2026-07-31`feat/player-attributes`spec/plan `docs_dev/{specs,plans}/2026-07-31-player-attributes*`):`AttributeFormula` 纯静态公式模块(`hybrid` 连乘 / `inverse` 反向下限 / `add_int``pct` + `pct` 越界钳 0);`PlayerStats``_attr_def` + `_modifiers` 并把公式结果写回静态类型裸字段(读取端零开销);`add_modifier` / `remove_modifiers_from(source)` 按来源增撤;`data/attributes.json` + 设计器「属性」页。**接线三条死数据**:`spell_evaluator` MAX_OPS 改读生效 `cpu_limit`(法杖份额以 `source="core"` 加成接入,运行时实测 3→120 / 5→200 / 8→320 步)、`player_manager``const MOVE_SPEED` 改读 `PlayerStats.move_speed`**发版基准仍是 200**;实测手段是临时挂一条 `+50%` 加成使生效值变 300,测得玩家实际位移速度同步由 200300 px/s,证明读取端是活的)、`_handle_auto_cast` 开火时乘 `cast_delay_mod`。删孤儿字段 `armor`/`resistance``hp_max`/`cpu_limit` 移出存档(改为派生值)。
- ⚠️ **实际范围小于原描述**:原写「定义 4/5 缺失 player stats(如暴击/急速/范围/吸血/减伤)」—— 那些属性名是拟稿推测,不存在于任何权威文档(见上方「现状」订正)。本期只做**框架 + 三条死数据**,明确延后三项,各有理由:
- `attunement_fire/ice/lightning/poison`(4 个元素精通)—— 属**新增玩法维度**而非断线,需接入元素伤害管线,独立立项。
- `luck` —— 权威表定义它「影响暴击率」,而**暴击链目前是死的**(`bullet_manager.gd:81``is_crit` 硬编码为 `false``CastStats.crit_chance` 从未被掷判);打通暴击链是其前置。
- `recharge_speed_mod` —— 代码中**不存在充能系统**(全项目 grep `recharge` 零命中),属新增机制而非断线。
- 另:`mana_max` 的「与法杖取 Min」语义(权威 §1.1)未迁入框架,现状是核心值直接覆盖玩家值;那是独立设计判断,记为已知遗留。
- ⚠️ **`cast_delay_mod` 存在饱和区`hard: 0.01` 承诺了循环交付不了的东西**(运行时实测,待人定)`_handle_auto_cast``if` 而非 `while`,每物理帧至多一次施法 → 60 次/秒硬顶,实际速率按**整帧量化**。小木法杖(0.5s) `mod ≤ 0.0333` 即饱和,`wand_fast`(0.25s) `mod ≤ 0.0667` 即饱和;从饱和点到 `hard`(0.01) 之间买不到任何东西。数字与两个处理选项`docs_dev/plans/2026-07-31-player-attributes.md` Task 5 Step 7b②
- **`cast_delay_mod` 饱和区问题已处理(`hard: 0.01 → 0.05`,提交 `ba7c885`,决定已关闭)**`_handle_auto_cast``if` 而非 `while`,每物理帧至多一次施法 → 60 次/秒硬顶,实际速率按**整帧量化**,故饱和点 = `(1/60) / 法杖基准间隔``circuit_fork` 0.0278 / `wand_basic` 0.0333 / `wand_fast` 0.0667)。原 `hard: 0.01` 远在所有杖的饱和点之下,从饱和点0.01 射速纹丝不动。因饱和点随杖而异、不存在对所有杖都最优的单一 `hard`,取 0.05 为折中:`wand_basic` / `circuit_fork` 无无效区间,`wand_fast` 仍剩 `[0.05, 0.0667)`(较原来的 6.7 倍缩至 1.33 倍)。完整实测数据集`docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2b。若日后要把 `wand_fast` 那段也消掉,正确做法是让 `hard` **逐杖化**(挪进 `cores.json`)而非继续挪这个全局数 —— 属独立立项,**不是本期遗留待办**
2. **货架 C(属性购买)**:商店可购属性词条(消耗金币/以太),叠加到 PlayerStats。S~M。
3. **货架 B(核心抽取)**:商店提供 Core 抽取/更换(复用 `_CORE_ROSTER`),与现有换杖打通。S~M。
4. **出售退款 G5**:背包内法术/核心可出售,按 `numerical` 退款比例返还货币。S。
@@ -1015,8 +1015,8 @@ Run: godot-mcp-pro `stop_scene`,然后 `execute_editor_script` 扫四个文件
| `wand_fast`(基准 0.25 s | 1.0 | 0.25 s | 16 帧 | 3.75 /s |
| | **0.1soft** | 0.025 s | 2 帧 | **30 /s** |
| | 0.07 | 0.0175 s | 2 帧 | 30 /s |
| | 0.0667 | 0.016675 s | 2 帧 | 30 /s |
| | **0.05** | 0.0125 s | 1 帧 | **60 /s ← 饱和点** |
| | 0.0667 | 0.016675 s | 2 帧 | 30 /s ← 采样点比真实边界 1/15 高 3.3e-5,故落在 2 帧侧 |
| | **0.05** | 0.0125 s | 1 帧 | **60 /s ← 已在饱和区内(饱和点是 0.0667,非 0.05** |
| | **0.01hard** | 0.0025 s | 1 帧 | 60 /s |
结论(供决策):
@@ -1028,13 +1028,17 @@ Run: godot-mcp-pro `stop_scene`,然后 `execute_editor_script` 扫四个文件
**两个选项曾提交人定**(a) 保持 `hard: 0.01` 并在文档写明饱和区;(b) 把 `hard` 提到 ~0.05。
**✅ 用户 2026-07-31 决定:采用 (b)`hard: 0.01 → 0.05`。** 依据正是上表 —— `wand_fast` 恰在 0.05 饱和、`wand_basic` 在 0.0333 饱和,取 0.05 使两把主力杖都不再有「买了没用」的无效区间。落地三处:`data/attributes.json``numerical_design.md` §1.1、spec 新增 §2.2b(收录完整实测数据集)。
**✅ 用户 2026-07-31 决定:采用 (b)`hard: 0.01 → 0.05`。** 落地三处:`data/attributes.json``numerical_design.md` §1.1、spec 新增 §2.2b(收录完整实测数据集)。
依据:饱和点 = `(1/60) / 法杖基准间隔` = `circuit_fork` 0.0278 / `wand_basic` **0.0333** / `wand_fast` **0.0667**。**该值随杖而异,故不存在对所有杖都最优的单一 `hard`**:取 0.05 使 `wand_basic``circuit_fork` 无无效区间,`wand_fast` 仍剩 `[0.05, 0.0667)` 一小段(从原来的 6.7 倍缩到 1.33 倍);若改取 0.0667 让 `wand_fast` 干净,则 `wand_basic`/`circuit_fork` 反而够不到自己的饱和点,制造新的不可达区间。
> **📌 订正(2026-07-31**:本步初稿曾写「`wand_fast` 恰好在 0.05 饱和 / 两把主力杖都不再有无效区间」,**算错了** —— `(1/60)/0.25 = 0.0667` 而非 0.05。上方实测表里的 0.0667 一直是对的,错的是从口头转述派生的标注。`hard` 数值不受影响(0.05 仍是选定值,只是理由要说准)。
**边界复测**(改动后用同一套扫描重跑,这是本次改动唯一的行为差异,值得实测而非推导):
| 法杖 | 请求 `mod` | 生效 `mod` | T | 实测帧距 | 速率 |
| :-- | --: | --: | --: | --: | --: |
| `wand_fast` | 0.05 | 0.05 | 0.0125 s | **1 帧×179** | 60 /s恰好饱和,不浪费|
| `wand_fast` | 0.05 | 0.05 | 0.0125 s | **1 帧×179** | 60 /s已入饱和区;饱和点是 0.0667,故 `[0.05, 0.0667)` 仍是无效区间|
| `wand_fast` | 0.01 | **0.05** | 0.0125 s | 1 帧×179 | 60 /s`maxf(hard,…)` 钳制生效)|
| `wand_basic` | 0.05 | 0.05 | 0.025 s | **2 帧×89** | 30 /s |
| `wand_basic` | 0.01 | **0.05** | 0.025 s | 2 帧×89 | 30 /s(钳制生效)|
@@ -132,7 +132,7 @@ if _cast_timer <= 0.0:
| | **0.1soft** | 0.025 s | 2 帧 | **30 /s** |
| | 0.07 | 0.0175 s | 2 帧 | 30 /s |
| | 0.0667 | 0.016675 s | 2 帧 | 30 /s |
| | **0.05(现 hard** | 0.0125 s | 1 帧 | **60 /s ← 恰好饱和** |
| | **0.05(现 hard** | 0.0125 s | 1 帧 | **60 /s ← 已在饱和区内(饱和点是 0.0667,见下)** |
| | 0.01(原 hard | 0.0025 s | 1 帧 | 60 /s |
#### 四条结论
@@ -141,8 +141,13 @@ if _cast_timer <= 0.0:
| 法杖 | 基准间隔 | 饱和点 | 来源 |
| :-- | --: | --: | :-- |
| `wand_basic` | 0.5 s | ≈ **0.0333** | 实测(0.034 仍 2 帧、0.033 已 1 帧)|
| `wand_fast` | 0.25 s | ≈ **0.0667** | 实测0.0667 仍 2 帧、0.05 已 1 帧|
| `wand_fast` | 0.25 s | ≈ **0.0667**= 1/15| 实测夹在 (0.05, 0.0667] 内:0.0667 仍 2 帧、0.05 已 1 帧 |
| `circuit_fork` | 0.6 s | ≈ 0.0278 | **公式推导,未实测** |
> **关于 `wand_fast @ 0.0667` 读到 2 帧**:数据没错,是采样点的问题。真实边界是 `1/15 = 0.06666…`
> 我取的 `0.0667` 比它高 `3.3e-5`T 上高 `8.3e-6 s`),故落在 2 帧那侧。再叠加结论 3 的浮点余量效应
> —— T 恰为整帧倍数时反而多花一帧 —— **实际的 1 帧上界略低于 0.0667**。故 `wand_fast` 的饱和点
> 应读作「0.0667 稍下方」,本文一律以公式值 0.0667 记,误差方向是保守的(真实无效区间只会**更大**一点)。
2. **soft→hard 之间的可达档位是台阶而非曲线**`wand_basic` 只有 20 / 30 / 60 次每秒三档,
`wand_fast` 只有 30 / 60 两档。中间值买不到。
3. **周期恒为 `ceil(T × 60)` 帧,且 T 恰为整帧倍数时会多花一帧**`T = 0.5`**31** 帧而非 30
@@ -154,8 +159,31 @@ if _cast_timer <= 0.0:
#### 决定(用户 2026-07-31
`hard: 0.01 → 0.05`取 0.05 的依据正是上表:`wand_fast` 恰在 0.05 饱和,`wand_basic` 在 0.0333 饱和,
故 0.05 使**两把主力杖都不再有「买了没用」的无效区间**,让 JSON 承诺的是真能交付的东西。
`hard: 0.01 → 0.05`
**关键前提:饱和点 = `(1/60) / 法杖基准间隔`,随杖而异,因此不存在对所有杖都最优的单一 `hard`。**
`hard` 是**全局**的一个数,而每把杖的饱和点各不相同(0.0278 / 0.0333 / 0.0667),任何取值都只能覆盖一部分:
| `hard` 取值 | `circuit_fork`(0.0278) | `wand_basic`(0.0333) | `wand_fast`(0.0667) |
| :-- | :-- | :-- | :-- |
| 0.01(原值) | 无效区间 0.0278→0.01 | 无效区间 0.0333→0.01 | 无效区间 0.0667→0.016.7 倍)|
| **0.05(已采用)** | ✅ 无无效区间 | ✅ 无无效区间 | ⚠️ 仍剩 `[0.05, 0.0667)` |
| 0.0667 | ✅ | ✅ | ✅ 无无效区间 |
—— 但 `0.0667` 会让 `wand_basic`(饱和 0.0333)与 `circuit_fork`(0.0278)**够不到自己的饱和点**,
即它们的最高射速被硬上限提前截断,**制造出新的"不可达区间"**(想更快但买不到)。
**故 0.05 是折中**:让**多数杖**`wand_basic` / `circuit_fork`,以及所有基准间隔 ≥ 0.333 s 的杖)
无无效区间,只在最快的 `wand_fast` 上留 `[0.05, 0.0667)` 这一小段。相对原 `0.01`
(两把主力杖都深陷饱和区)是大幅改进,但**不是"两把主力杖都不再有无效区间"** ——
`wand_fast` 那段仍在,只是从 6.7 倍缩到 1.33 倍。
> **📌 订正记录(2026-07-31**:本节初稿曾写「`wand_fast` 恰好在 0.05 饱和 / 两把主力杖都无无效区间」,
> **算错了** —— 按同一节自己给出的公式 `(1/60)/0.25 = 0.0667`,不是 0.05。实测数据表(§2.2b 上方)
> 记的 0.0667 一直是对的,错的只是从那句口头转述派生出来的几处标注。已连同
> `numerical_design.md` §1.1、roadmap E3-①、plan Step 7b② 一并订正。
> **教训与本期其余三条同源**:结论若与自己上一句的公式矛盾,是最容易被略过的一类错 ——
> 因为读者会假定作者算过。
**明确不做**:不在代码里加下限钳制。下限已由 `attributes.json` + `AttributeFormula`
`maxf(hard, ...)` 强制;在 `player_manager` 再加一个会形成**重复权威**,设计师调 `hard` 时立刻脱同步,
@@ -165,7 +193,7 @@ if _cast_timer <= 0.0:
| 法杖 | 请求 `mod` | 生效 `mod` | 生效间隔 T | 实测帧距 | 实际速率 | 判读 |
| :-- | --: | --: | --: | --: | --: | :-- |
| `wand_fast` | 0.05 | 0.05 | 0.0125 s | **1 帧×179** | 60 /s | ✅ 恰好饱和,硬上限一格不浪费 |
| `wand_fast` | 0.05 | 0.05 | 0.0125 s | **1 帧×179** | 60 /s | ⚠️ 已达 60 /s,但饱和点是 0.0667 —— `[0.05, 0.0667)` 这段买了没用 |
| `wand_fast` | 0.01 | **0.05** | 0.0125 s | 1 帧×179 | 60 /s | ✅ `maxf(hard, …)` 生效,越界请求被钳回 |
| `wand_basic` | 0.05 | 0.05 | 0.025 s | **2 帧×89** | 30 /s | ✅ |
| `wand_basic` | 0.01 | **0.05** | 0.025 s | 2 帧×89 | 30 /s | ✅ 钳制生效 |
@@ -173,10 +201,14 @@ if _cast_timer <= 0.0:
| `circuit_fork` | 0.05 | 0.05 | 0.03 s | **2 帧×89** | 30 /s | ✅ 见下方遗留 |
> **遗留(实测)**`circuit_fork`0.6 s)在新 `hard` 0.05 处实测为 **2 帧/次(30 /s** ——
> 它的饱和点 0.0278(公式推导)低于 0.05,故**没有**无效区间,但也吃不满 60 /s。这是
> 「用最快的杖定 `hard`」的必然结果,属可接受的取舍:**无效区间清零优先于人人吃满硬顶**
> (无效区间是"花了钱没效果",吃不满硬顶只是"上限没那么高")。若日后引入基准间隔更长的
> 法杖,此结论不变。
> 它的饱和点 0.0278(公式推导)低于 0.05,故**没有**无效区间,但也吃不满 60 /s`wand_basic`
> 同理(0.0333 < 0.0530 /s)。反过来 `wand_fast`0.0667 > 0.05)能吃满 60 /s,代价是
> `[0.05, 0.0667)` 那段无效区间。
>
> **这正是"单一全局 `hard` 撞上逐杖饱和点"的必然取舍**:`hard` 偏小则慢杖有无效区间,偏大则
> 快杖被截断。0.05 选择了**让无效区间只留在一把杖上**,因为无效区间是"花了钱没效果"(欺骗),
> 而吃不满硬顶只是"上限没那么高"(诚实)。若日后要彻底消除,正确做法是让 `hard` 逐杖化
> (即把它变成 `cores.json` 的一个字段),而不是继续挪这个全局数 —— 那属于新的设计立项。
### 2.3 公式模块 —— `AttributeFormula`