Files
spellforge/scripts/domain/attribute_formula.gd
T
joywayerandClaude Opus 5 9e3cbf8e5a fix(attr): pct 越界因子变负会静默给出错数,钳到 0 并 push_error
因子 (1±v) 可为负;单条负因子被末尾 maxf(0.0,…) 掩盖,两条负因子相乘
则变回正数 —— 得到一个无报错、看起来合理、实则错误的值。实测 hybrid
base 200 两条 pct=-1.5 得 50.0(应 0.0);inverse 两条 pct=2.0 得 1.0,
延迟纹丝不动(应钳到 hard)。这条路径不需要畸形 JSON,货架 C 传个越界
value 即可触发,而本文件是所有未来平衡的必经之地。

顺带:hard 的语义因 combine 而异(inverse 是下限、0.0 不表示不钳制),
从行尾注释提升进 compute() 文档块并在 inverse 分支点明与 hybrid 相反;
补 mods 的构造契约说明;两处下界 0 加注释;ri→floored、
_COMBINE_NAMES→_COMBINE_BY_NAME;查表与循环变量补静态类型。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 11:02:44 +08:00

68 lines
3.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## AttributeFormula — 属性合成公式的唯一实现处
## 纯静态函数:无状态、不依赖 PlayerStats / EventBus / 场景树,故可脱离游戏进程单元测试
## 权威来源:docs_dev/specs/2026-07-31-player-attributes-design.md §2.1 / §2.3
class_name AttributeFormula
extends RefCounted
enum Combine { HYBRID, INVERSE, ADD_INT }
const _COMBINE_BY_NAME: Dictionary[String, Combine] = {
"hybrid": Combine.HYBRID,
"inverse": Combine.INVERSE,
"add_int": Combine.ADD_INT,
}
## JSON 的 combine 字符串 → 枚举;未知值 push_error 并回退 HYBRID
static func combine_from_string(s: String) -> Combine:
if _COMBINE_BY_NAME.has(s):
return _COMBINE_BY_NAME[s]
push_error("AttributeFormula: 未知 combine「%s」,回退 hybrid" % s)
return Combine.HYBRID
## 唯一的公式入口
## base —— attributes.json 的基准值
## mods —— [{"mode": "flat"|"pct", "value": float}, ...];调用方负责只传本属性的加成
## 契约:仅由 PlayerStats.add_modifier 构造,故假定 mode/value 键必存在且类型正确;
## 此处不做防御性校验(未知 mode 会 push_error,但缺键会被静默当默认值)
## attr_def —— attributes.json 中该属性的定义(读 combine / hard
## hard 的语义**因 combine 而异**,勿混淆:
## · hybrid —— hard 是上限;`hard == 0.0` 约定为「不钳制」(如 hp_max 无硬上限)
## · add_int —— 同上,hard 是上限,`0.0` 即不钳制
## · inverse —— hard 是**下限**;此处 `0.0` **不是**「不钳制」而是下限 0,与上二者相反
## 返回统一为 floatadd_int 属性由调用方做 int() 转换(公式模块不感知目标字段类型)
static func compute(base: float, mods: Array, attr_def: Dictionary) -> float:
var combine: Combine = combine_from_string(String(attr_def.get("combine", "hybrid")))
var hard: float = float(attr_def.get("hard", 0.0))
var flat_sum: float = 0.0
var pct_prod: float = 1.0
for m: Dictionary in mods:
var mode: String = String(m.get("mode", "flat"))
var v: float = float(m.get("value", 0.0))
if mode == "flat":
flat_sum += v
elif mode == "pct":
if combine == Combine.ADD_INT:
push_error("AttributeFormula: add_int 属性不接受 pct 加成(value=%f),已忽略" % v)
continue
# 连乘而非线性求和:三条 +20% = ×1.728 而非 ×1.6,避免后期线性失控
var f: float = (1.0 + v) if combine == Combine.HYBRID else (1.0 - v)
if f < 0.0:
# 越界的 pct 会使因子变负;两条负因子相乘会变回正数,产生无报错的错数
push_error("AttributeFormula: pct 越界(value=%f → 因子 %f),已钳到 0" % [v, f])
f = 0.0
pct_prod *= f
else:
push_error("AttributeFormula: 未知 mode「%s」,已忽略" % mode)
match combine:
Combine.ADD_INT:
var floored: float = floorf(base + flat_sum)
# 下界 0:生效值不允许为负(负的运算力无意义)
return clampf(floored, 0.0, hard) if hard > 0.0 else maxf(0.0, floored)
Combine.INVERSE:
# 越低越快:hard 是**下限**。注意 hard=0.0 在此处不是「不钳制」而是下限 0,与 hybrid 相反
return maxf(hard, (base + flat_sum) * pct_prod)
_:
# 下界 0:生效值不允许为负(负的移速无意义)
var r: float = maxf(0.0, (base + flat_sum) * pct_prod)
return minf(r, hard) if hard > 0.0 else r # hard=0.0 约定为「不钳制」