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()