Files
spellforge/scripts/domain/price_formula.gd
T
joywayerandClaude Opus 5 6aeadb84b8 fix(shop): PriceFormula 大购买次数溢出静默塌陷为 1;同步计划文档残留类型名
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>
2026-08-03 11:05:51 +08:00

56 lines
2.9 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.
## 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 而非 CurveGodot 引擎自带全局类 `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 本身推到 INFn 极大,如 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)