## 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 约定为「不钳制」
