fix(attr): cast_delay_mod 硬上限 0.01 → 0.05,消掉两把主力杖的射速无效区间

_handle_auto_cast 用 if 而非 while,每物理帧至多施法一次,故本属性存在饱和点
= (1/60) / 法杖基准间隔:wand_basic(0.5s) ≈ 0.033、wand_fast(0.25s) ≈ 0.05。
原 hard: 0.01 在两把主力杖上都深深落在饱和区内——玩家把 cast_delay_mod 从
0.033 一路买到 0.01,射速一点没变,属"承诺了循环交付不了的东西"。

取 0.05 使 wand_fast 恰在硬上限处饱和、wand_basic 全程无无效区间。
边界实测(同一套 _physics_process 帧距扫描):wand_fast@0.05 = 1 帧/次(60/s)、
wand_basic@0.05 = 2 帧/次(30/s)、请求 0.01 被 maxf(hard,…) 钳回 0.05 后帧距不变、
soft(0.1) 对照仍 3 帧/次(20/s)、circuit_fork@0.05 = 2 帧/次。

未加任何代码钳制:下限已由 attributes.json + AttributeFormula 的 maxf(hard,…)
强制,代码里再加一个会形成重复权威,设计师调 hard 时立刻脱同步。

spec 新增 §2.2b 收录完整实测数据集(饱和点公式与三把杖的值、soft→hard 是台阶
而非曲线、周期恒为 ceil(T×60) 且整帧倍数多花一帧故实际射速恒 ≤ 名义值)——
结论易得而数据难得,故一并留档。circuit_fork 的饱和点 0.0278 标注为公式推导。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 13:18:46 +08:00
co-authored by Claude Opus 5
parent d259205db2
commit ba7c885291
4 changed files with 105 additions and 7 deletions
+15 -4
View File
@@ -1026,11 +1026,22 @@ Run: godot-mcp-pro `stop_scene`,然后 `execute_editor_script` 扫四个文件
3. **soft→hard 整段只有 3 个可达档位**`wand_basic`20 / 30 / 60 次每秒),**`wand_fast` 只有 2 个**(30 / 60)—— 快端是台阶不是平滑曲线。
4. 帧距总是 `ceil(T × 60)`,且 `T` 恰为整帧倍数时会**多一帧**`T=0.5` 得 31 而非 30`T=0.25` 得 16 而非 15):`_cast_timer` 是赋值,浮点余量使它多跑一帧。故**实际速率永远 ≤ 名义速率**,不会超发。
**两个选项,需人定**
- (a) 保持 `hard: 0.01`,在 `numerical_design.md``attributes.json` 注释里写明「低于 `(1/60)/基准间隔` 为饱和区,不再提升实际射速」;
- (b) 把 `hard` 提到 ~0.05(约等于最慢法杖的饱和点),让 JSON 承诺的是真能交付的东西。
**两个选项曾提交人定**(a) 保持 `hard: 0.01` 并在文档写明饱和区;(b) 把 `hard` 提到 ~0.05。
**不要在代码里加下限钳制** —— 下限已由 `attributes.json` + `AttributeFormula``maxf(hard, ...)` 强制,在 `player_manager` 再加一个会形成重复权威,设计师调 `hard` 时立刻脱同步,违反「纯数据驱动,代码不含硬编码副本或回退」。**本步只上报数字,未改 `hard`、未加钳制。**
**✅ 用户 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(收录完整实测数据集)。
**边界复测**(改动后用同一套扫描重跑,这是本次改动唯一的行为差异,值得实测而非推导):
| 法杖 | 请求 `mod` | 生效 `mod` | T | 实测帧距 | 速率 |
| :-- | --: | --: | --: | --: | --: |
| `wand_fast` | 0.05 | 0.05 | 0.0125 s | **1 帧×179** | 60 /s(恰好饱和,不浪费)|
| `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(钳制生效)|
| `wand_basic` | 0.1soft | 0.1 | 0.05 s | 3 帧×59 | 20 /s(对照,未受影响)|
| `circuit_fork` | 0.05 | 0.05 | 0.03 s | 2 帧×89 | 30 /s(最慢杖吃不满硬顶,见 spec §2.2b 遗留)|
**未在代码里加下限钳制** —— 下限已由 `attributes.json` + `AttributeFormula``maxf(hard, ...)` 强制,在 `player_manager` 再加一个会形成重复权威,设计师调 `hard` 时立刻脱同步,违反「纯数据驱动,代码不含硬编码副本或回退」。
**③ 顺带收两条 Task 3 复审的 Minor**(提交 `9f71108`):
- `player_manager.gd` 守卫注释里的 `player_stats.gd:173/191` 行号引用会腐烂,改为函数名 `PlayerStats._load_attr_definitions` / `PlayerStats.add_modifier`