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:
@@ -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.1(soft) | 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`。
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
| :-- | --: | --: | --: | :-- | :-- |
|
||||
| `cpu_limit` | **0** | 20 | 50 | `add_int` | 修死数据;生效 MAX_OPS 用 `玩家 + 法杖` |
|
||||
| `move_speed` | **200** | 600 | 800 | `hybrid` | 由 `const` 转为属性 |
|
||||
| `cast_delay_mod` | 1.0 | 0.1 | 0.01 | `inverse` | 新增,乘到 `core.cast_interval` |
|
||||
| `cast_delay_mod` | 1.0 | 0.1 | **0.05** | `inverse` | 新增,在**开火时**乘到 `core.cast_interval`;`hard` 原定 0.01,2026-07-31 据实测上调至 0.05,见 §2.2b |
|
||||
| `hp_max` | 100 | 2000 | — | `hybrid` | 已可用,迁入框架 |
|
||||
|
||||
**`cpu_limit` 玩家基准取 0**(权威表写 5):因生效值 = 玩家 + 法杖,而法杖已提供 3–8。取 0 使小木法杖仍为 5 → MAX_OPS 200,**与当前行为完全一致,零平衡改动**;取 5 会让所有法杖的执行预算翻倍。语义上玩家属性是「全局加成」(权威 §1.1 开头:「这些属性是全局的,会修正所有法杖的输出」),基准为 0 正是纯加成语义。
|
||||
@@ -91,6 +91,93 @@ PlayerStats.add_modifier("cpu_limit", "flat", float(core.cpu_limit), "core")
|
||||
|
||||
**`mana_max` 本期不迁入**:权威 §1.1 写明「法杖也有自己的上限,取 Min 值」,而现状是核心值直接覆盖玩家值。那个 Min 语义需要独立的设计判断(取 Min 后玩家升级蓝上限在低上限法杖下完全无效,这是否是设计意图?),不应混进属性框架这一期顺手改。本期保持原样,记录为已知遗留。
|
||||
|
||||
### 2.2b `cast_delay_mod` 的饱和点 —— 实测数据与 `hard` 上调(2026-07-31 追记)
|
||||
|
||||
> 本节记录的是**运行时实测**,不是推导。数据来自验收期(Task 5 Step 7b②)向 `get_tree().root`
|
||||
> 挂载的临时 `_physics_process` 扫描节点:16 个相位 × 180 物理帧,逐相位切换法杖与
|
||||
> `cast_delay_mod`,记录**相邻两次施法的帧距分布**。
|
||||
|
||||
#### 现象
|
||||
|
||||
`player_manager._handle_auto_cast` 用 `if` 而非 `while`:
|
||||
|
||||
```gdscript
|
||||
_cast_timer -= delta
|
||||
if _cast_timer <= 0.0:
|
||||
_cast_timer = _cast_interval * PlayerStats.cast_delay_mod # 赋值,非 -= 或 +=
|
||||
SpellEvaluator.execute_compiled(...)
|
||||
```
|
||||
|
||||
因此**每物理帧至多施法一次**,60 Hz 物理步长下硬顶 60 次/秒。且 `_cast_timer` 是**赋值**而非累积扣减,
|
||||
亚帧余量每周期被丢弃 —— 实际射速按**整帧量化**。
|
||||
|
||||
#### 实测表
|
||||
|
||||
每个相位的帧距分布都是**单值**(如「1 帧×179」「2 帧×89」),这本身就是量化的直接证据。
|
||||
|
||||
| 法杖 | `cast_delay_mod` | 生效间隔 T | 实测帧距 | 实际速率 |
|
||||
| :-- | --: | --: | --: | --: |
|
||||
| `wand_basic`(基准 0.5 s) | 1.0 | 0.5 s | 31 帧 | 1.94 /s |
|
||||
| | 0.5 | 0.25 s | 16 帧 | 3.75 /s |
|
||||
| | **0.1(soft)** | 0.05 s | 3 帧 | **20 /s** |
|
||||
| | 0.07 | 0.035 s | 3 帧 | 20 /s |
|
||||
| | 0.06 | 0.03 s | 2 帧 | 30 /s |
|
||||
| | **0.05(现 hard)** | 0.025 s | 2 帧 | **30 /s** |
|
||||
| | 0.04 | 0.02 s | 2 帧 | 30 /s |
|
||||
| | 0.034 | 0.017 s | 2 帧 | 30 /s |
|
||||
| | **0.033** | 0.0165 s | 1 帧 | **60 /s ← 饱和点** |
|
||||
| | 0.02 | 0.01 s | 1 帧 | 60 /s |
|
||||
| | 0.01(原 hard) | 0.005 s | 1 帧 | 60 /s |
|
||||
| `wand_fast`(基准 0.25 s) | 1.0 | 0.25 s | 16 帧 | 3.75 /s |
|
||||
| | **0.1(soft)** | 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.01(原 hard) | 0.0025 s | 1 帧 | 60 /s |
|
||||
|
||||
#### 四条结论
|
||||
|
||||
1. **饱和点 = `(1/60) / 法杖基准间隔`**。
|
||||
| 法杖 | 基准间隔 | 饱和点 | 来源 |
|
||||
| :-- | --: | --: | :-- |
|
||||
| `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 帧)|
|
||||
| `circuit_fork` | 0.6 s | ≈ 0.0278 | **公式推导,未实测** |
|
||||
2. **soft→hard 之间的可达档位是台阶而非曲线**:`wand_basic` 只有 20 / 30 / 60 次每秒三档,
|
||||
`wand_fast` 只有 30 / 60 两档。中间值买不到。
|
||||
3. **周期恒为 `ceil(T × 60)` 帧,且 T 恰为整帧倍数时会多花一帧**(`T = 0.5` 得 **31** 帧而非 30,
|
||||
`T = 0.25` 得 **16** 帧而非 15):`_cast_timer` 是赋值,`0.5 - 30 × delta` 的浮点余量为正,
|
||||
要到第 31 帧才跨过 0。故**实际射速恒 ≤ 名义值,永不超发** —— 这是好事,但意味着名义
|
||||
「2 次/秒」实际是 1.94 次/秒。
|
||||
4. 综上,原 `hard: 0.01` 在两把主力杖上都**深深落在饱和区内**:`wand_basic` 白费 3.3 倍区间、
|
||||
`wand_fast` 白费 6.7 倍。玩家在货架 C 花钱把 `cast_delay_mod` 从 0.033 买到 0.01,**射速一点没变**。
|
||||
|
||||
#### 决定(用户 2026-07-31)
|
||||
|
||||
`hard: 0.01 → 0.05`。取 0.05 的依据正是上表:`wand_fast` 恰在 0.05 饱和,`wand_basic` 在 0.0333 饱和,
|
||||
故 0.05 使**两把主力杖都不再有「买了没用」的无效区间**,让 JSON 承诺的是真能交付的东西。
|
||||
|
||||
**明确不做**:不在代码里加下限钳制。下限已由 `attributes.json` + `AttributeFormula` 的
|
||||
`maxf(hard, ...)` 强制;在 `player_manager` 再加一个会形成**重复权威**,设计师调 `hard` 时立刻脱同步,
|
||||
违反 CLAUDE.md「纯数据驱动,代码不含硬编码副本或回退」。
|
||||
|
||||
#### 改动后的边界复测(`hard = 0.05` 落地后,同一套扫描)
|
||||
|
||||
| 法杖 | 请求 `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.1(soft) | 0.1 | 0.05 s | 3 帧×59 | 20 /s | ✅ 对照,soft 段未受影响 |
|
||||
| `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`」的必然结果,属可接受的取舍:**无效区间清零优先于人人吃满硬顶**
|
||||
> (无效区间是"花了钱没效果",吃不满硬顶只是"上限没那么高")。若日后引入基准间隔更长的
|
||||
> 法杖,此结论不变。
|
||||
|
||||
### 2.3 公式模块 —— `AttributeFormula`
|
||||
|
||||
**所有加成公式集中在一个专门模块**,与状态持有分离。
|
||||
|
||||
Reference in New Issue
Block a user