W4 批次:三项设计裁定落地(Boss 战 Plan B / 离线契约 v2 / 长明灯加法化)
Boss 战修复(Plan B,用户拍板): - HP 公式 15xPxhall_factor -> HP = P x boss_army_seconds_n(S=23.0→28.0 递增, 每场军列约 23-28 秒清空,战斗时长成为可直接书写的设计旋钮,结构性消除 高殿 P 抵消导致不可胜的旧 bug);hall_factor 降级为纯叙事/视觉参数 - 新增 souls_par 列(0→90000):开场冲锋 intro_dmg = HP x 10% x clamp(souls/souls_par,0,1), 上限 20%->10%;删 hp_factor/intro_dmg_souls_factor/intro_dmg_cap_ratio 三列 - 点击伤害 P x κ x combo_mult,κ = (0.010 + 0.052 x click_ratio) x (1 + 0.30 x m06_owned), click_ratio = 已购点击升级 / 12(κ 0.010→0.0806,满配点击占比约 20.5%±3pp) - 防双重计入:L5-12 点击升级 +1% output/s 留全局区(抬 P 同时抬军列 DPS 与点击 伤害,可接受),不得再计入 κ 分子(recompute 注释固化) 离线契约 v2(P-8,改契约不改表): - OfflineModel.settle 第三参 offline_mult -> offline_add(加法);各档效率 min(rate+add, 1.0) 硬顶 100%,离线永不超在线 - 长明灯 x1.15 乘性 -> +5%/级 加法(满级 +30%,各档封顶 100%) - 0-2h 100% 保持;8h = 6.2 等效小时(77.5% 效率) - Gate #9 拆 9a/9b/9c:9a FAIL->PASS(0-2h 全额 + 8h∈[5.5,6.5]), 9b 长明灯满级 8h∈[7.5,8.0] 且 ≤100%,9c 任意配置效率 ≤100% 数据与工具: - gen_balance_sheets.py 补登 W3 遗留 7 行 tune(防重跑静默丢失),self_check 更新 - boss_battles/upgrades schema 同步(offline_mult->offline_add enum,漏改会违反导入) 测试(runner 7->8 用例,Node 交叉验证 145 断言全过): - 可胜性哨兵重写为「十殿全可胜」:每殿 S_n<30、军列+点击≥HP、余时∈[2,8]、 满配余时差≥4、点击占比∈[15,25]、H10≈20.5%±3pp - κ 四档断言(0.010/0.036/0.062/0.0806,分母 12) - 新增 test_offline_contract.gd:契约 v2 数值面 + 9a/9b/9c 全 PASS + β 缓议
This commit is contained in:
1 parent
4f650aeb39
commit
08dc5020c3
22 files changed
+738
-233
No files matched your search
@@ -19,7 +19,12 @@ func run(a, data) -> void:
|
||||
a.near(data.hall_by_no(1).gate_merit, 2000.0, 0.5, "H1 门 2K")
|
||||
a.near(data.hall_by_no(10).gate_merit, 1.5e9, 0.5, "H10 门 1.5B")
|
||||
a.true_(data.hall_by_no(10).is_prestige_gate, "H10 = prestige 门")
|
||||
a.near(data.boss_for_hall("hall_01_qinguang").hp_factor, 15.0, 1e-9, "殿王 HP 系数 15([B§5])")
|
||||
a.near(data.boss_for_hall("hall_01_qinguang").boss_army_seconds, 23.0, 1e-9,
|
||||
"H1 殿王军列单推秒数 S_1 = 23(方案 B;旧 HP 系数 15 已废止)")
|
||||
a.near(data.boss_for_hall("hall_10_zhuanlun").boss_army_seconds, 28.0, 1e-9,
|
||||
"H10 殿王军列单推秒数 S_10 = 28")
|
||||
a.eq(data.boss_for_hall("hall_01_qinguang").souls_par, 0, "H1 超度基准 0(无开场加成)")
|
||||
a.eq(data.boss_for_hall("hall_10_zhuanlun").souls_par, 90000, "H10 超度基准 90000")
|
||||
# 敕封诏书(P-12:requires = enthrone_ready)
|
||||
var edict := data.upgrade("up_gate_tianzi")
|
||||
a.true_(edict != null, "敕封诏书存在")
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
extends RefCounted
|
||||
|
||||
# 用例:W3 殿王战逻辑状态机(game/BossBattle;boss_battles.csv 十场)
|
||||
# 口径全部来自数据行([B§5]):HP = 15 × 进入时产出/s × 殿系数(1.0→2.2)。
|
||||
# 本类不用随机 → 结果可复现(W4 起 sim 门 #11 直接复用)。
|
||||
# 用例:W4 殿王战逻辑状态机(game/BossBattle;boss_battles.csv 十场;方案 B)
|
||||
# 口径全部来自数据行(systems-spec §1.2,用户 2026-10-20 拍板):
|
||||
# HP = 进入时产出/s × boss_army_seconds(S_n);点击伤害 = 产出/s × κ × combo_mult;
|
||||
# 开场冲锋 = HP × 0.10 × clamp(超度/souls_par, 0, 1)。
|
||||
# 本类不用随机 → 结果可复现(W4 sim 门 #11 直接复用)。
|
||||
#
|
||||
# W4 变更说明:原"可胜性事实记录"哨兵(8 座可胜 / 2 座必超时 / 点击天花板 25.6)
|
||||
# 钉的是旧 HP 公式下的**缺陷事实**;方案 B 落地后按 systems-spec 第七章第 1 条
|
||||
# 重写为"十殿全可胜 + S_n + κ + 修订门 #11 五判据"的**设计意图断言**。
|
||||
|
||||
func run(a, data) -> void:
|
||||
_test_hp_formula(a, data)
|
||||
_test_win(a, data)
|
||||
_test_intro_damage(a, data)
|
||||
_test_combo_cap(a, data)
|
||||
_test_lose_and_retry(a, data)
|
||||
_test_availability(a, data)
|
||||
_test_winnability_doc(a, data)
|
||||
_test_kappa_scaling(a, data)
|
||||
_test_winnability_b(a, data)
|
||||
|
||||
|
||||
func _new_econ(data, merit_run: float, gen0_level: int = 10) -> EconomyState:
|
||||
@@ -21,31 +29,83 @@ func _new_econ(data, merit_run: float, gen0_level: int = 10) -> EconomyState:
|
||||
return e
|
||||
|
||||
|
||||
# ---- HP = 15 × 产出/s × 殿系数([B§5],GDD 旧值 50× 已废止) ----
|
||||
## 满配点击:12 条点击升级全购 + m06 斩鬼刃 1 阶(直接置位 + recompute,同旧用例对
|
||||
## up_gate_boss_retry 的手法;购买合法性由 test_economy_state 覆盖)。
|
||||
func _full_click_config(e: EconomyState, data) -> void:
|
||||
for u in data.upgrades:
|
||||
if u.type == "click":
|
||||
e.bought[u.id] = true
|
||||
e.meta_ranks["meta_06"] = 1
|
||||
e.recompute()
|
||||
|
||||
|
||||
## 战斗模拟器(dt=0.1;click_every = 点击间隔秒,0 = 挂机;keep_alive = 每 tick 前补满 HP,
|
||||
## 防止 dummy 提前死亡,用于测满时长内的军列+点击伤害容量)。返回实测字典。
|
||||
func _run_battle(b: BossBattle, e: EconomyState, max_sec: float,
|
||||
click_every: float = 0.0, keep_alive: bool = false) -> Dictionary:
|
||||
var out := {"win": false, "active_sec": 0.0, "army_dmg": 0.0,
|
||||
"click_dmg": 0.0, "clicks": 0}
|
||||
var dt := 0.1
|
||||
var steps := int(round(max_sec / dt))
|
||||
var click_acc := 0.0
|
||||
for _i in steps:
|
||||
if b.phase == BossBattle.Phase.RESOLVED:
|
||||
break
|
||||
if keep_alive:
|
||||
b.hp = b.hp_max
|
||||
b.tick(dt, e)
|
||||
if b.phase == BossBattle.Phase.ACTIVE:
|
||||
out.army_dmg += e.output_per_sec() * dt
|
||||
out.active_sec += dt
|
||||
if click_every > 0.0:
|
||||
click_acc += dt
|
||||
while click_acc >= click_every - 1e-9:
|
||||
click_acc -= click_every
|
||||
var dmg := b.register_click(e)
|
||||
out.click_dmg += dmg
|
||||
out.clicks += 1
|
||||
if keep_alive and b.phase == BossBattle.Phase.RESOLVED:
|
||||
# 测试手法:点击击杀 dummy 后拉回 ACTIVE 续打(只动 phase/hp,
|
||||
# 不动任何公式;save_soul 的阴德副作用对本断言无影响)。
|
||||
b.phase = BossBattle.Phase.ACTIVE
|
||||
b.win = false
|
||||
b.hp = b.hp_max
|
||||
out.win = bool(b.view().get("win", false))
|
||||
return out
|
||||
|
||||
|
||||
# ---- HP = 产出/s × boss_army_seconds(方案 B1;hall_factor 降级为纯叙事参数) ----
|
||||
func _test_hp_formula(a, data) -> void:
|
||||
var e := _new_econ(data, 2000.0) # 过 H1
|
||||
a.eq(e.halls_passed, 1, "H1 门达成(2000 阴德)")
|
||||
var out := e.output_per_sec()
|
||||
a.near(out, 1.0, 1e-6, "基线产出 1.0/s")
|
||||
var expect_s := [23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 28.0]
|
||||
var expect_h := [1.0, 1.1, 1.2, 1.3, 1.4, 1.6, 1.8, 2.0, 2.1, 2.2]
|
||||
for i in data.halls.size():
|
||||
var h: HallDef = data.halls[i]
|
||||
var def := data.boss_for_hall(h.id)
|
||||
a.true_(def != null, "殿 %s 有 boss 行" % h.id)
|
||||
if def == null:
|
||||
continue
|
||||
a.near(float(def.hp_factor), 15.0, 1e-9, "%s:hp_factor = 15" % h.id)
|
||||
a.near(def.hp_at(out), 15.0 * out * float(def.hall_factor), 1e-6,
|
||||
"%s:HP = 15 × 产出/s × 殿系数" % h.id)
|
||||
# H1 殿系数 1.0 → HP = 15;H10 = 2.2 → 2.2 倍
|
||||
a.near(def.boss_army_seconds, expect_s[i], 1e-9,
|
||||
"%s:S_n = %.1f(军列单推秒数)" % [h.id, expect_s[i]])
|
||||
a.near(def.hp_at(out), expect_s[i] * out, 1e-6,
|
||||
"%s:HP = 产出/s × S_n" % h.id)
|
||||
a.near(float(def.hall_factor), expect_h[i], 1e-9,
|
||||
"%s:hall_factor = %.1f(纯叙事/视觉参数,值不动)" % [h.id, expect_h[i]])
|
||||
a.true_(def.boss_army_seconds < float(def.active_sec),
|
||||
"%s:S_n < active_sec(无必败殿)" % h.id)
|
||||
# H1:产出 1/s → HP 23;H10:产出 1/s → HP 28(旧式 15/33 已废止)
|
||||
var d1 := data.boss_for_hall("hall_01_qinguang")
|
||||
var d10 := data.boss_for_hall("hall_10_zhuanlun")
|
||||
a.near(float(d1.hall_factor), 1.0, 1e-9, "H1 殿系数 1.0")
|
||||
a.near(float(d10.hall_factor), 2.2, 1e-9, "H10 殿系数 2.2")
|
||||
a.near(d1.hp_at(out), 15.0, 1e-6, "H1:产出 1/s → HP 15")
|
||||
a.near(d10.hp_at(out), 33.0, 1e-6, "H10:产出 1/s → HP 33")
|
||||
a.near(d1.hp_at(out), 23.0, 1e-6, "H1:产出 1/s → HP 23")
|
||||
a.near(d10.hp_at(out), 28.0, 1e-6, "H10:产出 1/s → HP 28")
|
||||
# HP 与 hall_factor 无关(叙事参数不参与计算的证明)
|
||||
a.near(d10.hp_at(100.0), 2800.0, 1e-6, "H10:产出 100/s → HP 2800(不含殿系数 2.2)")
|
||||
|
||||
|
||||
# ---- 军列 DPS = 产出/s × 1.0;30s 内打完 → 胜 + 阴德爆发 HP×0.2 ----
|
||||
# ---- 军列 DPS = 产出/s × 1.0;S_n 秒打完 → 胜 + 阴德爆发 HP×0.2 ----
|
||||
func _test_win(a, data) -> void:
|
||||
var e := _new_econ(data, 2000.0)
|
||||
var b := BossBattle.new(data)
|
||||
@@ -54,25 +114,25 @@ func _test_win(a, data) -> void:
|
||||
a.eq(int(info.get("hall_index", -1)), 0, "可挑战的殿 index = 0")
|
||||
var r := b.start(e, 0)
|
||||
a.true_(bool(r.get("ok", false)), "开战")
|
||||
a.near(float(r.get("hp_max", 0.0)), 15.0, 1e-6, "进入时锁定 HP = 15")
|
||||
a.near(float(r.get("hp_max", 0.0)), 23.0, 1e-6, "进入时锁定 HP = 23(P=1/s × S_1=23)")
|
||||
a.eq(b.phase, BossBattle.Phase.INTRO, "先走 INTRO(3s 军列冲锋)")
|
||||
# 超度值为 0 → 开场伤害 0(min(0×0.5, HP×0.2))
|
||||
a.near(b.hp, 15.0, 1e-6, "开场伤害 = 0(超度值 0)")
|
||||
# 超度值为 0 且 H1 souls_par = 0 → 开场伤害 0
|
||||
a.near(b.hp, 23.0, 1e-6, "开场伤害 = 0(超度值 0 / H1 基准 0)")
|
||||
# 走完 INTRO
|
||||
for _i in 31:
|
||||
b.tick(0.1, e)
|
||||
a.eq(b.phase, BossBattle.Phase.ACTIVE, "3s 后进入 ACTIVE")
|
||||
a.near(float(b.view().get("timer", 0.0)), 30.0, 1.0, "ACTIVE 计时 30s 起步")
|
||||
# 军列 DPS = 1.0 → 15s 打完
|
||||
# 军列 DPS = 1.0 → 23s 打完(S_1)
|
||||
var before := e.merit_run_total
|
||||
for _i in 152:
|
||||
for _i in 240:
|
||||
b.tick(0.1, e)
|
||||
if b.phase == BossBattle.Phase.RESOLVED:
|
||||
break
|
||||
a.eq(b.phase, BossBattle.Phase.RESOLVED, "HP 归零 → RESOLVED")
|
||||
a.true_(bool(b.view().get("win", false)), "判定为胜")
|
||||
a.near(float(b.view().get("reward", 0.0)), 15.0 * 0.2, 1e-6, "奖励 = HP × 0.2 = 3")
|
||||
a.near(e.merit_run_total, before + 15.0 * 0.2, 1e-6, "阴德爆发进本轮累计口径")
|
||||
a.near(float(b.view().get("reward", 0.0)), 23.0 * 0.2, 1e-6, "奖励 = HP × 0.2 = 4.6")
|
||||
a.near(e.merit_run_total, before + 23.0 * 0.2, 1e-6, "阴德爆发进本轮累计口径")
|
||||
# RESOLVED(结算面板挂着)与"真在打"要能区分:原因串不许一律是"战斗中"
|
||||
var during: Dictionary = b.available(e)
|
||||
a.false_(bool(during.get("ok", false)), "结算未确认 → 不可开新战")
|
||||
@@ -86,10 +146,40 @@ func _test_win(a, data) -> void:
|
||||
a.true_(bool(b.available(e).get("ok", false)), "新轮回后重新可挑战")
|
||||
|
||||
|
||||
# ---- 连击:+2%/层,封顶 +60% ----
|
||||
# 注意:本用例必须让 HP 大到"100 刀砍不死",否则第 14 刀就把 boss 打死(HP=15×产出/s,
|
||||
# 而点击价值恒为 1.0),战斗提前进 RESOLVED,连击永远涨不到封顶 —— 这是用例设计陷阱,
|
||||
# 不是被测代码的 bug。故这里把军列拉到 200 级(产出 20/s → HP 300 >> 100 刀总伤 ≈151)。
|
||||
# ---- 开场冲锋 v2(方案 B3):HP × 0.10 × clamp(超度/souls_par, 0, 1) ----
|
||||
func _test_intro_damage(a, data) -> void:
|
||||
a.near(data.tune_num("tune_boss_intro_cap", 0.0), 0.10, 1e-9, "intro_cap = 0.10(旧 20% 已废止)")
|
||||
a.near(data.tune_num("tune_boss_intro_enabled", 0.0), 1.0, 1e-9, "intro_enabled = 1(默认启用)")
|
||||
# H5(souls_par = 600):超度 600 → clamp = 1 → 开场伤害 = 10% HP
|
||||
var e := _new_econ(data, 6000000.0) # 超度 = floor(6e6/1e4) = 600
|
||||
a.eq(e.souls_saved(), 600, "超度值 600")
|
||||
var b := BossBattle.new(data)
|
||||
var r := b.start(e, 4) # H5 殿王
|
||||
a.true_(bool(r.get("ok", false)), "H5 开战")
|
||||
a.near(float(r.get("hp_max", 0.0)), 25.0, 1e-6, "H5 HP = 25(P=1/s × S_5=25)")
|
||||
for _i in 31:
|
||||
b.tick(0.1, e)
|
||||
a.eq(b.phase, BossBattle.Phase.ACTIVE, "进入 ACTIVE")
|
||||
a.near(b.hp, 25.0 * 0.9, 1e-6, "开场伤害 = HP × 10% × clamp(600/600) = 2.5")
|
||||
# 超度 300 → clamp = 0.5 → 开场伤害 = 5% HP
|
||||
var e2 := _new_econ(data, 3000000.0)
|
||||
var b2 := BossBattle.new(data)
|
||||
b2.start(e2, 4)
|
||||
for _i in 31:
|
||||
b2.tick(0.1, e2)
|
||||
a.near(b2.hp, 25.0 * 0.95, 1e-6, "开场伤害 = HP × 10% × clamp(300/600) = 1.25")
|
||||
# H1(souls_par = 0)→ 无开场加成(防 0 除),即使超度很大
|
||||
var e3 := _new_econ(data, 6000000.0)
|
||||
var b3 := BossBattle.new(data)
|
||||
b3.start(e3, 0)
|
||||
for _i in 31:
|
||||
b3.tick(0.1, e3)
|
||||
a.near(b3.hp, 23.0, 1e-6, "H1 基准 0 → 开场伤害 0")
|
||||
|
||||
|
||||
# ---- 连击:+2%/层,封顶 +60%;点击伤害 = P × κ × 连击 ----
|
||||
# 用例前提:HP 大到"100 刀砍不死"。κ(零投资) = 0.010,P = 20/s(军列 200 级)
|
||||
# ⇒ HP = 23 × 20 = 460 >> 100 刀总伤 ≈ 20 × 0.010 × ~130 ≈ 26。
|
||||
func _test_combo_cap(a, data) -> void:
|
||||
var e := _new_econ(data, 2000.0, 200)
|
||||
var b := BossBattle.new(data)
|
||||
@@ -97,12 +187,14 @@ func _test_combo_cap(a, data) -> void:
|
||||
for _i in 31:
|
||||
b.tick(0.1, e)
|
||||
a.eq(b.phase, BossBattle.Phase.ACTIVE, "进入 ACTIVE")
|
||||
a.true_(b.hp_max > 100.0 * e.click_value() * 1.6,
|
||||
a.near(b.entry_kappa, 0.010, 1e-9, "零投资 κ = 0.010(tune_boss_kappa_base)")
|
||||
a.true_(b.hp_max > 100.0 * e.output_per_sec() * b.entry_kappa * 1.6,
|
||||
"用例前提:HP 足够撑过 100 刀(HP=%s)" % NumberFormat.short(b.hp_max))
|
||||
a.near(b.combo_mult(), 1.0, 1e-9, "初始连击 ×1.0")
|
||||
var dmg1 := b.register_click(e) # 第 1 刀:1 层 → ×1.02
|
||||
a.eq(b.combo, 1, "连击计数 1")
|
||||
a.near(b.combo_mult(), 1.02, 1e-6, "1 层 → +2%")
|
||||
a.near(dmg1, e.click_value() * 1.02, 1e-6, "伤害 = 点击价值 × 连击")
|
||||
a.near(dmg1, e.output_per_sec() * 0.010 * 1.02, 1e-6, "伤害 = 产出/s × κ × 连击")
|
||||
# 连点 100 下(窗口内)→ 封顶 +60%
|
||||
for _i in 99:
|
||||
b.register_click(e)
|
||||
@@ -173,39 +265,120 @@ func _test_availability(a, data) -> void:
|
||||
a.false_(bool(b2.start(e2, 99).get("ok", false)), "殿序越界被拒")
|
||||
|
||||
|
||||
# ---- 可胜性结构(**事实记录**,不是"应当如此"的断言) ----
|
||||
# 推导(与产出/s 无关,故对任何存档都成立):
|
||||
# 纯军列耗时 = HP ÷ (产出/s × army_dps_factor)
|
||||
# = (hp_factor × 产出/s × hall_factor) ÷ (产出/s × army_dps_factor)
|
||||
# = hp_factor × hall_factor ÷ army_dps_factor ← 产出/s 被约掉了
|
||||
# 于是:殿系数 > active_sec ÷ (hp_factor ÷ army_dps_factor) = 30 ÷ 15 = 2.0 的殿,
|
||||
# 军列单推必定超时;而缺口 ∝ 产出/s,点击伤害却是常数(不随产出/s 增长)
|
||||
# → **玩家越强,缺口越大,越打不过**。这是 W3 交付时点名上报文策渊的平衡缺陷,
|
||||
# 本用例把当前数据的事实钉死:一旦有人改 hall_factor / active_sec / hp_factor,
|
||||
# 这里会立刻变红,提醒重新评估可胜性。修数值的权力在文策渊,不在本文件。
|
||||
func _test_winnability_doc(a, data) -> void:
|
||||
var ratio: float = float(data.bosses[0].hp_factor) / float(data.bosses[0].army_dps_factor)
|
||||
a.near(ratio, 15.0, 1e-9, "hp_factor ÷ army_dps_factor = 15(纯军列耗时的基准秒数)")
|
||||
var hard: Array = []
|
||||
var easy: Array = []
|
||||
for d in data.bosses:
|
||||
var t := float(d.hp_factor) * float(d.hall_factor) / float(d.army_dps_factor)
|
||||
var label := "%s(%.1fs)" % [d.hall_id, t]
|
||||
if t > float(d.active_sec):
|
||||
hard.append(label)
|
||||
else:
|
||||
easy.append(label)
|
||||
# 事实:殿系数 ≤2.0 的殿(H1-H8)军列可胜;>2.0 的殿(H9/H10)必超时
|
||||
a.eq(easy.size(), 8, "军列单推可胜的殿 = 8 座:%s" % ", ".join(easy))
|
||||
a.eq(hard.size(), 2, "军列单推必超时的殿 = 2 座:%s(上报文策渊的平衡缺陷)" % ", ".join(hard))
|
||||
# 点击侧天花板(常数,不随产出/s 增长)—— 后期补不上缺口的根源。
|
||||
# 2^升级数 不写死:从 upgrades.csv 里数 click_mult 型的行数(数据行纪律)。
|
||||
var click_up_count := 0
|
||||
# ---- 点击伤害缩放 κ(方案 B2):零投资 0.010 / 满配 0.0806 / 分母 12 / DPS 占比 20.5% ----
|
||||
func _test_kappa_scaling(a, data) -> void:
|
||||
# 点击树 = 12 条(type == "click" 的数据行数;防双重计入:L5-12 的 +1% 产出/s 走全局区,
|
||||
# 不进 κ 分子——见 economy_state.recompute 注释)
|
||||
var click_total := 0
|
||||
for u in data.upgrades:
|
||||
if u.effect_type == "click_mult":
|
||||
click_up_count += 1
|
||||
var click_cap := data.tune_num("tune_click_base", 1.0) \
|
||||
* pow(2.0, float(click_up_count)) * (1.0 + float(data.bosses[0].combo_cap))
|
||||
a.near(click_cap, 25.6, 1e-6,
|
||||
"点击单次伤害天花板 = 基数1 × 2^%d(点击升级) × 1.6(连击封顶) = 25.6(常数)" % click_up_count)
|
||||
# 若哪天把点击伤害改成随产出/s 增长,上面这条会红 → 说明可胜性结构已变,需重评
|
||||
if u.type == "click":
|
||||
click_total += 1
|
||||
a.eq(click_total, 12, "点击树 12 条(κ 的 click_ratio 分母)")
|
||||
# 零投资
|
||||
var e := _new_econ(data, 2000.0)
|
||||
var b := BossBattle.new(data)
|
||||
b.start(e, 0)
|
||||
a.near(b.entry_kappa, 0.010, 1e-9, "零投资 κ = 0.010")
|
||||
a.near(b.click_line_ratio(e), 0.0, 1e-9, "法器线进度 0/12")
|
||||
# 半线(6/12,无 m06):κ = 0.010 + 0.052×0.5 = 0.036
|
||||
var e2 := _new_econ(data, 2000.0)
|
||||
var n := 0
|
||||
for u in data.upgrades:
|
||||
if u.type == "click":
|
||||
e2.bought[u.id] = true
|
||||
n += 1
|
||||
if n >= 6:
|
||||
break
|
||||
e2.recompute()
|
||||
var b2 := BossBattle.new(data)
|
||||
b2.start(e2, 0)
|
||||
a.near(b2.click_line_ratio(e2), 0.5, 1e-9, "法器线进度 6/12 = 0.5")
|
||||
a.near(b2.entry_kappa, 0.036, 1e-9, "半线 κ = 0.036")
|
||||
# 满配(12/12 + m06):κ = 0.062 × 1.30 = 0.0806
|
||||
var e3 := _new_econ(data, 2000.0)
|
||||
_full_click_config(e3, data)
|
||||
var b3 := BossBattle.new(data)
|
||||
b3.start(e3, 0)
|
||||
a.near(b3.click_line_ratio(e3), 1.0, 1e-9, "法器线进度 12/12 = 1.0")
|
||||
a.near(b3.entry_kappa, 0.0806, 1e-9, "满配 κ = (0.010+0.052) × 1.30 = 0.0806")
|
||||
# 封顶连击下的点击 DPS 占比 = 2κ×1.6 / (1 + 2κ×1.6) = 20.5%(spec §1.3 的"兵七成我三成")
|
||||
var kappa := 0.0806
|
||||
var click_dps := 2.0 * kappa * 1.6 # 2 次/s × κ × 连击封顶
|
||||
a.near(click_dps / (1.0 + click_dps), 0.205, 0.005,
|
||||
"封顶连击点击 DPS 占比 = 20.5%(军列 79.5% / 点击 20.5%)")
|
||||
# m06 单独验证:0 阶 → ×1.0;1 阶 → ×1.30
|
||||
var e4 := _new_econ(data, 2000.0)
|
||||
for u in data.upgrades:
|
||||
if u.type == "click":
|
||||
e4.bought[u.id] = true
|
||||
e4.recompute()
|
||||
var b4 := BossBattle.new(data)
|
||||
b4.start(e4, 0)
|
||||
a.near(b4.entry_kappa, 0.062, 1e-9, "12/12 无 m06 → κ = 0.062")
|
||||
|
||||
|
||||
# ---- 可胜性结构(方案 B 后重写;替代旧"8 胜 / 2 必超时 / 天花板 25.6"事实哨兵) ----
|
||||
# 断言 = systems-spec §1.4 修订门 #11 五判据 + 用户 W4 要求:
|
||||
# a) 十殿胜率 100%(挂机单推即过) b) 挂机余时 ∈ [2,8]s
|
||||
# c) 满配余时 ∈ [7,12]s d) 余时差(满配−挂机)≥ 4s
|
||||
# e) 点击伤害占比 ∈ [15%,25%] + 满配 30s 容量:军列+点击 ≥ HP
|
||||
func _test_winnability_b(a, data) -> void:
|
||||
var expect_s := [23.0, 23.5, 24.0, 24.5, 25.0, 25.5, 26.0, 26.5, 27.0, 28.0]
|
||||
for i in data.halls.size():
|
||||
var h: HallDef = data.halls[i]
|
||||
var s_n: float = expect_s[i]
|
||||
# --- 挂机单推(0 点击;merit_run = 0 ⇒ 超度 0 ⇒ 开场 0,纯军列口径) ---
|
||||
var e_idle := _new_econ(data, 0.0, 1000) # 产出 100/s
|
||||
var b_idle := BossBattle.new(data)
|
||||
var ri: Dictionary = b_idle.start(e_idle, i)
|
||||
a.true_(bool(ri.get("ok", false)), "%s:挂机开局" % h.id)
|
||||
var ri_run := _run_battle(b_idle, e_idle, 31.0, 0.0)
|
||||
a.true_(ri_run.win, "%s:挂机单推可胜(十殿全可胜,a 判据)" % h.id)
|
||||
a.near(ri_run.active_sec, s_n, 0.15,
|
||||
"%s:挂机耗时 = S_n = %.1fs(恒等式)" % [h.id, s_n])
|
||||
a.true_(ri_run.active_sec < 30.0, "%s:挂机不超时(富余 %.1fs)" % [h.id, 30.0 - s_n])
|
||||
var idle_left := 30.0 - ri_run.active_sec
|
||||
a.true_(idle_left >= 2.0 - 0.15 and idle_left <= 8.0 + 0.15,
|
||||
"%s:挂机余时 %.1fs ∈ [2,8](b 判据)" % [h.id, idle_left])
|
||||
# --- 满配点击(12/12 + m06,2 次/s) ---
|
||||
var e_full := _new_econ(data, 0.0, 1000)
|
||||
_full_click_config(e_full, data)
|
||||
var b_full := BossBattle.new(data)
|
||||
b_full.start(e_full, i)
|
||||
var rf := _run_battle(b_full, e_full, 31.0, 0.5)
|
||||
a.true_(rf.win, "%s:满配点击可胜" % h.id)
|
||||
var full_left := 30.0 - rf.active_sec
|
||||
a.true_(full_left >= 7.0 and full_left <= 12.0,
|
||||
"%s:满配余时 %.1fs ∈ [7,12](c 判据)" % [h.id, full_left])
|
||||
var margin := full_left - idle_left
|
||||
a.true_(margin >= 4.0,
|
||||
"%s:余时差 %.1fs ≥ 4s(d 判据:点击线有决定性意义)" % [h.id, margin])
|
||||
var share := rf.click_dmg / float(b_full.hp_max)
|
||||
a.true_(share >= 0.15 and share <= 0.25,
|
||||
"%s:点击伤害占比 %.1f%% ∈ [15%%,25%%](e 判据)" % [h.id, share * 100.0])
|
||||
# --- 满配 30s 伤害容量:军列 + 点击 ≥ HP(keep_alive 实测,无提前死亡) ---
|
||||
var e_cap := _new_econ(data, 0.0, 1000)
|
||||
_full_click_config(e_cap, data)
|
||||
var b_cap := BossBattle.new(data)
|
||||
b_cap.start(e_cap, i)
|
||||
var rc := _run_battle(b_cap, e_cap, 30.0, 0.5, true)
|
||||
var cap_ratio := (rc.army_dmg + rc.click_dmg) / float(b_cap.hp_max)
|
||||
a.true_(cap_ratio >= 1.0,
|
||||
"%s:30s 军列+点击容量 = HP × %.2f ≥ 1(余量充足)" % [h.id, cap_ratio])
|
||||
# 挂机容量同样 ≥ 1(纯军列 30s 也能磨死,只是压哨)
|
||||
var e_cap2 := _new_econ(data, 0.0, 1000)
|
||||
var b_cap2 := BossBattle.new(data)
|
||||
b_cap2.start(e_cap2, i)
|
||||
var rc2 := _run_battle(b_cap2, e_cap2, 30.0, 0.0, true)
|
||||
var cap_ratio2 := rc2.army_dmg / float(b_cap2.hp_max)
|
||||
a.true_(cap_ratio2 >= 1.0,
|
||||
"%s:30s 纯军列容量 = HP × %.2f ≥ 1" % [h.id, cap_ratio2])
|
||||
# H10 点击占比 ≈ 20.5% ±3pp(spec 表头值;实测为累计口径 ~18%,见 provenance P-25 说明)
|
||||
var e10 := _new_econ(data, 0.0, 1000)
|
||||
_full_click_config(e10, data)
|
||||
var b10 := BossBattle.new(data)
|
||||
b10.start(e10, 9)
|
||||
var r10 := _run_battle(b10, e10, 31.0, 0.5)
|
||||
var share10 := r10.click_dmg / float(b10.hp_max)
|
||||
a.true_(share10 >= 0.175 and share10 <= 0.235,
|
||||
"H10:满配点击占总伤害 %.1f%% ≈ 20.5%%(容差 ±3 个百分点)" % (share10 * 100.0))
|
||||
@@ -40,19 +40,39 @@ func run(a, _data) -> void:
|
||||
a.true_(PrestigeModel.enthrone_available(2, 2), "敕封:转世 2 可")
|
||||
a.true_(not PrestigeModel.enthrone_available(1, 2), "敕封:转世 1 不可")
|
||||
|
||||
# ---- OfflineModel 分带 + 72h 封顶 + 时钟回拨 ----
|
||||
var r1: Dictionary = OfflineModel.settle(3600.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
# ---- OfflineModel 分带 + 72h 封顶 + 时钟回拨 + 长明灯加法 + 硬顶 ----
|
||||
# W4 契约 v2(systems-spec §2.4/§2.5):第三参数由 offline_mult(乘性)改为
|
||||
# offline_add(加法,直接加在各档速率上,各档硬顶 100%)。
|
||||
var r1: Dictionary = OfflineModel.settle(3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r1["effective_hours"], 1.0, 1e-9, "离线 1h = 1h 等效")
|
||||
a.near(r1["merit"], 36000.0, 1e-6, "离线 1h × 10/s")
|
||||
var r8: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r8["effective_hours"], 6.2, 1e-9, "离线 8h = 6.2h 等效(P-8)")
|
||||
var r8: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r8["effective_hours"], 6.2, 1e-9, "离线 8h = 6.2h 等效(契约 v2:77.5%)")
|
||||
a.near(r8["merit"], 223200.0, 1e-6, "离线 8h 收益")
|
||||
var r100: Dictionary = OfflineModel.settle(100.0 * 3600.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r100["effective_hours"], 17.4, 1e-9, "离线 100h → 72h 封顶 = 17.4h 等效")
|
||||
var rneg: Dictionary = OfflineModel.settle(-5.0, 10.0, 1.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
var r24: Dictionary = OfflineModel.settle(24.0 * 3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r24["effective_hours"], 12.6, 1e-9, "离线 24h = 12.6h 等效(契约 v2)")
|
||||
var r100: Dictionary = OfflineModel.settle(100.0 * 3600.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r100["effective_hours"], 17.4, 1e-9, "离线 100h → 72h 封顶 = 17.4h 等效(24.2%)")
|
||||
var rneg: Dictionary = OfflineModel.settle(-5.0, 10.0, 0.0, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(rneg["merit"], 0.0, 1e-9, "时钟回拨 → 0")
|
||||
a.near(OfflineModel.ratio_8h_over_1h(2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1), 6.2, 1e-9,
|
||||
"门 #9 比率 6.2(契约 [1.5,2.5] 预计 FAIL,P-8)")
|
||||
"历史口径比率 6.2(契约 v2 起门 #9 改判等效小时,见 SimGates.gate_offline_v2)")
|
||||
# 长明灯加法:+30%(6 级)⇒ 2-8h 档 70%+30% = 100%(硬顶)⇒ 8h = 8.0h(效率 100%)
|
||||
var r8l: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 0.30, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r8l["effective_hours"], 8.0, 1e-9, "长明灯满级 8h = 8.0h 等效(硬顶 100%)")
|
||||
a.near(r8l["merit"], 288000.0, 1e-6, "长明灯满级 8h 收益")
|
||||
# 加法不越过硬顶:+55%(6 级 + m05)仍 = 8.0h;8-24h 档 40%+30% = 70%
|
||||
var r8m: Dictionary = OfflineModel.settle(8.0 * 3600.0, 10.0, 0.55, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r8m["effective_hours"], 8.0, 1e-9, "加法 +55% 仍钳 8.0h(0-2h/2-8h 双档顶格)")
|
||||
var r24l: Dictionary = OfflineModel.settle(24.0 * 3600.0, 10.0, 0.30, 2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.near(r24l["effective_hours"], 2.0 + 6.0 + 16.0 * 0.7, 1e-9, "长明灯满级 24h = 19.2h(8-24h 档 70%)")
|
||||
# 效率硬顶:任意加成下等效 ≤ 实际(离线不得优于在线)
|
||||
for dur_h in [1.0, 8.0, 24.0, 72.0]:
|
||||
for add in [0.0, 0.30, 0.55, 5.0]:
|
||||
var rr: Dictionary = OfflineModel.settle(dur_h * 3600.0, 10.0, add,
|
||||
2.0, 1.0, 8.0, 0.7, 24.0, 0.4, 0.1, 72.0)
|
||||
a.true_(float(rr["effective_hours"]) <= dur_h + 1e-9,
|
||||
"效率硬顶:%gh 离线 加法 +%g ⇒ 等效 %.2fh ≤ %gh" % [dur_h, add, float(rr["effective_hours"]), dur_h])
|
||||
|
||||
# ---- BigNumber 红线 ----
|
||||
a.true_(BigNumber.assert_within(1e13, "test"), "1e13 不过红线")
|
||||
|
||||
@@ -143,7 +143,7 @@ func _test_enthrone(a, data) -> void:
|
||||
a.near(e.output_per_sec(), 1073741824.0 * 4.25, 1.0, "gen12 1 级产出")
|
||||
|
||||
|
||||
# ---- 事件瞬时区 + 离线倍率 ----
|
||||
# ---- 事件瞬时区 + 离线加法(契约 v2:长明灯 ×2.31 → 加法 +30%) ----
|
||||
func _test_event_and_offline(a, data) -> void:
|
||||
var e := EconomyState.new(data)
|
||||
e.gen_levels[0] = 10
|
||||
@@ -151,9 +151,18 @@ func _test_event_and_offline(a, data) -> void:
|
||||
e.set_event_mult(5.0)
|
||||
a.near(e.output_per_sec(), base_out * 5.0, 1e-9, "事件 ×5 瞬时区")
|
||||
e.set_event_mult(1.0)
|
||||
a.near(e.offline_mult(), 1.0, 1e-9, "无长明灯 → ×1")
|
||||
a.near(e.offline_add(), 0.0, 1e-9, "无长明灯 → 加法 +0")
|
||||
e.halls_passed = 4 # 离线升级出现条件
|
||||
e.merit = 1e9
|
||||
var r: Dictionary = e.buy_upgrade("up_offline_1")
|
||||
a.true_(bool(r.get("ok", false)), "买长明灯 I")
|
||||
a.near(e.offline_mult(), 1.15, 1e-9, "离线 ×1.15(P-13 乘性链)")
|
||||
a.near(e.offline_add(), 0.05, 1e-9, "长明灯 I → 加法 +5%(旧 ×1.15 乘性已废止)")
|
||||
for i in range(2, 7):
|
||||
var ri: Dictionary = e.buy_upgrade("up_offline_%d" % i)
|
||||
a.true_(bool(ri.get("ok", false)), "买长明灯 %d" % i)
|
||||
a.near(e.offline_add(), 0.30, 1e-9, "长明灯 6 级满 = 加法 +30%(各档速率硬顶 100%)")
|
||||
# m05 阴司人脉(每阶 +25% 加法,max_rank 1)
|
||||
e.suihui = 100
|
||||
var rm: Dictionary = e.buy_meta("meta_05")
|
||||
a.true_(bool(rm.get("ok", false)), "买阴司人脉")
|
||||
a.near(e.offline_add(), 0.55, 1e-9, "+ m05 = 加法 +55%(仍由 OfflineModel 钳各档 ≤100%)")
|
||||
@@ -0,0 +1,84 @@
|
||||
extends RefCounted
|
||||
|
||||
# 用例:W4 离线契约 v2(systems-spec §2.4/§2.5,用户 2026-10-20 拍板)
|
||||
# 契约 v2:8h = 6.2 等效小时(效率 77.5%);24h = 12.6h;72h 封顶 = 17.4h(24.2%);
|
||||
# 长明灯 6 条 × +5% 加法(满 +30%,各档速率硬顶 100%);效率硬顶 ≤ 100%。
|
||||
# 门 #9 拆三条子检查(a 由 FAIL 转 PASS):9a 契约效率 / 9b 长明灯上限 / 9c 硬顶。
|
||||
|
||||
func run(a, data) -> void:
|
||||
_test_contract_v2(a, data)
|
||||
_test_gate_offline_v2(a, data)
|
||||
|
||||
|
||||
func _settle(a, data, hours: float, add: float) -> float:
|
||||
var r: Dictionary = OfflineModel.settle(hours * 3600.0, 1.0, add,
|
||||
data.tune_num("tune_offline_band1_h", 2.0), data.tune_num("tune_offline_band1_rate", 1.0),
|
||||
data.tune_num("tune_offline_band2_h", 8.0), data.tune_num("tune_offline_band2_rate", 0.7),
|
||||
data.tune_num("tune_offline_band3_h", 24.0), data.tune_num("tune_offline_band3_rate", 0.4),
|
||||
data.tune_num("tune_offline_band4_rate", 0.1),
|
||||
data.tune_num("tune_offline_cap_h", 72.0))
|
||||
return float(r["effective_hours"])
|
||||
|
||||
|
||||
# ---- 契约 v2 数值面 ----
|
||||
func _test_contract_v2(a, data) -> void:
|
||||
# 衰减表一字不改(systems-spec §2.4:错的是契约不是表)
|
||||
a.near(data.tune_num("tune_offline_band1_rate", 0.0), 1.0, 1e-9, "0-2h = 100%")
|
||||
a.near(data.tune_num("tune_offline_band2_rate", 0.0), 0.7, 1e-9, "2-8h = 70%")
|
||||
a.near(data.tune_num("tune_offline_band3_rate", 0.0), 0.4, 1e-9, "8-24h = 40%")
|
||||
a.near(data.tune_num("tune_offline_band4_rate", 0.0), 0.1, 1e-9, ">24h = 10%")
|
||||
a.near(data.tune_num("tune_offline_cap_h", 0.0), 72.0, 1e-9, "72h 封顶")
|
||||
# 8h = 6.2 等效小时(77.5%)∈ [5.5, 7.0](用户口径)/ [5.5, 6.5](spec 口径,双重满足)
|
||||
var e8 := _settle(a, data, 8.0, 0.0)
|
||||
a.near(e8, 6.2, 1e-9, "8h 离线 = 6.2h 等效(契约 v2)")
|
||||
a.true_(e8 >= 5.5 and e8 <= 7.0, "8h 等效 ∈ [5.5, 7.0](用户构造口径)")
|
||||
a.true_(e8 >= 5.5 and e8 <= 6.5, "8h 等效 ∈ [5.5, 6.5](systems-spec §2.4 口径)")
|
||||
a.near(e8 / 8.0, 0.775, 1e-9, "8h 效率 = 77.5%")
|
||||
# 24h = 12.6h;72h 封顶 = 17.4h(24.2%)
|
||||
a.near(_settle(a, data, 24.0, 0.0), 12.6, 1e-9, "24h 离线 = 12.6h 等效")
|
||||
a.near(_settle(a, data, 72.0, 0.0), 17.4, 1e-9, "72h 封顶 = 17.4h 等效")
|
||||
a.near(_settle(a, data, 72.0, 0.0) / 72.0, 17.4 / 72.0, 1e-9, "72h 效率 ≈ 24.2%")
|
||||
a.near(_settle(a, data, 100.0, 0.0), 17.4, 1e-9, "100h 也被 72h 硬顶钳到 17.4h")
|
||||
# 长明灯:数据面 = 6 条 × 0.05 加法(effect_type offline_add)
|
||||
var lamp := 0.0
|
||||
var lamp_rows := 0
|
||||
for u in data.upgrades:
|
||||
if u.type == "offline":
|
||||
lamp += u.effect_param
|
||||
lamp_rows += 1
|
||||
a.eq(lamp_rows, 6, "长明灯 6 条")
|
||||
a.near(lamp, 0.30, 1e-9, "长明灯满级 = 加法 +30%(旧 ×2.31 乘性已废止)")
|
||||
# 长明灯满级:8h = 8.0h,效率恰好 100%(硬顶允许的最大值)
|
||||
var e8l := _settle(a, data, 8.0, lamp)
|
||||
a.near(e8l, 8.0, 1e-9, "长明灯满级 8h = 8.0h 等效")
|
||||
a.near(e8l / 8.0, 1.0, 1e-9, "效率 = 100%(≤ 硬顶,离线不优于在线)")
|
||||
# + m05(+25%)也不越过硬顶
|
||||
var m05 := data.meta("meta_05")
|
||||
var m05_add := 0.0
|
||||
if m05 != null:
|
||||
m05_add = m05.effect_param * float(m05.max_rank)
|
||||
a.near(_settle(a, data, 8.0, lamp + m05_add), 8.0, 1e-9, "+ m05(+25%)仍钳 8.0h")
|
||||
# 效率硬顶:任意配置/任意时长 ≤ 100%
|
||||
for dur_h in [1.0, 8.0, 24.0, 72.0]:
|
||||
for add in [0.0, lamp, lamp + m05_add, 5.0]:
|
||||
var eff := _settle(a, data, dur_h, add)
|
||||
a.true_(eff <= dur_h + 1e-9,
|
||||
"效率硬顶:%sh + 加法 %.2f ⇒ %.2fh ≤ %sh" % [str(dur_h), add, eff, str(dur_h)])
|
||||
|
||||
|
||||
# ---- 门 #9 三条子检查(a 由 FAIL 转 PASS) ----
|
||||
func _test_gate_offline_v2(a, data) -> void:
|
||||
var g: Dictionary = SimGates.gate_offline_v2(data)
|
||||
a.true_(bool(g.get("pass", false)), "门 #9 三条子检查全过(a 由 FAIL 转 PASS)")
|
||||
a.true_(str(g.get("detail", "")).contains("契约 v2"), "detail 标注契约 v2")
|
||||
var sub: Dictionary = g.get("sub", {})
|
||||
var g9a: Dictionary = sub.get("9a", {})
|
||||
var g9b: Dictionary = sub.get("9b", {})
|
||||
var g9c: Dictionary = sub.get("9c", {})
|
||||
a.true_(bool(g9a.get("pass", false)),
|
||||
"9a PASS:%s" % str(g9a.get("detail", "")))
|
||||
a.true_(bool(g9b.get("pass", false)),
|
||||
"9b PASS:%s" % str(g9b.get("detail", "")))
|
||||
a.true_(bool(g9c.get("pass", false)),
|
||||
"9c PASS:%s" % str(g9c.get("detail", "")))
|
||||
a.true_(str(g.get("deferred_beta", "")).contains("β"), "进度口径 β 标记为待 sim 标定")
|
||||
@@ -43,6 +43,8 @@ const CASE_FILES: Array[String] = [
|
||||
"res://tests/unit/cases/test_camera_data.gd",
|
||||
"res://tests/unit/cases/test_event_director.gd",
|
||||
"res://tests/unit/cases/test_boss_battle.gd",
|
||||
# W4 新增:离线契约 v2 + 门 #9 三条子检查(P-8 裁定落地)
|
||||
"res://tests/unit/cases/test_offline_contract.gd",
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in new issue
Block a user