- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
199 lines
6.4 KiB
GDScript
199 lines
6.4 KiB
GDScript
extends SceneTree
|
|
|
|
const NetWorld = preload("res://net_world.gd")
|
|
const GroundItems = preload("res://ui/ground_items.gd")
|
|
const PlayerController = preload("res://player_controller.gd")
|
|
|
|
class MockClient extends Node:
|
|
signal entity_spawned(e: Dictionary)
|
|
signal entity_despawned(vid: int)
|
|
signal entity_dead(vid: int)
|
|
signal entity_moved(vid: int)
|
|
signal entity_main_set(vid: int)
|
|
signal vitals_updated(vid: int)
|
|
signal vitals_changed(vid: int)
|
|
signal warp_requested(x: float, y: float)
|
|
signal ground_item_added(item: Dictionary)
|
|
signal ground_item_removed(vid: int)
|
|
signal chat(mode: int, text: String)
|
|
signal chat_bubble(vid: int, text: String)
|
|
signal damage(target_vid: int, attacker_vid: int, damage_val: int, flag: int)
|
|
signal whisper_received(author: String, text: String)
|
|
|
|
var _main_vid := 1
|
|
var picked_vids: Array[int] = []
|
|
|
|
func get_main_vid() -> int:
|
|
return _main_vid
|
|
|
|
func get_entity(vid: int) -> Dictionary:
|
|
# The ground item owner is the local player. Keep the fixture aligned with
|
|
# the 40250 ownership pre-check instead of accidentally testing rejection.
|
|
return {"vid": vid, "pos": Vector3.ZERO, "race": 101, "name": "Hero" if vid == _main_vid else "Wild Dog", "hp": 100, "max_hp": 100}
|
|
|
|
func get_ground_items() -> Array:
|
|
return []
|
|
|
|
func pickup_item(vid: int) -> bool:
|
|
picked_vids.append(vid)
|
|
return true
|
|
|
|
class MockWorld extends Node:
|
|
func sample_height(x: float, z: float) -> float:
|
|
return 5.5
|
|
|
|
func _init() -> void:
|
|
print("--- Running mob_death_drop_test ---")
|
|
test_mob_death_and_despawn()
|
|
test_player_controller_ignores_dead()
|
|
test_ground_items_coordinate_mapping()
|
|
print("mob_death_drop_test passed with 0 failures!")
|
|
await process_frame
|
|
await process_frame
|
|
call_deferred("quit", 0)
|
|
|
|
func test_mob_death_and_despawn() -> void:
|
|
var client = MockClient.new()
|
|
var mount = Node3D.new()
|
|
root.add_child(mount)
|
|
|
|
var net_world = NetWorld.new()
|
|
root.add_child(net_world)
|
|
net_world.setup(client, mount)
|
|
|
|
# Spawn a mob with tags
|
|
var mob_vid := 100
|
|
var mob_node := Node3D.new()
|
|
mob_node.name = "entity_%d" % mob_vid
|
|
var name_tag := Label3D.new()
|
|
name_tag.name = "NameTag"
|
|
name_tag.text = "Wild Dog"
|
|
mob_node.add_child(name_tag)
|
|
var shadow := Decal.new()
|
|
shadow.name = "Shadow"
|
|
mob_node.add_child(shadow)
|
|
mount.add_child(mob_node)
|
|
|
|
net_world._by_vid[mob_vid] = mob_node
|
|
net_world._make_hp_bar(mob_vid, {"hp": 100, "max_hp": 100, "name": "Wild Dog"})
|
|
net_world.set_target_vid(mob_vid)
|
|
net_world.set_hover_vid(mob_vid)
|
|
|
|
assert(net_world._hp_bars.has(mob_vid), "HP bar should exist initially")
|
|
assert(net_world._target_vid == mob_vid, "Target should be mob_vid")
|
|
assert(net_world._hover_vid == mob_vid, "Hover should be mob_vid")
|
|
assert(name_tag.visible == true, "NameTag should be visible initially")
|
|
|
|
# Trigger death
|
|
net_world._on_dead(mob_vid)
|
|
|
|
assert(not net_world._hp_bars.has(mob_vid), "HP bar should be removed immediately on death")
|
|
assert(name_tag.visible == false, "NameTag should be detached/hidden on death")
|
|
assert(shadow.visible == false, "Shadow should be hidden on death")
|
|
assert(bool(mob_node.get_meta("dead", false)) == true, "Mob should have dead meta")
|
|
assert(net_world._target_vid == 0, "Target should be cleared on death")
|
|
assert(net_world._hover_vid == 0, "Hover should be cleared on death")
|
|
|
|
# Trigger despawn
|
|
net_world._on_despawn(mob_vid)
|
|
assert(not net_world._by_vid.has(mob_vid), "Mob should be erased from _by_vid on despawn")
|
|
print("OK: Mob death detach & despawn lifecycle verified")
|
|
|
|
net_world.free()
|
|
mount.free()
|
|
client.free()
|
|
|
|
func test_player_controller_ignores_dead() -> void:
|
|
var pc = PlayerController.new()
|
|
var player = Node3D.new()
|
|
player.position = Vector3(0, 0, 0)
|
|
root.add_child(player)
|
|
pc.player = player
|
|
|
|
var dead_mob = Node3D.new()
|
|
dead_mob.position = Vector3(0, 0, 2)
|
|
dead_mob.set_meta("dead", true)
|
|
root.add_child(dead_mob)
|
|
|
|
pc.pickables.clear()
|
|
pc.pickables.append(dead_mob)
|
|
|
|
# Test collision check (line 317)
|
|
var blocked: bool = pc._actor_blocked(Vector3(0, 0, 1.8))
|
|
assert(not blocked, "Dead mob should not block player movement")
|
|
|
|
print("OK: PlayerController ignores dead mobs for collision and picking")
|
|
pc.free()
|
|
player.free()
|
|
dead_mob.free()
|
|
|
|
func test_ground_items_coordinate_mapping() -> void:
|
|
# Set map base position (e.g. Map 1: 400000 cm X, 300000 cm Y)
|
|
MapCoord.set_base(Vector2(400000, 300000))
|
|
|
|
var client = MockClient.new()
|
|
var mount = Node3D.new()
|
|
root.add_child(mount)
|
|
|
|
var world = MockWorld.new()
|
|
root.add_child(world)
|
|
|
|
var player = Node3D.new()
|
|
player.position = Vector3(100, 5.5, 200) # Player is at world (100, 5.5, 200)
|
|
root.add_child(player)
|
|
|
|
var gi = GroundItems.new()
|
|
root.add_child(gi)
|
|
|
|
gi.setup(client, mount, func() -> Node: return player, null, null,
|
|
Callable(), "", func() -> Node: return world)
|
|
|
|
# Net position from server:
|
|
# Metin2 global: X = 410000 cm, Y = 320000 cm (local world should be: X = 100m, Z = 200m)
|
|
# M2Client position_to_godot produces:
|
|
# net.x = 4100.0, net.y = 0.0, net.z = -3200.0
|
|
var net_drop_pos := Vector3(4100.0, 0.0, -3200.0)
|
|
var drop_vid := 9999
|
|
var drop_dict := {
|
|
"vid": drop_vid,
|
|
"vnum": 1, # Yang / Gold
|
|
"owner": "Hero",
|
|
"pos": net_drop_pos
|
|
}
|
|
|
|
gi._on_added(drop_dict)
|
|
|
|
assert(gi._by_vid.has(drop_vid), "Ground item should be registered in _by_vid")
|
|
var drop_node: Node3D = gi._by_vid[drop_vid]
|
|
assert(drop_node != null, "Drop node should exist")
|
|
|
|
# Check world coordinate translation
|
|
assert(is_equal_approx(drop_node.position.x, 100.0), "X should be translated to 100m")
|
|
assert(is_equal_approx(drop_node.position.z, 200.0), "Z should be translated to 200m")
|
|
# Check terrain height snapping (+0.1m above 5.5m = 5.6m)
|
|
assert(is_equal_approx(drop_node.position.y, 5.6), "Y should be snapped to terrain height + 0.1m")
|
|
|
|
# Check item name for vnum 1
|
|
var tag := drop_node.get_node("tag") as Label3D
|
|
assert(tag != null and tag.text == "Yang", "Vnum 1 name should be Yang")
|
|
|
|
# Test pickup with player near (distance = 0.1m < PICKUP_RANGE 3.0m)
|
|
var picked_vid := gi.try_pickup()
|
|
assert(picked_vid == drop_vid, "try_pickup should pick nearby item")
|
|
assert(client.picked_vids.has(drop_vid), "client.pickup_item should be called")
|
|
|
|
# Test remove
|
|
gi._on_removed(drop_vid)
|
|
assert(not gi._by_vid.has(drop_vid), "Item should be removed from _by_vid")
|
|
|
|
print("OK: Ground items coordinate mapping, height snapping, Yang naming, and pickup verified")
|
|
|
|
# Reset MapCoord base
|
|
MapCoord.set_base(Vector2.ZERO)
|
|
|
|
gi.free()
|
|
world.free()
|
|
player.free()
|
|
mount.free()
|
|
client.free()
|