- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
162 lines
5.6 KiB
GDScript
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")
|