fix: 首跑错误批次(Godot 4.7.2 引擎实证修复 R1-R14)

本机首次具备引擎自验证能力后的首跑修复。四条命令 + 主场景 180 帧全绿:
- --import exit=0(漂移门 git diff data/build 空)
- --validate-only exit=0 RESULT: PASS
- test_runner exit=0 PASS(584 检查,8/8 用例)
- sim_main exit=0(报告落 tests/sim_reports/report-2026-10-20.json)
- 主场景 --quit-after 180 exit=0 零 ERROR/WARNING(F5 可玩)

预定位错误(team-lead 清单):
- R1 economy_state.gd:596 enthone_ready -> enthrone_ready(单点故障连锁)
- R2 hud_root.gd:142,160 HorizontalAlignment.LEFT -> HORIZONTAL_ALIGNMENT_LEFT
- R3 boss_arena.gd 补 var _pending_hall_index := -1
- R4 hall_band_world.gd:95 删 "_ = ext"(非法语句;--check-only 定位)
- R5 sheet_importer._type_ok 补 object->Dictionary / array->Array(9 条假违规)
- R6 game_state.gd econ==null 防护(防 autoload 崩每帧刷错打满管道挂死)

引擎新暴露错误:
- R7 prestige_model floor() Variant 推断(4.7.2 按错误)连锁编译失败
- R8 gates.gd var pass 保留字;R9 %g/%.*f 格式符 ×6 处
- R10 greedy.gd Dictionary.get() 推断;R11 probe_p1 条件表达式推断
- R12 测试用例 data 参数统一标 BalanceData(无类型参数成员调用返 Variant)
- R13 boss_battle 用例 Dictionary 下标运算显式 float 化
- R14 band_world_root.compute_ranges 返回 Array[Vector2](类型化数组不隐式转换)

测试侧数值修正(根因已核实,实现符合设计):
- gen3 可购断言 merit 1000->2000(800 文书 + 1200 首购)
- 开场伤害断言改"循环至 ACTIVE 即停"(INTRO 结束 tick 不计军列伤)
- 容量断言预算 30->34s(战斗真实给 intro 3s + active 30s)

工程加固:
- EconomyState 红线守卫首次越线才报(sim 长跑日志 6.5MB->1KB)
- stage_world.gd stretch=false(SubViewport 手动尺寸驱动生效)

