Files
mtgodot-poc/project/animation_blend_test.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

85 lines
3.3 KiB
GDScript

extends SceneTree
# Regression for locomotion crossfades: blending world-space joint endpoints
# briefly shortened or stretched limbs. Parent/child segment lengths must stay
# close to the two endpoint poses while wait/run transitions are in progress.
const PlayerView = preload("res://ui/player_view.gd")
var failures: Array[String] = []
var checked_segments := 0
func _initialize() -> void:
call_deferred("run")
func _check(ok: bool, message: String) -> void:
if not ok:
failures.append(message)
printerr("FAIL: ", message)
func _pose(anim: Node, skeleton: Skeleton3D) -> Dictionary:
var result := {}
for bone in skeleton.get_bone_count():
var data: Dictionary = anim.call("get_effect_bone_pose", skeleton.get_bone_name(bone))
if data.has("transform"):
result[bone] = (data["transform"] as Transform3D).origin
return result
func _segments(pose: Dictionary, skeleton: Skeleton3D) -> Dictionary:
var result := {}
for bone in skeleton.get_bone_count():
var parent := skeleton.get_bone_parent(bone)
if parent >= 0 and pose.has(bone) and pose.has(parent):
var length := (pose[bone] as Vector3).distance_to(pose[parent] as Vector3)
if length > 0.01:
result[bone] = length
return result
func run() -> void:
OS.set_environment("MTGODOT_GPUSKIN", "0")
for race in 8:
var view := PlayerView.new()
root.add_child(view)
if not view.build(AssetRoot.path(), race):
_check(false, "race %d failed to build" % race)
view.queue_free()
continue
view.model.set("lod_enabled", false)
view.anim.set("blend_time", 0.15)
await process_frame
view.anim.set("time", minf(0.25, float(view.anim.call("get_duration")) * 0.5))
var skeleton := view.model.get_node("Skeleton3D") as Skeleton3D
var start := _segments(_pose(view.anim, skeleton), skeleton)
view.set_anim_state("run")
var transition: Array[Dictionary] = []
for frame in 12:
await process_frame
transition.append(_segments(_pose(view.anim, skeleton), skeleton))
var finish := _segments(_pose(view.anim, skeleton), skeleton)
for bone in start:
if not finish.has(bone):
continue
# Terminal *Nub helpers are zero-area animation handles and carry no
# skinned child segment. Some clips intentionally collapse them.
if String(skeleton.get_bone_name(bone)).ends_with("Nub"):
continue
var endpoint_min: float = minf(float(start[bone]), float(finish[bone]))
var endpoint_max: float = maxf(float(start[bone]), float(finish[bone]))
# Sub-5 cm helpers/finger tips may carry authored local translation
# animation and do not explain whole-character deformation. Keep this
# gate focused on visible limb and torso segments.
if endpoint_min < 5.0:
continue
for frame in transition.size():
if not transition[frame].has(bone):
continue
var value: float = transition[frame][bone]
checked_segments += 1
_check(value >= endpoint_min * 0.75 and value <= endpoint_max * 1.25,
"race %d bone %s deforms in blend frame %d: %.3f not near %.3f..%.3f" %
[race, skeleton.get_bone_name(bone), frame, value, endpoint_min, endpoint_max])
view.queue_free()
await process_frame
_check(checked_segments > 1000, "blend regression did not inspect enough skinned segments")
print("animation_blend_test: failures=", failures.size(), " segments=", checked_segments,
" (8 races, wait -> run)")
quit(0 if failures.is_empty() else 1)