- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
85 lines
3.3 KiB
GDScript
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)
|