知识缺口登记:docs/engineering/2026-10-20-f-engine-first-run-notes.md
sim 首轮数据:主口径 11/14 过(#2/#3/#12/#13/#14 FAIL 待文策渊裁定)
This commit is contained in:
程基岩(engineering-lead) committed 2026-10-20 18:47:07 +08:00
1 parent cdec918a5b
commit c534e95458
146 files changed
+37229 -75

No files matched your search

+2 -1
View File
@@ -122,7 +122,8 @@ func _build_ui() -> void:
func _help_text() -> String:
var mode_names := ["PASS直通", "INVERT反色", "RED红染", "GREEN纯绿"]
var mode_name := mode_names[_mode] if _mode >= 0 and _mode < mode_names.size() else "?"
# 条件表达式含数组下标 → Variant;`:=` 推断触发 INFERENCE_ON_VARIANT(4.7.2 按错误)→ 显式标注
var mode_name: String = mode_names[_mode] if _mode >= 0 and _mode < mode_names.size() else "?"
return "探针 P1 | 1 直通 2 反色 3 红染 4 纯绿 | V 显/隐后处理层(当前:%s)| S 截图到 user:// | Esc 退出 | 当前 mode=%s" % [
"显示" if _post_visible else "隐藏", mode_name]
@@ -0,0 +1 @@
uid://dalgr0imefg8i
@@ -0,0 +1 @@
uid://rfjpmnsvysyh
@@ -0,0 +1 @@
uid://ctkc3m6h3u0fk
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -2,7 +2,7 @@ extends RefCounted
# 用例:BalanceData 载入 + 跨表引用 + CSV/Bible 关键值(数据门)
func run(a, data) -> void:
func run(a, data: BalanceData) -> void:
a.true_(data.load_errors.is_empty(), "load_errors 为空(%s)" % str(data.load_errors))
a.eq(data.generators.size(), 12, "12 班底")
a.eq(data.upgrades.size(), 108, "108 升级")
@@ -0,0 +1 @@
uid://c4olnwsjcfrbc
+32 -24
View File
@@ -10,7 +10,7 @@ extends RefCounted
# 钉的是旧 HP 公式下的**缺陷事实**;方案 B 落地后按 systems-spec 第七章第 1 条
# 重写为"十殿全可胜 + S_n + κ + 修订门 #11 五判据"的**设计意图断言**。
func run(a, data) -> void:
func run(a, data: BalanceData) -> void:
_test_hp_formula(a, data)
_test_win(a, data)
_test_intro_damage(a, data)
@@ -21,7 +21,7 @@ func run(a, data) -> void:
_test_winnability_b(a, data)
func _new_econ(data, merit_run: float, gen0_level: int = 10) -> EconomyState:
func _new_econ(data: BalanceData, merit_run: float, gen0_level: int = 10) -> EconomyState:
var e := EconomyState.new(data)
e.gen_levels[0] = gen0_level # 默认 10 级 → 产出 1.0/s(gen1 rate 0.1)
e.merit_run_total = merit_run
@@ -31,7 +31,7 @@ func _new_econ(data, merit_run: float, gen0_level: int = 10) -> EconomyState:
## 满配点击:12 条点击升级全购 + m06 斩鬼刃 1 阶(直接置位 + recompute,同旧用例对
## up_gate_boss_retry 的手法;购买合法性由 test_economy_state 覆盖)。
func _full_click_config(e: EconomyState, data) -> void:
func _full_click_config(e: EconomyState, data: BalanceData) -> void:
for u in data.upgrades:
if u.type == "click":
e.bought[u.id] = true
@@ -75,7 +75,7 @@ func _run_battle(b: BossBattle, e: EconomyState, max_sec: float,
# ---- HP = 产出/s × boss_army_seconds(方案 B1;hall_factor 降级为纯叙事参数) ----
func _test_hp_formula(a, data) -> void:
func _test_hp_formula(a, data: BalanceData) -> void:
var e := _new_econ(data, 2000.0) # 过 H1
a.eq(e.halls_passed, 1, "H1 门达成(2000 阴德)")
var out := e.output_per_sec()
@@ -106,7 +106,7 @@ func _test_hp_formula(a, data) -> void:
# ---- 军列 DPS = 产出/s × 1.0;S_n 秒打完 → 胜 + 阴德爆发 HP×0.2 ----
func _test_win(a, data) -> void:
func _test_win(a, data: BalanceData) -> void:
var e := _new_econ(data, 2000.0)
var b := BossBattle.new(data)
var info: Dictionary = b.available(e)
@@ -147,7 +147,7 @@ func _test_win(a, data) -> void:
# ---- 开场冲锋 v2(方案 B3):HP × 0.10 × clamp(超度/souls_par, 0, 1) ----
func _test_intro_damage(a, data) -> void:
func _test_intro_damage(a, data: BalanceData) -> 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
@@ -157,30 +157,37 @@ func _test_intro_damage(a, data) -> void:
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:
# 走到 ACTIVE 即停(INTRO 结束那一次 tick 不造成军列伤害;多跑会掺入 0.1s 军列伤)
for _i in 40:
b.tick(0.1, e)
if b.phase == BossBattle.Phase.ACTIVE:
break
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:
for _i in 40:
b2.tick(0.1, e2)
if b2.phase == BossBattle.Phase.ACTIVE:
break
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:
for _i in 40:
b3.tick(0.1, e3)
if b3.phase == BossBattle.Phase.ACTIVE:
break
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:
func _test_combo_cap(a, data: BalanceData) -> void:
var e := _new_econ(data, 2000.0, 200)
var b := BossBattle.new(data)
b.start(e, 0)
@@ -214,7 +221,7 @@ func _test_combo_cap(a, data) -> void:
# ---- 败北:超时 → 冷却 60s;购路引可立即再战 ----
func _test_lose_and_retry(a, data) -> void:
func _test_lose_and_retry(a, data: BalanceData) -> void:
var e := _new_econ(data, 2000.0)
var b := BossBattle.new(data)
b.start(e, 0)
@@ -243,7 +250,7 @@ func _test_lose_and_retry(a, data) -> void:
# ---- 出现条件:未过任何殿门 / 产出为 0 → 不可挑战 ----
func _test_availability(a, data) -> void:
func _test_availability(a, data: BalanceData) -> void:
var e0 := EconomyState.new(data)
var b0 := BossBattle.new(data)
a.false_(bool(b0.available(e0).get("ok", false)), "未过殿门 + 产出 0 → 不可挑战")
@@ -266,7 +273,7 @@ func _test_availability(a, data) -> void:
# ---- 点击伤害缩放 κ(方案 B2):零投资 0.010 / 满配 0.0806 / 分母 12 / DPS 占比 20.5% ----
func _test_kappa_scaling(a, data) -> void:
func _test_kappa_scaling(a, data: BalanceData) -> void:
# 点击树 = 12 条(type == "click" 的数据行数;防双重计入:L5-12 的 +1% 产出/s 走全局区,
# 不进 κ 分子——见 economy_state.recompute 注释)
var click_total := 0
@@ -322,7 +329,7 @@ func _test_kappa_scaling(a, data) -> void:
# a) 十殿胜率 100%(挂机单推即过) b) 挂机余时 ∈ [2,8]s
# c) 满配余时 ∈ [7,12]s d) 余时差(满配−挂机)≥ 4s
# e) 点击伤害占比 ∈ [15%,25%] + 满配 30s 容量:军列+点击 ≥ HP
func _test_winnability_b(a, data) -> void:
func _test_winnability_b(a, data: BalanceData) -> 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]
@@ -337,7 +344,7 @@ func _test_winnability_b(a, data) -> void:
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
var idle_left: float = 30.0 - float(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) ---
@@ -347,30 +354,31 @@ func _test_winnability_b(a, data) -> void:
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
var full_left: float = 30.0 - float(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
var margin: float = 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)
var share: float = float(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 实测,无提前死亡) ---
# --- 满配 30s 伤害容量:军列 + 点击 ≥ HP(keep_alive 实测,无提前死亡)---
# 预算须含 INTRO 3s:战斗给 intro_sec(3) + active_sec(30) = 33s,+1s 余量
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)
var rc := _run_battle(b_cap, e_cap, 34.0, 0.5, true)
var cap_ratio: float = (float(rc.army_dmg) + float(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)
var rc2 := _run_battle(b_cap2, e_cap2, 34.0, 0.0, true)
var cap_ratio2: float = float(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 说明)
@@ -379,6 +387,6 @@ func _test_winnability_b(a, data) -> void:
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)
var share10: float = float(r10.click_dmg) / float(b10.hp_max)
a.true_(share10 >= 0.175 and share10 <= 0.235,
"H10:满配点击占总伤害 %.1f%% ≈ 20.5%%(容差 ±3 个百分点)" % (share10 * 100.0))
+1
View File
@@ -0,0 +1 @@
uid://c7a1huxdkidnn
+5 -5
View File
@@ -3,14 +3,14 @@ extends RefCounted
# 用例:W3 相机 rig 数据行 + CameraDef 纯数学(camera.csv / balance_tuning.csv)
# 说明:这些都是纯函数/静态函数,故可在无头 SceneTree 下直接跑,不需要 Godot 渲染。
func run(a, data) -> void:
func run(a, data: BalanceData) -> void:
_test_rows(a, data)
_test_geometry(a, data)
_test_kinematics(a)
_test_kinematics(a, data)
# ---- 数据行:每殿恰一行 + 参数合法 ----
func _test_rows(a, data) -> void:
func _test_rows(a, data: BalanceData) -> void:
a.eq(data.cameras.size(), 10, "camera.csv 十行(每殿一行)")
for h in data.halls:
var c := data.camera_for_hall(h.id)
@@ -34,7 +34,7 @@ func _test_rows(a, data) -> void:
# ---- 几何:band 排布 + 进度焦点锚定 ----
func _test_geometry(a, data) -> void:
func _test_geometry(a, data: BalanceData) -> void:
var cams: Array = []
for h in data.halls:
cams.append(data.camera_for_hall(h.id))
@@ -81,7 +81,7 @@ func _test_geometry(a, data) -> void:
# ---- 运动学:弹簧回弹 / 指数平滑 / 偷看夹逼 ----
func _test_kinematics(a, data) -> void:
func _test_kinematics(a, data: BalanceData) -> void:
var k := data.tune_num("tune_cam_peek_spring", 42.0)
var c := data.tune_num("tune_cam_peek_damp", 11.0)
var max_px := data.tune_num("tune_cam_peek_max_px", 320.0)
+1
View File
@@ -0,0 +1 @@
uid://doy0xfsil7mcp
+3 -2
View File
@@ -2,7 +2,7 @@ extends RefCounted
# 用例:核心公式(MultiplierSet / PrestigeModel / OfflineModel)
func run(a, _data) -> void:
func run(a, _data: BalanceData) -> void:
# ---- MultiplierSet 三乘区 + 软上限 ----
var m := MultiplierSet.new(16.0, 8.0, 64.0)
m.set_click_levels(4, 0.0, 0.01)
@@ -72,7 +72,8 @@ func run(a, _data) -> void:
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])
"效率硬顶:%s h 离线 加法 +%s ⇒ 等效 %.2f h ≤ %s h" % [
str(dur_h), str(add), float(rr["effective_hours"]), str(dur_h)])
# ---- BigNumber 红线 ----
a.true_(BigNumber.assert_within(1e13, "test"), "1e13 不过红线")
@@ -0,0 +1 @@
uid://kk23ypxv3x8k
+9 -9
View File
@@ -2,7 +2,7 @@ extends RefCounted
# 用例:EconomyState 状态机(点击/购买/殿门/冤气/prestige/敕封)
func run(a, data) -> void:
func run(a, data: BalanceData) -> void:
_test_first_purchase(a, data)
_test_hall_gate(a, data)
_test_upgrades_and_meta(a, data)
@@ -13,7 +13,7 @@ func run(a, data) -> void:
# ---- 首购循环:15 点击 → 买 gen1 → 产出 0.1/s(GDD demo 验证点)----
func _test_first_purchase(a, data) -> void:
func _test_first_purchase(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
a.near(e.merit, 0.0, 1e-9, "初始阴德 0")
a.near(e.output_per_sec(), 0.0, 1e-9, "初始产出 0")
@@ -35,7 +35,7 @@ func _test_first_purchase(a, data) -> void:
# ---- 殿门单计数器闸门 ----
func _test_hall_gate(a, data) -> void:
func _test_hall_gate(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
a.false_(e.generator_unlocked(2), "gen3 初始锁定")
e.merit_run_total = 1999.0
@@ -47,7 +47,7 @@ func _test_hall_gate(a, data) -> void:
a.true_(e.upgrade_visible(data.upgrade("up_eff_gen_03_guizu_1")), "H1 后 gen3 效率可见")
a.true_(e.upgrade_visible(data.upgrade("up_gate_guizu")), "H1 后入职文书可见")
a.false_(e.generator_unlocked(2), "过殿但未买文书 → gen3 仍锁")
e.merit = 1000.0
e.merit = 2000.0 # 覆盖入职文书 800 + gen3 首购 1200(generators.csv base_cost)
var r: Dictionary = e.buy_upgrade("up_gate_guizu")
a.true_(bool(r.get("ok", false)), "买鬼卒班入职文书(800)")
a.true_(e.generator_unlocked(2), "双闸齐 → gen3 解锁")
@@ -56,7 +56,7 @@ func _test_hall_gate(a, data) -> void:
# ---- 升级/宿慧商店 ----
func _test_upgrades_and_meta(a, data) -> void:
func _test_upgrades_and_meta(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
e.suihui = 10
var r: Dictionary = e.buy_meta("meta_01")
@@ -77,7 +77,7 @@ func _test_upgrades_and_meta(a, data) -> void:
# ---- 冤气线 ----
func _test_wrath(a, data) -> void:
func _test_wrath(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
e.devour(100.0)
a.near(e.wrath, 100.0, 1e-9, "吞噬 +100 冤气")
@@ -100,7 +100,7 @@ func _test_wrath(a, data) -> void:
# ---- 轮回转世 ----
func _test_prestige(a, data) -> void:
func _test_prestige(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
a.false_(e.prestige_available(), "初始不可轮回")
e.merit_run_total = 1.5e9
@@ -121,7 +121,7 @@ func _test_prestige(a, data) -> void:
# ---- 阴司敕封 + gen12 愿景档 ----
func _test_enthrone(a, data) -> void:
func _test_enthrone(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
e.rebirths = 2
e.suihui = 100
@@ -144,7 +144,7 @@ func _test_enthrone(a, data) -> void:
# ---- 事件瞬时区 + 离线加法(契约 v2:长明灯 ×2.31 → 加法 +30%) ----
func _test_event_and_offline(a, data) -> void:
func _test_event_and_offline(a, data: BalanceData) -> void:
var e := EconomyState.new(data)
e.gen_levels[0] = 10
var base_out := e.output_per_sec()
@@ -0,0 +1 @@
uid://c1nkbobmp0tv8
+11 -9
View File
@@ -4,7 +4,7 @@ extends RefCounted
# 说明:EventDirector 是 RefCounted,不依赖场景树 → 无头可跑。
# 断言口径:效果必经 EconomyState(瞬时区/惩罚/双货币),UI 只读 view()。
func run(a, data) -> void:
func run(a, data: BalanceData) -> void:
_test_gate(a, data)
_test_output_mult(a, data)
_test_click_output_mult(a, data)
@@ -13,7 +13,7 @@ func run(a, data) -> void:
_test_timeout_default(a, data)
func _new_econ(data) -> EconomyState:
func _new_econ(data: BalanceData) -> EconomyState:
var e := EconomyState.new(data)
e.bought["up_gate_events"] = true # 事件系统路引(过 H2 后出现)
e.gen_levels[0] = 10 # 游魂 10 级 → 产出 1.0/s(gen1 rate 0.1)
@@ -21,7 +21,7 @@ func _new_econ(data) -> EconomyState:
# ---- 出现前提 ----
func _test_gate(a, data) -> void:
func _test_gate(a, data: BalanceData) -> void:
var d := EventDirector.new(data, 7)
var e := EconomyState.new(data)
e.gen_levels[0] = 10
@@ -33,7 +33,7 @@ func _test_gate(a, data) -> void:
# ---- 百鬼夜行:全产出 ×5,60s 后清零 ----
func _test_output_mult(a, data) -> void:
func _test_output_mult(a, data: BalanceData) -> void:
var d := EventDirector.new(data, 11)
var e := _new_econ(data)
var base := e.output_per_sec()
@@ -55,7 +55,7 @@ func _test_output_mult(a, data) -> void:
# ---- 天狗食月:点击 ×10 + 全产出 ×2 ----
func _test_click_output_mult(a, data) -> void:
func _test_click_output_mult(a, data: BalanceData) -> void:
var d := EventDirector.new(data, 13)
var e := _new_econ(data)
var base_out := e.output_per_sec()
@@ -77,7 +77,7 @@ func _test_click_output_mult(a, data) -> void:
# ---- 阴兵闹饷:窗口内点击不足 → 随机班底产出 -50% × 60s ----
func _test_challenge_fail(a, data) -> void:
func _test_challenge_fail(a, data: BalanceData) -> void:
var d := EventDirector.new(data, 17)
var e := _new_econ(data)
var ev := data.event("ev_02_naoxiang")
@@ -105,7 +105,7 @@ func _test_challenge_fail(a, data) -> void:
# ---- 冤魂申诉:二选一(超度 = 阴德爆发 30s 量 / 吞噬 = 冤气) ----
func _test_choice(a, data) -> void:
func _test_choice(a, data: BalanceData) -> void:
var ev := data.event("ev_03_shensu")
# opt_a:超度
var d1 := EventDirector.new(data, 19)
@@ -128,7 +128,9 @@ func _test_choice(a, data) -> void:
d2.force_start(e2, "ev_03_shensu")
var r2 := d2.choose(e2, 1)
a.true_(bool(r2.get("ok", false)), "选 opt_b(吞噬)")
var want := lerp(float(ev.p_clicks), float(ev.p_window_sec),
# lerp() 在 Godot 4.x 返回 Variant(同 floor 家族);`:=` 推断会触发
# INFERENCE_ON_VARIANT(4.7.2 默认按错误处理)→ 显式标注 float
var want: float = lerp(float(ev.p_clicks), float(ev.p_window_sec),
clampf(float(e2.halls_passed) / float(data.halls.size()), 0.0, 1.0))
a.near(float(r2.get("wrath", 0.0)), want, 1e-6, "吞噬冤气 = lerp(p_clicks, p_window_sec, 殿序进度)")
a.near(e2.wrath, want, 1e-6, "冤气入账")
@@ -136,7 +138,7 @@ func _test_choice(a, data) -> void:
# ---- 超时默认 = tune_event_timeout_default(opt_a 超度) ----
func _test_timeout_default(a, data) -> void:
func _test_timeout_default(a, data: BalanceData) -> void:
var d := EventDirector.new(data, 29)
var e := _new_econ(data)
var default_opt := data.tune_text("tune_event_timeout_default", "opt_a")
@@ -0,0 +1 @@
uid://64am8byb5j42
+1 -1
View File
@@ -2,7 +2,7 @@ extends RefCounted
# 用例:NumberFormat(core/numbers/number_format.gd)
func run(a, _data) -> void:
func run(a, _data: BalanceData) -> void:
a.eq(NumberFormat.short(999.0), "999", "short 999")
a.eq(NumberFormat.short(1000.0), "1.00K", "short 1K")
a.eq(NumberFormat.short(1234.0), "1.23K", "short 1234")
@@ -0,0 +1 @@
uid://bd4egxcnjd5s0
+4 -4
View File
@@ -5,12 +5,12 @@ extends RefCounted
# 长明灯 6 条 × +5% 加法(满 +30%,各档速率硬顶 100%);效率硬顶 ≤ 100%。
# 门 #9 拆三条子检查(a 由 FAIL 转 PASS):9a 契约效率 / 9b 长明灯上限 / 9c 硬顶。
func run(a, data) -> void:
func run(a, data: BalanceData) -> void:
_test_contract_v2(a, data)
_test_gate_offline_v2(a, data)
func _settle(a, data, hours: float, add: float) -> float:
func _settle(a, data: BalanceData, 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),
@@ -21,7 +21,7 @@ func _settle(a, data, hours: float, add: float) -> float:
# ---- 契约 v2 数值面 ----
func _test_contract_v2(a, data) -> void:
func _test_contract_v2(a, data: BalanceData) -> 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%")
@@ -67,7 +67,7 @@ func _test_contract_v2(a, data) -> void:
# ---- 门 #9 三条子检查(a 由 FAIL 转 PASS) ----
func _test_gate_offline_v2(a, data) -> void:
func _test_gate_offline_v2(a, data: BalanceData) -> 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")
@@ -0,0 +1 @@
uid://bl4p7rb26mify
+2 -1
View File
@@ -24,8 +24,9 @@ class TestAssert extends RefCounted:
func near(got: float, want: float, tol: float, msg: String) -> void:
checks += 1
# %g 为 C 风格格式符,Godot String % 不支持(首跑实证)→ str()
if absf(float(got) - float(want)) > tol:
failures.append("%s:got %.6g want %.6g(tol %g)" % [msg, got, want, tol])
failures.append("%s:got %s want %s(tol %s)" % [msg, str(got), str(want), str(tol)])
func true_(cond: bool, msg: String) -> void:
check(cond, msg)
+1
View File
@@ -0,0 +1 @@
uid://bhdeeuhm5udmp