Files
game-code-4/tests/unit/cases/test_camera_data.gd
T
程基岩(engineering-lead) c534e95458 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 待文策渊裁定)
2026-10-20 18:47:07 +08:00

112 lines
5.1 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 → 瞬时")