本机首次具备引擎自验证能力后的首跑修复。四条命令 + 主场景 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 待文策渊裁定)
155 lines
6.6 KiB
GDScript
155 lines
6.6 KiB
GDScript
extends RefCounted
|
||
|
||
# 用例:W3 事件系统运行时(game/EventDirector;events.csv 4 事件)
|
||
# 说明:EventDirector 是 RefCounted,不依赖场景树 → 无头可跑。
|
||
# 断言口径:效果必经 EconomyState(瞬时区/惩罚/双货币),UI 只读 view()。
|
||
|
||
func run(a, data: BalanceData) -> void:
|
||
_test_gate(a, data)
|
||
_test_output_mult(a, data)
|
||
_test_click_output_mult(a, data)
|
||
_test_challenge_fail(a, data)
|
||
_test_choice(a, data)
|
||
_test_timeout_default(a, data)
|
||
|
||
|
||
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)
|
||
return e
|
||
|
||
|
||
# ---- 出现前提 ----
|
||
func _test_gate(a, data: BalanceData) -> void:
|
||
var d := EventDirector.new(data, 7)
|
||
var e := EconomyState.new(data)
|
||
e.gen_levels[0] = 10
|
||
a.false_(d.unlocked(e), "未买路引 → 事件系统未解锁")
|
||
d.tick(600.0, e) # 10 分钟(远超首延迟)
|
||
a.false_(d.is_busy(), "未解锁时不触发事件")
|
||
e.bought["up_gate_events"] = true
|
||
a.true_(d.unlocked(e), "买路引 → 解锁")
|
||
|
||
|
||
# ---- 百鬼夜行:全产出 ×5,60s 后清零 ----
|
||
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()
|
||
a.near(base, 1.0, 1e-6, "基线产出 1.0/s")
|
||
var r := d.force_start(e, "ev_01_baigui")
|
||
a.true_(bool(r.get("ok", false)), "强制开百鬼夜行")
|
||
a.eq(str(r.get("type", "")), "output_mult", "类型 output_mult")
|
||
a.near(e.output_per_sec(), base * 5.0, 1e-6, "全产出 ×5 生效(瞬时区)")
|
||
var v: Dictionary = d.view(e)
|
||
a.true_(bool(v.get("active", false)), "view 报告事件进行中")
|
||
a.eq(str(v.get("id", "")), "ev_01_baigui", "view 的 id")
|
||
a.near(float(v.get("mult", 1.0)), 5.0, 1e-9, "view 的产出倍率来自数据行")
|
||
# 跑到结束
|
||
var steps := int(float(data.event("ev_01_baigui").duration_sec) / 0.1) + 2
|
||
for _i in steps:
|
||
d.tick(0.1, e)
|
||
a.false_(d.is_busy(), "持续时间到 → 事件结束")
|
||
a.near(e.output_per_sec(), base, 1e-6, "瞬时区复位(不会永久 ×5)")
|
||
|
||
|
||
# ---- 天狗食月:点击 ×10 + 全产出 ×2 ----
|
||
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()
|
||
var base_click := e.click_value()
|
||
a.near(base_click, 1.0, 1e-6, "基线点击价值 1.0")
|
||
var r := d.force_start(e, "ev_04_tiangou")
|
||
a.true_(bool(r.get("ok", false)), "强制开天狗食月")
|
||
a.near(e.output_per_sec(), base_out * 2.0, 1e-6, "全产出 ×2")
|
||
a.near(e.click_value(), base_click * 10.0, 1e-6, "点击价值 ×10(独立瞬时区)")
|
||
# 点击仍能被计数(挑战计数只对 click_challenge 生效,这里不应污染挑战状态)
|
||
d.notify_click()
|
||
a.eq(int(d.challenge_clicks), 0, "非挑战事件不计数点击")
|
||
# 结束复位
|
||
var steps := int(float(data.event("ev_04_tiangou").duration_sec) / 0.1) + 2
|
||
for _i in steps:
|
||
d.tick(0.1, e)
|
||
a.near(e.click_value(), base_click, 1e-6, "点击瞬时区复位")
|
||
a.near(e.output_per_sec(), base_out, 1e-6, "产出瞬时区复位")
|
||
|
||
|
||
# ---- 阴兵闹饷:窗口内点击不足 → 随机班底产出 -50% × 60s ----
|
||
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")
|
||
var need := int(ev.p_clicks)
|
||
var r := d.force_start(e, "ev_02_naoxiang")
|
||
a.true_(bool(r.get("ok", false)), "强制开阴兵闹饷")
|
||
# 只点了一半
|
||
for _i in need / 2:
|
||
d.notify_click()
|
||
a.eq(int(d.challenge_clicks), need / 2, "点击计数 %d" % (need / 2))
|
||
a.eq(e.mutiny_gen_index, -1, "窗口未结束 → 尚无惩罚")
|
||
# 走完窗口(p_window_sec = 15s)
|
||
var steps := int(float(ev.p_window_sec) / 0.1) + 2
|
||
for _i in steps:
|
||
d.tick(0.1, e)
|
||
a.false_(d.is_busy(), "窗口结束 → 事件结束")
|
||
a.true_(e.mutiny_gen_index >= 0, "失败 → 哗变惩罚落到某个已解锁班底")
|
||
a.near(e.mutiny_mult, 1.0 - float(ev.p_penalty), 1e-9, "惩罚倍率 = 1 - p_penalty(-50%)")
|
||
a.near(e.mutiny_left, float(ev.p_penalty_dur_sec), 1e-6, "惩罚持续时间来自数据行")
|
||
# 惩罚到期自动清除
|
||
for _i in int(float(ev.p_penalty_dur_sec) / 0.1) + 2:
|
||
e.tick(0.1)
|
||
a.eq(e.mutiny_gen_index, -1, "惩罚到期 → 自动清除")
|
||
a.near(e.mutiny_mult, 1.0, 1e-9, "惩罚倍率复位")
|
||
|
||
|
||
# ---- 冤魂申诉:二选一(超度 = 阴德爆发 30s 量 / 吞噬 = 冤气) ----
|
||
func _test_choice(a, data: BalanceData) -> void:
|
||
var ev := data.event("ev_03_shensu")
|
||
# opt_a:超度
|
||
var d1 := EventDirector.new(data, 19)
|
||
var e1 := _new_econ(data)
|
||
var out1 := e1.output_per_sec()
|
||
d1.force_start(e1, "ev_03_shensu")
|
||
a.eq(d1.state, EventDirector.State.CHOOSING, "choice 事件进入 CHOOSING")
|
||
var before1 := e1.merit_run_total
|
||
var r1 := d1.choose(e1, 0)
|
||
a.true_(bool(r1.get("ok", false)), "选 opt_a(超度)")
|
||
a.near(float(r1.get("merit", 0.0)), out1 * float(ev.opt_a_param), 1e-6,
|
||
"超度 = 产出/s × 30s 量")
|
||
a.near(e1.merit_run_total, before1 + out1 * float(ev.opt_a_param), 1e-6,
|
||
"阴德进本轮累计口径(可推殿门)")
|
||
a.near(e1.wrath, 0.0, 1e-9, "超度不给冤气")
|
||
|
||
# opt_b:吞噬
|
||
var d2 := EventDirector.new(data, 23)
|
||
var e2 := _new_econ(data)
|
||
d2.force_start(e2, "ev_03_shensu")
|
||
var r2 := d2.choose(e2, 1)
|
||
a.true_(bool(r2.get("ok", false)), "选 opt_b(吞噬)")
|
||
# 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, "冤气入账")
|
||
a.eq(e2.devour_count, 1, "吞噬计数 +1")
|
||
|
||
|
||
# ---- 超时默认 = tune_event_timeout_default(opt_a 超度) ----
|
||
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")
|
||
a.eq(default_opt, "opt_a", "超时默认 = opt_a([B§7])")
|
||
d.force_start(e, "ev_03_shensu")
|
||
var before := e.merit_run_total
|
||
# 决策窗 45s 走完
|
||
var steps := int(float(data.event("ev_03_shensu").duration_sec) / 0.1) + 2
|
||
for _i in steps:
|
||
d.tick(0.1, e)
|
||
a.false_(d.is_busy(), "超时 → 事件结束")
|
||
a.true_(e.merit_run_total > before, "超时走默认分支(超度 → 阴德入账)")
|
||
a.near(e.wrath, 0.0, 1e-9, "超时不吞冤气")
|