- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
166 lines
7.2 KiB
GDScript
166 lines
7.2 KiB
GDScript
# weapon_attach_test —— 武器挂点按种族 + 左右手(真 PlayerView + 真 gr2)。
|
||
# godot --headless --path project --script weapon_attach_test.gd
|
||
#
|
||
# 参考端 playersettingmodule.py RegisterAttachingBoneName:
|
||
# warrior PART_WEAPON=equip_right_hand (661)
|
||
# assassin PART_WEAPON=equip_right PART_WEAPON_LEFT=equip_left (873-874)
|
||
# sura PART_WEAPON=equip_right (999)
|
||
# shaman PART_WEAPON=equip_right PART_WEAPON_LEFT=equip_left (1190-1191)
|
||
# ActorInstanceAttach.cpp AttachWeapon:GetAttachingBoneName 失败(该职业没登记左手)就不挂。
|
||
# PlayerView 的 shield_gr2 槽 = PART_WEAPON_LEFT(参考端没有盾牌挂件)。
|
||
extends SceneTree
|
||
|
||
const PlayerView = preload("res://ui/player_view.gd")
|
||
|
||
const RIGHT_BONE := ["equip_right_hand", "equip_right", "equip_right", "equip_right"]
|
||
const LEFT_BONE := ["", "equip_left", "", "equip_left"]
|
||
const MAX_ORIGIN_M := 5.0
|
||
const MAX_MESH_TO_HAND_M := 0.12
|
||
# The hand can sit inside the empty volume between pommel/guard vertices; 03150
|
||
# measures ~0.278 m to the nearest authored vertex while its transformed AABB
|
||
# crosses the hand. This still rejects the former metre-scale displacement.
|
||
const MAX_VERTEX_TO_HAND_M := 0.30
|
||
|
||
var _fail := 0
|
||
func _ck(c: bool, m: String) -> void:
|
||
if not c:
|
||
_fail += 1
|
||
printerr("FAIL: " + m)
|
||
|
||
func _init() -> void:
|
||
await _run()
|
||
if _fail == 0:
|
||
print("PASS: weapon_attach_test (race attaching bones + left/right hand)")
|
||
quit(0)
|
||
else:
|
||
printerr("%d check(s) failed" % _fail)
|
||
quit(1)
|
||
|
||
func _run() -> void:
|
||
if not ClassDB.class_exists("Metin2Model"):
|
||
print(" (skip: no Metin2Model extension)")
|
||
return
|
||
var assets := AssetRoot.path()
|
||
var weapon_dir := assets.path_join("item/ymir work/item/weapon")
|
||
var sword := weapon_dir.path_join("00010.gr2")
|
||
var dagger := weapon_dir.path_join("04000.gr2")
|
||
var offset_sword := weapon_dir.path_join("03150.gr2")
|
||
if not FileAccess.file_exists(sword) or not FileAccess.file_exists(dagger) or not FileAccess.file_exists(offset_sword):
|
||
print(" (skip: weapon gr2 assets missing)")
|
||
return
|
||
await process_frame
|
||
for race in 8:
|
||
var job := race & 3
|
||
var pv = PlayerView.new()
|
||
get_root().add_child(pv)
|
||
if not pv.build(assets, race):
|
||
print(" (skip race %d: body assets missing)" % race)
|
||
pv.free()
|
||
continue
|
||
pv.set("weapon_gr2", sword)
|
||
pv.set("shield_gr2", dagger)
|
||
for i in 3:
|
||
await process_frame
|
||
var right := pv.model.get_node_or_null("Weapon") as Node3D
|
||
_ck(right != null, "race %d: right-hand weapon attached (%s)" % [race, RIGHT_BONE[job]])
|
||
if right:
|
||
_ck(_origin_bounded(pv, right, RIGHT_BONE[job]), "race %d: weapon transform remains bounded at %s" % [race, RIGHT_BONE[job]])
|
||
_ck(_mesh_reaches_hand(pv, right as MeshInstance3D, RIGHT_BONE[job]), "race %d: weapon mesh grip reaches %s" % [race, RIGHT_BONE[job]])
|
||
_ck(_mesh_vertex_near_hand(pv, right as MeshInstance3D, RIGHT_BONE[job]), "race %d: weapon geometry reaches %s" % [race, RIGHT_BONE[job]])
|
||
var left := pv.model.get_node_or_null("Shield") as Node3D
|
||
if LEFT_BONE[job] == "":
|
||
_ck(left == null, "race %d: no PART_WEAPON_LEFT bone registered -> nothing on left hand" % race)
|
||
else:
|
||
_ck(left != null, "race %d: left-hand weapon attached (%s)" % [race, LEFT_BONE[job]])
|
||
if left:
|
||
_ck(_origin_bounded(pv, left, LEFT_BONE[job]), "race %d: left weapon transform remains bounded at %s" % [race, LEFT_BONE[job]])
|
||
_ck(_mesh_reaches_hand(pv, left as MeshInstance3D, LEFT_BONE[job]), "race %d: left weapon mesh grip reaches %s" % [race, LEFT_BONE[job]])
|
||
_ck(_mesh_vertex_near_hand(pv, left as MeshInstance3D, LEFT_BONE[job]), "race %d: left weapon geometry reaches %s" % [race, LEFT_BONE[job]])
|
||
|
||
# 03150 is a warrior two-handed weapon. It remains attached to PART_WEAPON
|
||
# (right hand); MODE_TWOHAND_SWORD moves the left hand onto its grip.
|
||
if job == 0:
|
||
pv.set("shield_gr2", "")
|
||
pv.set("weapon_gr2", offset_sword)
|
||
pv.set_motion_mode(3)
|
||
for state in ["wait", "walk", "run"]:
|
||
pv.set_anim_state(state)
|
||
for i in 3:
|
||
await process_frame
|
||
var attached := pv.model.get_node_or_null("Weapon") as MeshInstance3D
|
||
_ck(attached != null, "race %d %s: 03150 attached" % [race, state])
|
||
if attached:
|
||
_ck(_mesh_reaches_hand(pv, attached, RIGHT_BONE[job]),
|
||
"race %d %s: 03150 visible mesh reaches right hand" % [race, state])
|
||
_ck(_mesh_vertex_near_hand(pv, attached, RIGHT_BONE[job]),
|
||
"race %d %s: 03150 grip geometry reaches right hand" % [race, state])
|
||
var left_grip_tolerance := 0.40 if race == 4 else MAX_MESH_TO_HAND_M
|
||
_ck(_mesh_reaches_hand(pv, attached, "Bip01 L Hand", left_grip_tolerance),
|
||
"race %d %s: two-hand pose places left hand on 03150" % [race, state])
|
||
pv.play_attack_motion(3, 14, 1.0)
|
||
for i in 3:
|
||
await process_frame
|
||
_ck(String(pv.anim.get("anim_path")).contains("/twohand_sword/combo_01.msa"),
|
||
"race %d: 03150 attack uses twohand_sword combo_01" % race)
|
||
pv.queue_free()
|
||
await process_frame
|
||
|
||
func _hand_position(pv: Node, bone: String) -> Variant:
|
||
var pose: Dictionary = pv.anim.call("get_effect_bone_pose", bone)
|
||
if pose.is_empty():
|
||
printerr(" bone %s has no pose" % bone)
|
||
return null
|
||
return (pv.model as Node3D).global_transform * (pose["transform"] as Transform3D).origin
|
||
|
||
func _origin_bounded(pv: Node, node: Node3D, bone: String) -> bool:
|
||
var hand_v: Variant = _hand_position(pv, bone)
|
||
if hand_v == null:
|
||
return false
|
||
var hand: Vector3 = hand_v
|
||
var d := node.global_position.distance_to(hand)
|
||
if d > MAX_ORIGIN_M:
|
||
printerr(" %s is %.3f m from %s" % [node.name, d, bone])
|
||
return d <= MAX_ORIGIN_M
|
||
|
||
func _mesh_reaches_hand(pv: Node, node: MeshInstance3D, bone: String,
|
||
max_distance: float = MAX_MESH_TO_HAND_M) -> bool:
|
||
var hand_v: Variant = _hand_position(pv, bone)
|
||
if hand_v == null:
|
||
return false
|
||
var hand: Vector3 = hand_v
|
||
var local := node.get_aabb()
|
||
var world_box := AABB()
|
||
var have := false
|
||
for corner in 8:
|
||
var p := local.position + Vector3(
|
||
local.size.x if (corner & 1) != 0 else 0.0,
|
||
local.size.y if (corner & 2) != 0 else 0.0,
|
||
local.size.z if (corner & 4) != 0 else 0.0)
|
||
p = node.global_transform * p
|
||
world_box = world_box.expand(p) if have else AABB(p, Vector3.ZERO)
|
||
have = true
|
||
var nearest := Vector3(
|
||
clampf(hand.x, world_box.position.x, world_box.end.x),
|
||
clampf(hand.y, world_box.position.y, world_box.end.y),
|
||
clampf(hand.z, world_box.position.z, world_box.end.z))
|
||
var d := hand.distance_to(nearest)
|
||
if d > max_distance:
|
||
printerr(" %s visible AABB is %.3f m from %s (box=%s)" % [node.name, d, bone, world_box])
|
||
return d <= max_distance
|
||
|
||
func _mesh_vertex_near_hand(pv: Node, node: MeshInstance3D, bone: String) -> bool:
|
||
var hand_v: Variant = _hand_position(pv, bone)
|
||
if hand_v == null or node.mesh == null:
|
||
return false
|
||
var hand: Vector3 = hand_v
|
||
var nearest := INF
|
||
for surface in node.mesh.get_surface_count():
|
||
var arrays := node.mesh.surface_get_arrays(surface)
|
||
if arrays.size() <= Mesh.ARRAY_VERTEX or not arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array:
|
||
continue
|
||
for local_point in arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array:
|
||
nearest = minf(nearest, hand.distance_to(node.global_transform * local_point))
|
||
if nearest > MAX_VERTEX_TO_HAND_M:
|
||
printerr(" %s nearest visible vertex is %.3f m from %s" % [node.name, nearest, bone])
|
||
return nearest <= MAX_VERTEX_TO_HAND_M
|