Files
mtgodot-poc/project/test_drop_and_cursor_parity.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

162 lines
5.6 KiB
GDScript

extends SceneTree
const CursorManager = preload("res://ui/cursor_manager.gd")
const GroundItems = preload("res://ui/ground_items.gd")
const PlayerController = preload("res://player_controller.gd")
var _fail := 0
func _ck(condition: bool, message: String) -> void:
if not condition:
_fail += 1
printerr("FAIL: " + message)
func _init() -> void:
print("--- Running test_drop_and_cursor_parity ---")
await _run()
preload("res://ui/ui_assets.gd").clear_cache()
if _fail == 0:
print("test_drop_and_cursor_parity passed with 0 failures!")
quit(0)
else:
printerr("%d failure(s)" % _fail)
quit(1)
func _run() -> void:
test_mapcoord_heading()
test_cursor_manager()
await test_ground_item_models_and_picking()
func test_mapcoord_heading() -> void:
print("Testing MapCoord heading alignment with 40250...")
_ck(is_equal_approx(MapCoord.heading_to_yaw(0.0), 0.0), "0 deg heading must be 0 yaw (South)")
_ck(is_equal_approx(MapCoord.heading_to_yaw(90.0), PI * 0.5), "90 deg heading must be PI/2 yaw (East)")
_ck(is_equal_approx(MapCoord.heading_to_yaw(180.0), PI), "180 deg heading must be PI yaw (North)")
_ck(is_equal_approx(MapCoord.heading_to_yaw(270.0), PI * 1.5), "270 deg heading must be 3PI/2 yaw (West)")
_ck(is_equal_approx(MapCoord.yaw_to_heading(0.0), 0.0), "0 yaw must be 0 heading")
_ck(is_equal_approx(MapCoord.yaw_to_heading(PI * 0.5), 90.0), "PI/2 yaw must be 90 heading")
_ck(is_equal_approx(MapCoord.yaw_to_heading(PI), 180.0), "PI yaw must be 180 heading")
print("OK: MapCoord heading parity verified")
func test_cursor_manager() -> void:
print("Testing CursorManager custom cursor registration...")
var cm := CursorManager.new()
get_root().add_child(cm)
var root_assets: String = AssetRoot.path() if AssetRoot.available() else ""
cm.setup(root_assets)
cm.set_cursor("NORMAL")
_ck(cm.get_cursor() == "NORMAL", "shape is NORMAL")
_ck(cm._godot_shape("NORMAL") == Input.CURSOR_ARROW, "godot shape is CURSOR_ARROW")
cm.set_cursor("ATTACK")
_ck(cm.get_cursor() == "ATTACK", "shape is ATTACK")
_ck(cm._godot_shape("ATTACK") == Input.CURSOR_CROSS, "godot shape is CURSOR_CROSS")
cm.set_cursor("PICK")
_ck(cm.get_cursor() == "PICK", "shape is PICK")
_ck(cm._godot_shape("PICK") == Input.CURSOR_CAN_DROP, "godot shape is CURSOR_CAN_DROP")
cm.set_cursor("TALK")
_ck(cm.get_cursor() == "TALK", "shape is TALK")
_ck(cm._godot_shape("TALK") == Input.CURSOR_POINTING_HAND, "godot shape is CURSOR_POINTING_HAND")
cm.set_cursor("DOOR")
_ck(cm.get_cursor() == "DOOR", "shape is DOOR")
_ck(cm._godot_shape("DOOR") == Input.CURSOR_HELP, "godot shape is CURSOR_HELP")
cm.set_cursor("CANT_GO")
_ck(cm.get_cursor() == "CANT_GO", "shape is CANT_GO")
_ck(cm._godot_shape("CANT_GO") == Input.CURSOR_FORBIDDEN, "godot shape is CURSOR_FORBIDDEN")
cm.set_cursor("CAMERA_ROTATE")
_ck(cm.get_cursor() == "CAMERA_ROTATE", "shape is CAMERA_ROTATE")
_ck(cm._godot_shape("CAMERA_ROTATE") == Input.CURSOR_BDIAGSIZE, "godot shape is CURSOR_BDIAGSIZE")
cm.set_cursor("CHAIR")
_ck(cm.get_cursor() == "CHAIR", "shape is CHAIR")
_ck(cm._godot_shape("CHAIR") == Input.CURSOR_WAIT, "godot shape is CURSOR_WAIT")
cm.set_cursor("HSIZE")
_ck(cm.get_cursor() == "HSIZE", "shape is HSIZE")
_ck(cm._godot_shape("HSIZE") == Input.CURSOR_HSIZE, "godot shape is CURSOR_HSIZE")
cm.queue_free()
for shape_idx in range(17):
Input.set_custom_mouse_cursor(null, shape_idx)
CursorManager.UiAssets.clear_cache()
print("OK: CursorManager parity verified")
class DummyClient extends Node:
signal ground_item_added(d: Dictionary)
signal ground_item_removed(vid: int)
var picked_vids: Array[int] = []
func pickup_item(vid: int) -> bool:
picked_vids.append(vid)
return true
func test_ground_item_models_and_picking() -> void:
print("Testing GroundItems model instantiation and picking...")
var mount := Node3D.new()
get_root().add_child(mount)
var cam := Camera3D.new()
get_root().add_child(cam)
cam.position = Vector3(0, 5, 5)
cam.look_at_from_position(Vector3(0, 5, 5), Vector3.ZERO, Vector3.UP)
cam.current = true
var client := DummyClient.new()
get_root().add_child(client)
var gi := GroundItems.new()
get_root().add_child(gi)
var player := Node3D.new()
player.position = Vector3.ZERO
get_root().add_child(player)
gi.assets_override = "__none__"
gi.setup(client, mount, func() -> Node: return player, null, null,
func() -> Camera3D: return cam)
gi._on_added({"vid": 1001, "vnum": 1, "owner": "Player", "pos": Vector3.ZERO})
_ck(gi._by_vid.has(1001), "Drop 1001 must exist")
var drop1: Node3D = gi._by_vid[1001]
var m1 = drop1.get_node_or_null("model")
if m1 == null:
m1 = drop1.get_child(0)
_ck(m1 != null, "Drop must have a visual child (Metin2Model or BoxMesh fallback)")
gi._on_added({"vid": 1002, "vnum": 10, "owner": "Player", "pos": Vector3(10, 0, 0)})
_ck(gi._by_vid.has(1002), "Drop 1002 must exist")
await process_frame
await process_frame
var sp1 := cam.unproject_position(drop1.global_position + Vector3(0, 0.35, 0))
var picked_vid := gi.pick_at(cam, sp1)
_ck(picked_vid == 1001, "pick_at should pick drop 1001 at its screen projection")
var pc := PlayerController.new()
get_root().add_child(pc)
pc.player = player
pc.camera = cam
pc.ground_items = gi
var emitted := [0]
pc.ground_item_clicked.connect(func(vid: int): emitted[0] = vid)
pc._on_click(sp1)
_ck(emitted[0] == 1001, "PlayerController._on_click must emit ground_item_clicked with picked vid")
gi.clear_for_map_change()
pc.queue_free()
player.queue_free()
gi.queue_free()
client.queue_free()
cam.queue_free()
mount.queue_free()
await process_frame
await process_frame
print("OK: GroundItems model and picking verified")