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, "超时不吞冤气")