# p2b_test —— P2 续:地面掉落物 + PlayerView(真模型)headless 自检。 # godot --headless --path project --script p2b_test.gd extends SceneTree const GroundItems = preload("res://ui/ground_items.gd") const PlayerView = preload("res://ui/player_view.gd") class FakeClient extends Node: signal ground_item_added(item: Dictionary) signal ground_item_removed(vid: int) var ground := {} var picked := [] func get_ground_items() -> Array: return ground.values() func pickup_item(vid) -> bool: picked.append(vid); return true func add_ground(vid, vnum, pos): ground[vid] = {"vid": vid, "vnum": vnum, "pos": pos} ground_item_added.emit(ground[vid]) func del_ground(vid): ground.erase(vid); ground_item_removed.emit(vid) class FakeProto extends Node: func item(vnum: int) -> Dictionary: return {"vnum": vnum, "name": "N%d" % vnum, "locale_name": "掉落%d" % vnum} var _fail := 0 func _ck(c: bool, m: String) -> void: if not c: _fail += 1 printerr("FAIL: " + m) func _init() -> void: _run() if _fail == 0: print("PASS: p2b_test (ground items + player view)") quit(0) else: printerr("%d check(s) failed" % _fail) quit(1) func _run() -> void: # --- ground items --- var mount := Node3D.new() var fc := FakeClient.new() var fp := FakeProto.new() var pl := Node3D.new() get_root().add_child(mount) get_root().add_child(fc) get_root().add_child(fp) get_root().add_child(pl) pl.position = Vector3.ZERO var gi: Node = GroundItems.new() get_root().add_child(gi) gi.setup(fc, mount, func() -> Node: return pl, fp, null) fc.add_ground(101, 27, Vector3(1, 0, 1)) # 距玩家 ~1.4m(范围内) fc.add_ground(102, 28, Vector3(20, 0, 0)) # 远 await process_frame _ck(mount.get_child_count() == 2, "2 drop nodes spawned") _ck(mount.get_node_or_null("drop_101") != null, "drop_101 present") var tag := mount.get_node("drop_101").get_node("tag") as Label3D _ck(tag.text == "掉落27", "drop name from proto") # 拾取最近的(101 在范围,102 不在) var got: int = gi.try_pickup() _ck(got == 101, "try_pickup -> nearest 101, got %d" % got) _ck(fc.picked == [101], "client.pickup_item(101) called") fc.del_ground(101) await process_frame _ck(mount.get_node_or_null("drop_101") == null, "drop_101 freed on removed") _ck(mount.get_child_count() == 1, "1 drop left") # 远处的捡不到 _ck(gi.try_pickup() == 0, "far item not picked") # --- player view(有真资产才建)--- var assets := AssetRoot.path() if not DirAccess.dir_exists_absolute(assets.path_join("PC/ymir work/pc/warrior")): print(" (skip PlayerView: no PC assets)") return if not ClassDB.class_exists("Metin2Model"): print(" (skip PlayerView: extension not built)") return var pv: Node3D = PlayerView.new() get_root().add_child(pv) var ok: bool = pv.build(assets, 0) # race 0 = warrior male _ck(ok, "PlayerView.build(race 0) ok") _ck(pv.model != null, "PlayerView has Metin2Model") _ck(pv.motion_dir.ends_with("warrior/general"), "motion_dir -> warrior/general (%s)" % pv.motion_dir) # 转发 set pv.set("weapon_gr2", "/tmp/fake_weapon.gr2") _ck(String(pv.model.get("weapon_gr2")) == "/tmp/fake_weapon.gr2", "PlayerView forwards weapon_gr2 to model") pv.set_anim_state("run") await process_frame if pv.anim: _ck(String(pv.anim.get("anim_path")).ends_with("run.msa"), "set_anim_state('run') -> run.msa")