diff --git a/data/attributes.json b/data/attributes.json index 60fcd1e..2e83751 100644 --- a/data/attributes.json +++ b/data/attributes.json @@ -1,6 +1,6 @@ { "cpu_limit": { "display_name": "运算力 cpu_limit", "base": 0.0, "soft": 20.0, "hard": 50.0, "combine": "add_int" }, "move_speed": { "display_name": "移动速度 move_speed", "base": 200.0, "soft": 600.0, "hard": 800.0, "combine": "hybrid" }, - "cast_delay_mod": { "display_name": "施法延迟 cast_delay_mod", "base": 1.0, "soft": 0.1, "hard": 0.01, "combine": "inverse" }, + "cast_delay_mod": { "display_name": "施法延迟 cast_delay_mod", "base": 1.0, "soft": 0.1, "hard": 0.05, "combine": "inverse" }, "hp_max": { "display_name": "最大生命 hp_max", "base": 100.0, "soft": 2000.0, "hard": 0.0, "combine": "hybrid" } } diff --git a/docs/design/numerical_design.md b/docs/design/numerical_design.md index 74a1a26..a7ba3dd 100644 --- a/docs/design/numerical_design.md +++ b/docs/design/numerical_design.md @@ -18,7 +18,7 @@ | `hp_max` | 最大生命 | 100 | 2000 | - | | | `mana_max` | 最大魔力 | 100 | 1000 | - | 全局蓝条上限,法杖也有自己的上限,取 Min 值 | | `move_speed` | 移动速度 | **200** | 600 | 800 | 像素/秒。⚠️ **2026-07-31 订正**:原写 300 从未被任何代码读取过,是纸面孤值;实际实现自 S0 起即为 200(`player_manager.gd` 的 `const MOVE_SPEED`,现已改为读 `data/attributes.json`),20 波内容、Boss 弹幕密度、无敌帧 0.5 s 均按此值调校。此处以既成事实为准 —— 改回 300 等于用未经验证的数字推翻一整轮已调校的内容。理由详见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2。 | -| `cast_delay_mod` | 施法延迟修正 | 1.0 (100%) | 0.1 | 0.01 | 越低越快,乘算系数 | +| `cast_delay_mod` | 施法延迟修正 | 1.0 (100%) | 0.1 | **0.05** | 越低越快,乘算系数。⚠️ **2026-07-31 由 0.01 上调至 0.05**:`player_manager._handle_auto_cast` 用 `if` 而非 `while`,**每物理帧至多施法一次**,故本属性存在**饱和点** = `(1/60) / 法杖基准间隔` —— `wand_basic`(0.5 s) ≈ 0.033、`wand_fast`(0.25 s) ≈ 0.05。原 `0.01` 在两把主力杖上都深深落在饱和区内,即玩家买到 0.033 以下**再也换不到任何射速**,属"承诺了循环交付不了的东西"。取 0.05 使 `wand_fast` 恰好在硬上限处饱和、`wand_basic` 全程无无效区间。实测数据(可达档位是台阶而非曲线、周期恒为 `ceil(T×60)`)见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2b。 | | `recharge_speed_mod` | 充能速度修正 | 1.0 (100%) | 5.0 | - | 越低越快 | | `luck` | 幸运 | 0 | 100 | - | 影响高阶法术掉率、暴击率 | | `cpu_limit` | 运算力上限 | **0**(玩家侧) | 20 | 50 | `SpellEvaluator` 实际 MAX_OPS = `生效 cpu_limit × 40`,与 `implementation_plan.md §2.2` 的 `MAX_OPS_PER_CPU = 40` 常量对应。**生效值 = 玩家侧基准 0 + 法杖份额(`cores.json` 逐杖 3–8,以 `source="core"` 的加成来源接入)+ 未来的玩家加成**;`hard`(50) 作用于**加总后的生效值**,故法杖份额也受硬上限约束。玩家侧基准取 0 而非 5 是刻意的:生效值已含法杖的 3–8,取 0 使小木法杖仍为 5 → 200 步(现有平衡零改动),取 5 会让所有法杖的执行预算翻倍;语义上玩家属性是"全局加成",基准 0 正是纯加成语义。 ✅ **2026-07-31 已修复**(原 ⚠️「代码硬编码 MAX_OPS = 40×5 = 200,从不读 cpu_limit」现已失效):`spell_evaluator.gd` 改读 `MAX_OPS_PER_CPU * maxi(PlayerStats.cpu_limit, 1)`,`cores.json` 逐杖配置的运算力自此生效(高速法杖 3 → 120 步、小木 5 → 200、矩阵板 8 → 320,运行时实测)。设计见 `docs_dev/specs/2026-07-31-player-attributes-design.md` §2.2。 | diff --git a/docs_dev/plans/2026-07-31-player-attributes.md b/docs_dev/plans/2026-07-31-player-attributes.md index 532f836..0f54916 100644 --- a/docs_dev/plans/2026-07-31-player-attributes.md +++ b/docs_dev/plans/2026-07-31-player-attributes.md @@ -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`。 diff --git a/docs_dev/specs/2026-07-31-player-attributes-design.md b/docs_dev/specs/2026-07-31-player-attributes-design.md index ef4be63..9ae3ec7 100644 --- a/docs_dev/specs/2026-07-31-player-attributes-design.md +++ b/docs_dev/specs/2026-07-31-player-attributes-design.md @@ -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` **所有加成公式集中在一个专门模块**,与状态持有分离。