# 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") const TextTailArrange = preload("res://text_tail_arrange.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, owner := "", item_color: Variant = null): ground[vid] = {"vid": vid, "vnum": vnum, "pos": pos, "owner": owner} if item_color != null: ground[vid]["item_color"] = item_color 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") # --- §8.8 seam ⑩:多条名条投影后去重叠(ArrangeTextTail item 块)--------------- # 独立的 mount / FakeClient,避免与上面的 gi 抢同名 drop 节点。 var mount2 := Node3D.new() var fc2 := FakeClient.new() get_root().add_child(mount2) get_root().add_child(fc2) var cam := Camera3D.new() get_root().add_child(cam) cam.position = Vector3(0, 3, 6) cam.look_at(Vector3.ZERO, Vector3.UP) cam.current = true var gi2: Node = GroundItems.new() get_root().add_child(gi2) gi2.setup(fc2, mount2, func() -> Node: return pl, fp, null, func() -> Camera3D: return cam) # 单条:名条留默认局部 y。 fc2.add_ground(201, 27, Vector3(0, 0, 0)) await process_frame gi2._process(0.0) var t201 := mount2.get_node("drop_201").get_node("tag") as Label3D _ck(absf(t201.position.y - gi2.TAG_BASE_Y) < 0.0001, "单条名条 -> 留默认局部 y 0.75") # 同位两条:靠前的(vid 小,m_ItemTextTailList 先)被顶下去,靠后的留原位。 fc2.add_ground(202, 28, Vector3(0, 0, 0)) await process_frame gi2._process(0.0) var t202 := mount2.get_node("drop_202").get_node("tag") as Label3D _ck(t201.position.y < gi2.TAG_BASE_Y - 0.0001, "重叠 -> 靠前名条被下推(世界 y 变小)") _ck(absf(t202.position.y - gi2.TAG_BASE_Y) < 0.0001, "重叠 -> 靠后名条留默认") # 被下推量 = (h + gap) px 折世界:(15 + 5) * pixel_size(0.005) = 0.1。 _ck(absf(t201.position.y - (gi2.TAG_BASE_Y - (gi2.TAG_LINE_H_PX + TextTailArrange.PUSH_GAP) * t201.pixel_size)) < 0.0001, "下推量 = (TAG_LINE_H_PX + PUSH_GAP) * pixel_size = 0.1") _ck(absf(t201.position.y - t202.position.y) > 0.0001, "两名条错开后局部 y 不同") # 远离:把 202 挪远,重跑 -> 两条都回默认。 mount2.get_node("drop_202").position = Vector3(-40, 0, -2) gi2._process(0.0) _ck(absf(t201.position.y - gi2.TAG_BASE_Y) < 0.0001, "拉开距离 -> 201 名条回默认") _ck(absf(t202.position.y - gi2.TAG_BASE_Y) < 0.0001, "拉开距离 -> 202 名条回默认") # --- §8.8 seam ⑩ 末尾:owner 拆成独立 CENTER 黄字 Label3D(SetItemTextTailOwner) # + 名字实例下移 15px(ArrangeTextTail :200);框仍 yEnd += ownerH + 4(增量 125)--- var mount3 := Node3D.new() var fc3 := FakeClient.new() get_root().add_child(mount3) get_root().add_child(fc3) var gi3: Node = GroundItems.new() get_root().add_child(gi3) gi3.setup(fc3, mount3, func() -> Node: return pl, fp, null, func() -> Camera3D: return cam) fc3.add_ground(301, 27, Vector3(0, 0, 0)) # 靠前、无 owner var owner_color := Color(0.25, 0.75, 1.0) fc3.add_ground(302, 28, Vector3(0, 0, 0), "Alice", owner_color) # 靠后、有 owner await process_frame gi3._process(0.0) var d301 := mount3.get_node("drop_301") var d302 := mount3.get_node("drop_302") var t301 := d301.get_node("tag") as Label3D var t302 := d302.get_node("tag") as Label3D # owner 拆成独立 owner_tag:名字实例只放名字,owner_tag 放 "Alice" + "'s"(黄 / CENTER)。 _ck(t302.text == "掉落28", "名字实例只放名字(owner 后缀不再拼串,:723)") _ck(d301.get_node_or_null("owner_tag") == null, "无 owner 的掉落不建 owner_tag(:741-745)") var o302 := d302.get_node_or_null("owner_tag") as Label3D _ck(o302 != null, "有 owner 的掉落建独立 owner_tag(:715)") _ck(o302.text == "Alice's", "owner_tag 文本 = owner + strOwnership(:722 字面 \"'s\")") _ck(o302.horizontal_alignment == HORIZONTAL_ALIGNMENT_CENTER, "owner_tag CENTER 对齐(:727)") _ck(o302.modulate.is_equal_approx(owner_color), "owner_tag 使用道具品阶色(:729 pInsertTextTail->Color)") # owner 名条框:无字体度量时 name_h = owner_h = 15 -> yEnd = 15 + 15 + 4 = 34(增量 125 不变)。 # 靠前 301(无 owner)被顶 (34 + PUSH_GAP) px 折世界:(34 + 5) * 0.005 = 0.195。 var push_owner: float = (gi3.TAG_LINE_H_PX * 2.0 + 4.0 + TextTailArrange.PUSH_GAP) * t301.pixel_size _ck(absf(t301.position.y - (gi3.TAG_BASE_Y - push_owner)) < 0.0001, "靠前名条被带 owner 的框顶 (34+5)*px = 0.195") # 靠后 302 未被推:owner 行落基点、名字行下移 15px(:195 owner 在 y、:200 名字在 y+15)。 _ck(absf(o302.position.y - gi3.TAG_BASE_Y) < 0.0001, "owner_tag 落尾标基点(:195)") _ck(absf(t302.position.y - (gi3.TAG_BASE_Y - gi3.OWNER_NAME_GAP_PX * t302.pixel_size)) < 0.0001, "名字实例在 owner 行下移 15px(:200 y + 15.0f)") # --- strOwnership 经 setup(possessive) 注入(SetItemTextTailOwner :722 本地化语素)--- var mount4 := Node3D.new() var fc4 := FakeClient.new() get_root().add_child(mount4) get_root().add_child(fc4) var gi4: Node = GroundItems.new() get_root().add_child(gi4) gi4.setup(fc4, mount4, func() -> Node: return pl, fp, null, func() -> Camera3D: return cam, "@") fc4.add_ground(401, 27, Vector3(0, 0, 0), "Bob") await process_frame var o401 := mount4.get_node("drop_401").get_node_or_null("owner_tag") as Label3D _ck(o401 != null and o401.text == "Bob@", "setup(possessive=\"@\") -> owner_tag 文本 = \"Bob@\"") # --- 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")