187 lines
8.5 KiB
GDScript
187 lines
8.5 KiB
GDScript
# 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")
|