# 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