Files
mtgodot-poc/project/mob_death_drop_test.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

195 lines
6.2 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:
return {"vid": vid, "pos": Vector3.ZERO, "race": 101, "name": "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!")
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.queue_free()
mount.queue_free()
client.queue_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.queue_free()
player.queue_free()
dead_mob.queue_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.queue_free()
world.queue_free()
player.queue_free()
mount.queue_free()
client.queue_free()