## 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,与上二者相反 ## 返回统一为 float;add_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 约定为「不钳制」