extends RefCounted # 用例:W3 相机 rig 数据行 + CameraDef 纯数学(camera.csv / balance_tuning.csv) # 说明:这些都是纯函数/静态函数,故可在无头 SceneTree 下直接跑,不需要 Godot 渲染。 func run(a, data: BalanceData) -> void: _test_rows(a, data) _test_geometry(a, data) _test_kinematics(a, data) # ---- 数据行:每殿恰一行 + 参数合法 ---- 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) a.true_(c != null, "殿 %s 有 camera 行" % h.id) if c == null: continue a.true_(c.band_screens >= 1.0 and c.band_screens <= 6.0, "%s.band_screens 在 [1,6]" % h.id) a.true_(c.arena_zoom > c.idle_zoom, "%s:殿王战推近缩放 > 巡游缩放" % h.id) a.true_(c.layer_far_scroll < c.layer_mid_scroll, "%s:远景比中景慢" % h.id) a.true_(c.layer_near_scroll <= c.layer_fore_scroll, "%s:前景不慢于近景" % h.id) # 全局标量已进 balance_tuning(缺失会被 tune_num 记 load_errors,此处兜底断言) a.true_(data.has_tune("tune_cam_peek_max_px"), "tune_cam_peek_max_px 存在") a.true_(data.has_tune("tune_cam_peek_spring"), "tune_cam_peek_spring 存在") a.true_(data.has_tune("tune_cam_peek_damp"), "tune_cam_peek_damp 存在") a.true_(data.has_tune("tune_cam_follow_tau"), "tune_cam_follow_tau 存在") a.true_(data.has_tune("tune_cam_zoom_tau"), "tune_cam_zoom_tau 存在") a.true_(data.has_tune("tune_boss_preview_min"), "tune_boss_preview_min 存在") # ---- 几何:band 排布 + 进度焦点锚定 ---- func _test_geometry(a, data: BalanceData) -> void: var cams: Array = [] for h in data.halls: cams.append(data.camera_for_hall(h.id)) var rng := BandWorldRoot.compute_ranges(cams) a.eq(rng.size(), 10, "ranges 十段") a.near(float(rng[0].x), 0.0, 1e-9, "首段左端 = 0") var total := 0.0 for c in cams: total += (c as CameraDef).band_width() a.near(float(rng[9].y), total, 1e-6, "末段右端 = Σ band 宽度") var w0 := float(rng[0].y) - float(rng[0].x) a.near(BandWorldRoot.front_x_of(rng, 0, 0.0), 0.0, 1e-9, "H1 进度 0 → 段首") a.near(BandWorldRoot.front_x_of(rng, 0, 0.5), w0 * 0.5, 1e-6, "H1 进度 0.5 → 段中") a.near(BandWorldRoot.front_x_of(rng, 0, 1.0), w0, 1e-6, "H1 进度 1.0 → 段尾") # 越界夹逼(保底:不会算出段外坐标) a.near(BandWorldRoot.front_x_of(rng, 99, 0.5), float(rng[9].x) + ( float(rng[9].y) - float(rng[9].x)) * 0.5, 1e-6, "殿序越界夹到末段") a.near(BandWorldRoot.front_x_of(rng, -5, 0.5), w0 * 0.5, 1e-6, "负序夹到首段") # arena 锚点在段内偏殿门一侧 var anchor := BandWorldRoot.arena_anchor_x(rng, 0) a.true_(anchor > 0.0 and anchor < w0, "arena 锚点落在段内") a.near(anchor, w0 * 0.65, 1e-6, "arena 锚点 = 段内 65%(构图常量)") # 焦点锚定:focus=0.66 时相机比 front 靠左 0.16 屏宽 var cam_x := CameraDef.camera_x_for_front(1000.0, 1.0, 0.66, 1280.0) a.near(cam_x, 1000.0 - 1280.0 * 0.16, 1e-6, "焦点 0.66 → 相机左移 0.16 屏") var cam_x2 := CameraDef.camera_x_for_front(1000.0, 2.0, 0.66, 1280.0) a.near(cam_x2, 1000.0 - 1280.0 * 0.16 / 2.0, 1e-6, "缩放 2× 时位移减半") # 视口宽度与夹逼 a.near(CameraDef.view_width(2.0, 1280.0), 640.0, 1e-9, "缩放 2× → 可视宽 640") a.near(CameraDef.clamp_camera_x(100.0, 1.0, 400.0, 1280.0), 200.0, 1e-9, "世界比视口窄 → 居中") a.near(CameraDef.clamp_camera_x(-9999.0, 1.0, 5000.0, 1280.0), 640.0, 1e-9, "左侧越界夹到半屏") a.near(CameraDef.clamp_camera_x(9999.0, 1.0, 5000.0, 1280.0), 4360.0, 1e-9, "右侧越界夹到 world_w - 半屏") # 确定性剪影布局(同一数据行永远同一张图) a.near(HallBandWorld.silhouette_x(0, 1000.0), 1000.0 / 6.0, 1e-6, "剪影 0 位置") a.near(HallBandWorld.silhouette_x(4, 1000.0), 1000.0 / 6.0 * 5.0, 1e-6, "剪影 4 位置") # ---- 运动学:弹簧回弹 / 指数平滑 / 偷看夹逼 ---- 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) # 偷看夹逼 a.near(CameraDef.clamp_peek(9999.0, max_px), max_px, 1e-9, "偷看上限夹逼") a.near(CameraDef.clamp_peek(-9999.0, max_px), -max_px, 1e-9, "偷看下限夹逼") # 弹簧:给 200px 初位移,2 秒后应回到 ~0(欠阻尼也收敛) var x := 200.0 var v := 0.0 for _i in 120: var s := CameraDef.spring_step(x, v, 1.0 / 60.0, k, c) x = s.x v = s.y a.true_(absf(x) < 1.0, "松手 2s 后偷看位移回到 ≈0(got %.4f)" % x) a.true_(absf(v) < 5.0, "速度同时收敛(got %.4f)" % v) # 指数平滑:tau=0.35 → 一步后应走到目标的一部分且单调 var y := 0.0 var prev := 0.0 for _i in 5: y = CameraDef.exp_smooth(y, 100.0, 0.1, 0.35) a.true_(y >= prev, "平滑单调不回头") prev = y a.true_(y > 20.0 and y < 100.0, "0.5s 后走到目标的 20%%-100%% 之间(got %.2f)" % y) a.near(CameraDef.exp_smooth(0.0, 100.0, 0.1, 0.0), 100.0, 1e-9, "tau=0 → 瞬时")