geometric 曲线在 purchased 较大时(如 n=300,base=60 growth=1.15)raw 虽仍是合法有限 double(约 9.7e19),但已超出 int64 安全范围,roundi() 对此行为未定义/环绕,经 maxi(...,1) 静默塌陷成 1——方向与「买得越多越 贵」相反,且零诊断。仅判断 is_finite(raw) 测不出这种情况(double 本身 溢出为 INF 要到 n≈5077 才发生,晚于 int64 溢出很多),故改为 `not is_finite(raw) or raw > 9.0e15` 双重判据,触发时 push_error 并钳 到统一上限,不再依赖具体常数断言(新增用例只断言单调性与「不再塌陷回 归」)。 同时补齐 docs_dev/plans/2026-07-31-shelf-c-attribute-shop.md:64 遗漏的 `PriceFormula.Curve` → `PriceFormula.PriceCurve` 同步(enum 部分先前已 改,返回类型标注漏改),并全仓复核确认无其它残留。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
56 lines
2.9 KiB
GDScript
56 lines
2.9 KiB
GDScript
## PriceFormula — 价格计算的唯一实现处
|
||
## 纯静态函数:无状态、不依赖 ShopManager / PlayerStats / 场景树,故可脱离游戏进程单元测试
|
||
## 本模块**不认识**「属性/武器/装备」,只认识 {price_base, price_growth, curve} ——
|
||
## 划分点在数据里,故货架 B(核心抽取)与子计划 ④(出售退款)可直接复用。
|
||
## 权威来源:docs_dev/specs/2026-07-31-shelf-c-attribute-shop-design.md §2.1
|
||
class_name PriceFormula
|
||
extends RefCounted
|
||
|
||
## 命名为 PriceCurve 而非 Curve:Godot 引擎自带全局类 `Curve`(曲线资源),
|
||
## 嵌套枚举若同名会被解析器拒绝("member Curve shadows a native class"),
|
||
## 与 class_name 是否注册无关,故不能叫 Curve。此为本任务对简报字面代码的
|
||
## 唯一必要偏离,详见 task-1-report.md。
|
||
enum PriceCurve { GEOMETRIC, LINEAR, FLAT }
|
||
|
||
const _CURVE_BY_NAME: Dictionary[String, PriceCurve] = {
|
||
"geometric": PriceCurve.GEOMETRIC,
|
||
"linear": PriceCurve.LINEAR,
|
||
"flat": PriceCurve.FLAT,
|
||
}
|
||
|
||
## JSON 的 curve 字符串 → 枚举;未知值 push_error 并回退 GEOMETRIC
|
||
static func curve_from_string(s: String) -> PriceCurve:
|
||
if _CURVE_BY_NAME.has(s):
|
||
return _CURVE_BY_NAME[s]
|
||
push_error("PriceFormula: 未知 curve「%s」,回退 geometric" % s)
|
||
return PriceCurve.GEOMETRIC
|
||
|
||
## 唯一的价格入口
|
||
## spec —— 数据文件里的定价段,读 price_base / price_growth / curve
|
||
## purchased —— 已购次数(0 = 首次购买)
|
||
## 返回 int(金币是整数),下限 1:免费购买无意义,且 0 价会让「买不起」的判定失效
|
||
static func compute(spec: Dictionary, purchased: int) -> int:
|
||
var base: float = float(spec.get("price_base", 0.0))
|
||
var growth: float = float(spec.get("price_growth", 1.0))
|
||
var n: int = maxi(purchased, 0)
|
||
var curve: PriceCurve = curve_from_string(String(spec.get("curve", "geometric")))
|
||
var raw: float = 0.0
|
||
match curve:
|
||
PriceCurve.LINEAR:
|
||
raw = base + float(n) * growth
|
||
PriceCurve.FLAT:
|
||
raw = base
|
||
_:
|
||
raw = base * pow(growth, float(n))
|
||
# raw 可能在两个不同的地方失控:pow() 把 double 本身推到 INF(n 极大,如 geometric
|
||
# n≈5077+),或者 raw 仍是合法有限 double,但早已超出 int64 可安全表示的范围(如
|
||
# geometric n=300 时 raw≈9.7e19——finite 但 > INT64_MAX≈9.22e18)。后者 is_finite()
|
||
# 测不出来,roundi() 对超范围 float 的行为是未定义/环绕,曾亲测把它环绕成极小/负值,
|
||
# 再经 maxi(...,1) 静默塌陷成 1——方向与"买得越多越贵"完全相反。故用同一个安全阈值
|
||
# 9.0e15(远小于 INT64_MAX,远超任何合理金币量)同时兼答两种情况。
|
||
const _PRICE_CLAMP: float = 9.0e15
|
||
if not is_finite(raw) or raw > _PRICE_CLAMP:
|
||
push_error("PriceFormula: 价格溢出(base=%f growth=%f purchased=%d),已钳到上限" % [base, growth, n])
|
||
raw = _PRICE_CLAMP
|
||
return maxi(roundi(raw), 1)
|