【订正】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>
25 KiB
玩家属性系统(Player Attributes)设计
日期:2026-07-31 Epic:E3 经济与 build · 子计划 ①(缺失功能路线图
docs_dev/plans/2026-07-23-missing-features-roadmap.md) 优先级:P0 · 工作量 M 目标:建立数据驱动的玩家属性框架(含独立的加成公式模块),并接线三条既有死数据;为子计划 ②「货架 C(属性购买)」提供接入点。
0. 范围界定(重要)
路线图对本子计划的描述是错的,本 spec 以权威文档为准。
路线图写「定义 4/5 缺失 player stats(如暴击/急速/范围/吸血/减伤)」—— 这些属性名是拟稿时的推测,不存在于任何权威文档。实际的权威属性表是 docs/design/numerical_design.md §1.1,内容完全不同(11 个属性:hp_max / mana_max / move_speed / cast_delay_mod / recharge_speed_mod / luck / cpu_limit / attunement_×4)。
11 个属性 + 加成层 + 数据文件 + 设计器页,对单个 spec 过大。本期只做「属性框架 + 修复既有死线」,即那些「消费方已存在、只是没接上」的属性。明确排除:
attunement_fire/ice/lightning/poison(4 个元素精通)—— 新增玩法维度,需接入元素伤害管线,独立立项。luck—— 依赖暴击链,而暴击链目前是死的(见 §1.3),独立立项。recharge_speed_mod—— 代码中不存在充能系统(全项目 greprecharge零命中),属新增机制而非断线,独立立项。mana_max迁入框架 —— 见 §2.2 的理由。
1. 背景与现状(代码复核 2026-07-31)
1.1 三条死数据
cpu_limit双份死数据:PlayerStats.cpu_limit(player_stats.gd:21,进存档)与CoreDefinition.cpu_limit(core_definition.gd:16←cores.json,逐法杖 3/5/6/8,设计器可改)两者都不被读取。spell_evaluator.gd:332写死var max_ops: int = MAX_OPS_PER_CPU * 5。权威文档 §1.1 自己已标注此 bug(「代码硬编码 MAX_OPS = 40×5 = 200,从不读 cpu_limit」)。后果:六个法杖各自配置过的运算力完全无效,法杖间少了一个区分维度,且违反「纯数据驱动」。move_speed不是属性:player_manager.gd:7的const MOVE_SPEED: float = 200.0,硬编码常量。cast_delay_mod不存在:player_manager.gd:88直接取core.cast_interval,无任何全局修正系数。
1.2 孤儿字段
PlayerStats.armor(:22)与 PlayerStats.resistance(:23)零消费方(全项目 grep PlayerStats.armor / PlayerStats.resistance 无命中),且不在权威属性表 §1.1 内。take_damage(:65)只乘难度系数,两者从未参与伤害计算。
它们不是「断线」而是实现先于设计的残留:从来没有被设计过。
1.3 暴击链是死的(影响 luck 的排除决定)
CastStats.crit_chance(cast_stats.gd:13)只在 CIRCUIT 分支快照中被读写,从未被掷判;bullet_manager.gd:81 调 DamageContextPool.acquire(b_dmg, b_mult, b_type, b_own, false, 0.0) 把 is_crit 硬编码为 false。故权威 §1.1 中「luck 影响暴击率」这条设计,落地前必须先打通暴击链 —— 属独立工作。
1.4 可复用的既有模式
PlayerStats.mana_leech 是本项目已验证的「多来源属性重算」模式:combat_manager._rebuild_wand 在换杖/换牌时按 core.base_mana_leech + Σ(deck node meta.mana_leech) 重算,读取端是裸字段。本设计的加成层复刻此模式。
2. 设计
2.1 加成模型 —— 公式而非叠加
核心约束(用户 2026-07-31 指定):任何加成都必须有明确的合成公式,不做简单叠加。这与权威 §0 的核心公式 构建强度 = (基础数值 × 修正系数) ^ 逻辑复杂度 一致 —— 修正是乘算系数。
一条加成是 {attr_id: String, mode: String, value: float, source: String},mode 仅两种:flat(平坦加减)、pct(百分比)。
每个属性在 attributes.json 声明自己的 combine:
combine |
公式 | 用于 |
|---|---|---|
hybrid(默认) |
(base + Σflat) × Π(1 + pct) |
move_speed、hp_max |
inverse |
(base + Σflat) × Π(1 − pct),下限钳制到 hard |
cast_delay_mod(越低越快) |
add_int |
base + Σflat,取整;拒绝 pct |
cpu_limit(离散指令预算) |
乘算用连乘 Π 而非线性求和 Σ,这是刻意的:三个「+20% 移速」在连乘下是 ×1.728,线性求和则是 ×1.6。连乘永远逼近而不会突然爆炸,也避免「+50% × 2 = +100%」这类线性叠加导致的后期失控。
inverse 的 pct 取 (1 − pct):「施法延迟 −20%」得 ×0.8,符合直觉;连乘保证永远碰不到 0,再由 hard 兜底。
add_int 拒绝 pct 并 push_error:对离散指令预算做乘算无意义(「1.5 个 CPU」不存在),静默取整会掩盖配置错误。
2.2 本期纳入的属性
| 属性 | base | soft | hard | combine | 处理 |
|---|---|---|---|---|---|
cpu_limit |
0 | 20 | 50 | add_int |
修死数据;生效 MAX_OPS 用 玩家 + 法杖 |
move_speed |
200 | 600 | 800 | hybrid |
由 const 转为属性 |
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 正是纯加成语义。
法杖的 cpu_limit 作为一条加成来源接入,而非在调用点相加(自审修正):
# combat_manager._rebuild_wand(与既有 mana_leech 重算同处、同时机)
PlayerStats.remove_modifiers_from("core")
PlayerStats.add_modifier("cpu_limit", "flat", float(core.cpu_limit), "core")
理由是钳制位置。若在 spell_evaluator 调用点写 PlayerStats.cpu_limit + core.cpu_limit,则 hard(50) 只钳制到玩家那一份,法杖的 3–8 加在钳制之后,总值可达 58 —— 硬上限失效。走加成来源则 hard 天然作用于生效总值,spell_evaluator 只需读 PlayerStats.cpu_limit 一个数。
这还有两个附带好处:① _modifiers 本期不再是空的,加成系统从第一天起就有真实消费者,而非「有机制无消费者」;② 验收第 11 条(remove_modifiers_from)得以对真实来源而非合成数据验证。换杖时机与 mana_leech 完全一致,无需新增钩子。
move_speed 基准取 200(权威表写 300),并订正权威表:权威的 300 从未被任何代码读取过,是纸面孤值;而 200 自 S0 沿用至今,20 波内容、Boss 弹幕密度、无敌帧 0.5 s 均按此值调校。此处既成事实的信息量大于纸面设计 —— 改成 300 等于用未经验证的数字推翻一整轮已调校的内容。
与归航那次的对照(重要,避免误用「以权威为准」):
numerical_design.md的 Mana 列与每一个已实现法术吻合,证明该列是活的、被遵守的,故归航定价越权是真错误。而move_speed300 从未被读取,是同一文档中未被实践检验的部分。「以权威为准」不是无条件的 —— 要看那条权威有没有被实践检验过。
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:
_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.0667,见下) | |
| 0.01(原 hard) | 0.0025 s | 1 帧 | 60 /s |
四条结论
-
饱和点 =
(1/60) / 法杖基准间隔。法杖 基准间隔 饱和点 来源 wand_basic0.5 s ≈ 0.0333 实测(0.034 仍 2 帧、0.033 已 1 帧) wand_fast0.25 s ≈ 0.0667(= 1/15) 实测夹在 (0.05, 0.0667] 内:0.0667 仍 2 帧、0.05 已 1 帧 circuit_fork0.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 记,误差方向是保守的(真实无效区间只会更大一点)。 -
soft→hard 之间的可达档位是台阶而非曲线:
wand_basic只有 20 / 30 / 60 次每秒三档,wand_fast只有 30 / 60 两档。中间值买不到。 -
周期恒为
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 次/秒。 -
综上,原
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。
关键前提:饱和点 = (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.01(6.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 时立刻脱同步,
违反 CLAUDE.md「纯数据驱动,代码不含硬编码副本或回退」。
改动后的边界复测(hard = 0.05 落地后,同一套扫描)
| 法杖 | 请求 mod |
生效 mod |
生效间隔 T | 实测帧距 | 实际速率 | 判读 |
|---|---|---|---|---|---|---|
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 | ✅ 钳制生效 |
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)在新hard0.05 处实测为 2 帧/次(30 /s) —— 它的饱和点 0.0278(公式推导)低于 0.05,故没有无效区间,但也吃不满 60 /s;wand_basic同理(0.0333 < 0.05,30 /s)。反过来wand_fast(0.0667 > 0.05)能吃满 60 /s,代价是[0.05, 0.0667)那段无效区间。这正是"单一全局
hard撞上逐杖饱和点"的必然取舍:hard偏小则慢杖有无效区间,偏大则 快杖被截断。0.05 选择了让无效区间只留在一把杖上,因为无效区间是"花了钱没效果"(欺骗), 而吃不满硬顶只是"上限没那么高"(诚实)。若日后要彻底消除,正确做法是让hard逐杖化 (即把它变成cores.json的一个字段),而不是继续挪这个全局数 —— 那属于新的设计立项。
2.3 公式模块 —— AttributeFormula
所有加成公式集中在一个专门模块,与状态持有分离。
scripts/domain/attribute_formula.gd:
## AttributeFormula — 属性合成公式的唯一实现处
## 纯静态函数,无状态、不依赖 PlayerStats / EventBus / 场景树
class_name AttributeFormula
extends RefCounted
enum Combine { HYBRID, INVERSE, ADD_INT }
## 唯一的公式入口:base + 加成列表 + 属性定义 → 生效值
static func compute(base: float, mods: Array, attr_def: Dictionary) -> float
## combine 字符串(JSON)→ 枚举;未知值 push_error 并回退 HYBRID
static func combine_from_string(s: String) -> Combine
设计要点:
- 纯函数、零依赖 —— 因此不需要启动游戏就能测。公式正确性(连乘 vs 线性、
inverse方向、add_int拒绝pct、hard两个方向的钳制)全部是execute_editor_script里的静态函数断言,比在运行中的战斗场景构造玩家状态更快、更确定,且改公式时能立刻回归。 - 公式的唯一实现处 —— 改公式只需读这一个文件,不必翻还管着 HP/金币/经验/魔力/无敌帧的
PlayerStats。 class_name注册的RefCounted值对象,与cast_stats.gd同类,放同一层级风格的scripts/domain/。
实现注意:
class_name脚本会触发validate_script的既有假阴性(报hides a global script class)—— 工具问题非代码问题。验证用既有绕法:GDScript.new()+ 从磁盘读source_code+reload()。
2.4 PlayerStats 结构
PlayerStats 瘦回单一职责 —— 只负责加载数据、持有加成列表、把公式结果写进类型化字段,一个公式都不含。
# 生效值:静态类型裸字段,读取端零开销(move_speed 每物理帧被 player_manager 读)
var move_speed: float = 200.0
var cast_delay_mod: float = 1.0
var cpu_limit: int = 0
var _attr_def: Dictionary = {} # attributes.json 全量定义,只读
var _modifiers: Array[Dictionary] = [] # 加成来源;本期恒空
func add_modifier(attr_id: String, mode: String, value: float, source: String) -> void
func remove_modifiers_from(source: String) -> void
func _recompute_attrs() -> void # 逐属性调 AttributeFormula.compute,写回裸字段
compute 统一返回 float;add_int 属性在写回时由 PlayerStats 做 int() 转换(cpu_limit = int(AttributeFormula.compute(...)))。公式模块不感知目标字段的静态类型,保持纯粹。
读取端永远是裸字段访问 —— 零开销、静态类型,符合 CLAUDE.md 的热路径纪律(move_speed 每物理帧被读,字典查找不可接受)。写入端集中在 _recompute_attrs()。
remove_modifiers_from 本期就加:子计划 ④「出售退款」必然需要按来源撤销加成,届时不加就要改签名或加并行结构。
本期已有的真实加成来源:法杖的
cpu_limit以source = "core"接入(§2.2),故_modifiers从第一天起就非空,加成层不是「有机制无消费者」。本期确实无消费方的部分:
soft字段(钳制只用hard)。保留理由是它属于权威表已写死的设计数据,不存反而丢失信息,且子计划 ②「货架 C」要靠它区分「常规来源可达」与「需特殊来源突破」。pct模式本期也无来源使用("core"用的是flat),但公式与其单元断言一并落地 —— 否则货架 C 落地时才第一次验证乘算,风险后置。
2.5 三处接线
| 位置 | 改动 |
|---|---|
spell_evaluator.gd:332 |
MAX_OPS_PER_CPU * 5 → MAX_OPS_PER_CPU * PlayerStats.cpu_limit(法杖份额已由 §2.2 的 "core" 加成来源计入) |
combat_manager._rebuild_wand |
换杖/换牌时 remove_modifiers_from("core") + add_modifier("cpu_limit", "flat", core.cpu_limit, "core"),与既有 mana_leech 重算同处 |
player_manager.gd:7,45 |
删 const MOVE_SPEED,改读 PlayerStats.move_speed |
player_manager.gd:88 |
_cast_interval = core.cast_interval → × PlayerStats.cast_delay_mod |
2.6 删除孤儿字段
删 PlayerStats.armor(:22)与 PlayerStats.resistance(:23)。
理由:零消费方、不在权威属性表内 —— 留着会误导(下一个人看到 var resistance: float = 0.0 # 0.0~1.0 会以为玩家抗性已设计好只差接线)。
真要做玩家侧抗性/护甲时怎么立项:它是货架 C 属性词条的好候选(子计划 ②)—— 届时有明确消费场景(花钱买减伤),连同权威 §1.1 表一起补充定义,而非像现在这样悬空。敌人侧已有 armor(
enemy_manager)与 per-element resistance(E1-① 已上线),玩家侧做成镜像是自洽的,但那是扩充权威属性表的设计决策,需走正常立项流程。
2.7 数据与设计器
data/attributes.json:
{
"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" },
"hp_max": { "display_name": "最大生命 hp_max", "base": 100.0, "soft": 2000.0, "hard": 0.0, "combine": "hybrid" }
}
hard: 0.0 表示无硬上限(hybrid 下 0 视为不钳制;inverse 的 hard 是下限,不适用此约定 —— 见 §2.1)。
设计器新增「属性」页(addons/game_designer/attribute_tab.gd):每属性一行,base/soft/hard 用 UI.spin、combine 用 UI.opt(存 key 不存显示串),标签「中文 English」双语序,经 designer_ui.gd 工厂创建。按 CLAUDE.md「新增分页 3 步」注册。
3. 影响文件
| 文件 | 改动 |
|---|---|
scripts/domain/attribute_formula.gd |
新增 —— 公式模块 |
scripts/autoloads/player_stats.gd |
属性字段 + 加成列表 + _recompute_attrs;删 armor/resistance |
scripts/domain/spell_system/spell_evaluator.gd |
:332 MAX_OPS 接线 |
scripts/domain/player_manager.gd |
删 const MOVE_SPEED;移速与施法间隔接线 |
data/attributes.json |
新增 |
addons/game_designer/attribute_tab.gd |
新增 设计器页 |
addons/game_designer/designer_panel.gd |
注册新分页 |
docs/design/numerical_design.md |
§1.1 move_speed 基准 300 → 200 并注明理由 |
4. 验收标准
公式类(纯函数单元断言,无需启动游戏):
hybrid连乘:三条+20% pct→ ×1.728(非 ×1.6,证明是连乘而非线性求和)。hybrid混合:base 200 + flat 50再×(1+0.2)→ 300。inverse方向:base 1.0+ 三条−20% pct→ 0.512;再多条也不小于hard(0.01)。add_int拒绝pct:传入pct加成时push_error且该条被忽略,flat仍正确累加。hard两个方向:hybrid上限钳制、inverse下限钳制;hard: 0.0在hybrid下表示不钳制。- 边界:空加成列表 → 返回 base;未知
combine字符串 →push_error并回退hybrid。
接线类(运行时实测):
cores.json的cpu_limit生效:矩阵板(8) → MAX_OPS 320、高速法杖(3) → 120(改前恒为 200)。- 现有平衡零改动:小木法杖(5) 仍为 MAX_OPS 200。
- 换杖时加成来源正确轮换:小木(5) → 矩阵板(8) 后
PlayerStats.cpu_limit为 8 而非 13(证明remove_modifiers_from("core")先于add_modifier生效,旧法杖份额未累积)。 move_speed数据驱动:改attributes.json后玩家实际移速跟随变化。cast_delay_mod = 0.5→ 实际_cast_interval减半。remove_modifiers_from(source)精确撤销该来源的全部加成且重算正确 —— 混入一条他源加成,撤销"core"后他源仍在。
清理与规范:
armor/resistance全项目零残留(含存档to_dict/from_dict)。- 设计器「属性」页对真实
data/attributes.json回读往返一致;combine下拉存 key 不存显示串。 - 解析通过(
class_name脚本用GDScript.reload()绕法验证);纯数据驱动,文件缺失时push_error明确报错不静默回退。
5. 非目标(YAGNI)
attunement_×4/luck/recharge_speed_mod—— 见 §0,各自独立立项。mana_max的「与法杖取 Min」语义 —— 见 §2.2,独立设计判断。- 打通暴击链 —— 见 §1.3,
luck的前置。 - 货架 C(属性购买 UI 与经济)—— 子计划 ②;本期只提供
add_modifier接入点。 - 升级时自动加属性 —— 本期不引入任何加成来源。
- 玩家侧 armor/resistance —— 见 §2.6,删除并记录立项路径。
- 存档迁移 ——
armor/resistance从未被写入过有意义的值,旧存档读取时忽略即可,不需要 schema 版本号提升。