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>
This commit is contained in:
2026-07-31 11:02:44 +08:00
co-authored by Claude Opus 5
parent cd3618c066
commit 9e3cbf8e5a
+21 -8
View File
@@ -6,7 +6,7 @@ extends RefCounted
enum Combine { HYBRID, INVERSE, ADD_INT } enum Combine { HYBRID, INVERSE, ADD_INT }
const _COMBINE_NAMES: Dictionary = { const _COMBINE_BY_NAME: Dictionary[String, Combine] = {
"hybrid": Combine.HYBRID, "hybrid": Combine.HYBRID,
"inverse": Combine.INVERSE, "inverse": Combine.INVERSE,
"add_int": Combine.ADD_INT, "add_int": Combine.ADD_INT,
@@ -14,22 +14,28 @@ const _COMBINE_NAMES: Dictionary = {
## JSON 的 combine 字符串 → 枚举;未知值 push_error 并回退 HYBRID ## JSON 的 combine 字符串 → 枚举;未知值 push_error 并回退 HYBRID
static func combine_from_string(s: String) -> Combine: static func combine_from_string(s: String) -> Combine:
if _COMBINE_NAMES.has(s): if _COMBINE_BY_NAME.has(s):
return _COMBINE_NAMES[s] return _COMBINE_BY_NAME[s]
push_error("AttributeFormula: 未知 combine「%s」,回退 hybrid" % s) push_error("AttributeFormula: 未知 combine「%s」,回退 hybrid" % s)
return Combine.HYBRID return Combine.HYBRID
## 唯一的公式入口 ## 唯一的公式入口
## base —— attributes.json 的基准值 ## base —— attributes.json 的基准值
## mods —— [{"mode": "flat"|"pct", "value": float}, ...];调用方负责只传本属性的加成 ## mods —— [{"mode": "flat"|"pct", "value": float}, ...];调用方负责只传本属性的加成
## 契约:仅由 PlayerStats.add_modifier 构造,故假定 mode/value 键必存在且类型正确;
## 此处不做防御性校验(未知 mode 会 push_error,但缺键会被静默当默认值)
## attr_def —— attributes.json 中该属性的定义(读 combine / hard ## 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() 转换(公式模块不感知目标字段类型) ## 返回统一为 floatadd_int 属性由调用方做 int() 转换(公式模块不感知目标字段类型)
static func compute(base: float, mods: Array, attr_def: Dictionary) -> float: static func compute(base: float, mods: Array, attr_def: Dictionary) -> float:
var combine: Combine = combine_from_string(String(attr_def.get("combine", "hybrid"))) var combine: Combine = combine_from_string(String(attr_def.get("combine", "hybrid")))
var hard: float = float(attr_def.get("hard", 0.0)) var hard: float = float(attr_def.get("hard", 0.0))
var flat_sum: float = 0.0 var flat_sum: float = 0.0
var pct_prod: float = 1.0 var pct_prod: float = 1.0
for m in mods: for m: Dictionary in mods:
var mode: String = String(m.get("mode", "flat")) var mode: String = String(m.get("mode", "flat"))
var v: float = float(m.get("value", 0.0)) var v: float = float(m.get("value", 0.0))
if mode == "flat": if mode == "flat":
@@ -39,16 +45,23 @@ static func compute(base: float, mods: Array, attr_def: Dictionary) -> float:
push_error("AttributeFormula: add_int 属性不接受 pct 加成(value=%f),已忽略" % v) push_error("AttributeFormula: add_int 属性不接受 pct 加成(value=%f),已忽略" % v)
continue continue
# 连乘而非线性求和:三条 +20% = ×1.728 而非 ×1.6,避免后期线性失控 # 连乘而非线性求和:三条 +20% = ×1.728 而非 ×1.6,避免后期线性失控
pct_prod *= (1.0 + v) if combine == Combine.HYBRID else (1.0 - v) 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: else:
push_error("AttributeFormula: 未知 mode「%s」,已忽略" % mode) push_error("AttributeFormula: 未知 mode「%s」,已忽略" % mode)
match combine: match combine:
Combine.ADD_INT: Combine.ADD_INT:
var ri: float = floorf(base + flat_sum) var floored: float = floorf(base + flat_sum)
return clampf(ri, 0.0, hard) if hard > 0.0 else maxf(0.0, ri) # 下界 0:生效值不允许为负(负的运算力无意义)
return clampf(floored, 0.0, hard) if hard > 0.0 else maxf(0.0, floored)
Combine.INVERSE: Combine.INVERSE:
# 越低越快:hard 是**下限** # 越低越快:hard 是**下限**。注意 hard=0.0 在此处不是「不钳制」而是下限 0,与 hybrid 相反
return maxf(hard, (base + flat_sum) * pct_prod) return maxf(hard, (base + flat_sum) * pct_prod)
_: _:
# 下界 0:生效值不允许为负(负的移速无意义)
var r: float = maxf(0.0, (base + flat_sum) * pct_prod) var r: float = maxf(0.0, (base + flat_sum) * pct_prod)
return minf(r, hard) if hard > 0.0 else r # hard=0.0 约定为「不钳制」 return minf(r, hard) if hard > 0.0 else r # hard=0.0 约定为「不钳制」