- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
153 lines
5.4 KiB
GDScript
153 lines
5.4 KiB
GDScript
extends SceneTree
|
|
|
|
const AssetRoot = preload("res://asset_root.gd")
|
|
const Quickbar = preload("res://ui/quickbar.gd")
|
|
const SkillTable = preload("res://ui/skill_table.gd")
|
|
const NetPlay = preload("res://net_play.gd")
|
|
const PlayerController = preload("res://player_controller.gd")
|
|
|
|
func _init() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _run() -> void:
|
|
print("=== Running test_skill_ground_fix ===")
|
|
var fails := 0
|
|
var check = func(cond: bool, msg: String):
|
|
if not cond:
|
|
fails += 1
|
|
print("FAIL: ", msg)
|
|
else:
|
|
print("OK: ", msg)
|
|
|
|
var assets_root := AssetRoot.path()
|
|
var st := SkillTable.new()
|
|
st.load_file(assets_root.path_join("locale/locale/en/skilldesc.txt"))
|
|
|
|
# --- Test 1: Quickbar coordinate and heading parity with MapCoord ---
|
|
MapCoord.set_base(Vector2(409600, 896000))
|
|
var player := Node3D.new()
|
|
player.position = Vector3(150.0, 175.3, 200.0)
|
|
player.rotation.y = deg_to_rad(45.0)
|
|
get_root().add_child(player)
|
|
|
|
var ui := CanvasLayer.new()
|
|
get_root().add_child(ui)
|
|
|
|
var client_mock = Node.new()
|
|
var script = GDScript.new()
|
|
script.source_code = """
|
|
extends Node
|
|
var casts := []
|
|
var uses := []
|
|
func is_in_game() -> bool: return true
|
|
func get_target() -> Dictionary: return {"vid": 2000}
|
|
func get_main_vid() -> int: return 1
|
|
func get_entity(_v: int) -> Dictionary: return {"race": 0, "kind": 2}
|
|
func get_skills() -> Array: return [{"id": 1, "level": 5, "master": 0}]
|
|
func use_skill(id: int, vid: int) -> bool:
|
|
uses.append([id, vid])
|
|
return true
|
|
func cast_skill(mi: int, yaw: float, x: int, y: int) -> bool:
|
|
casts.append([mi, yaw, x, y])
|
|
return true
|
|
func quickslot_add(_slot: int, _type: int, _id: int) -> bool:
|
|
return true
|
|
"""
|
|
script.reload()
|
|
client_mock.set_script(script)
|
|
get_root().add_child(client_mock)
|
|
|
|
var qb := Quickbar.new()
|
|
get_root().add_child(qb)
|
|
qb.setup(client_mock, st, ui, func() -> Node: return player)
|
|
qb.assign(0, "skill", 1) # Skill 1: samyeon (no target required)
|
|
|
|
qb.activate(0)
|
|
|
|
check.call(client_mock.get("casts").size() == 1, "activate sends cast_skill")
|
|
if client_mock.get("casts").size() == 1:
|
|
var c = client_mock.get("casts")[0]
|
|
var expected_yaw := MapCoord.yaw_to_heading(player.rotation.y)
|
|
var expected_xy := MapCoord.to_server_cm(player.position)
|
|
check.call(absf(c[1] - expected_yaw) < 0.01, "cast_skill heading matches MapCoord (%f vs %f)" % [c[1], expected_yaw])
|
|
check.call(c[2] == int(expected_xy.x) and c[3] == int(expected_xy.y),
|
|
"cast_skill xy matches MapCoord.to_server_cm (%d, %d vs %d, %d)" % [c[2], c[3], int(expected_xy.x), int(expected_xy.y)])
|
|
check.call(c[2] == 424600 and c[3] == 916000, "cast_skill contains map base offset (424600, 916000)")
|
|
|
|
# --- Test 2: PlayerController height clamping when locked ---
|
|
var mock_world = Node.new()
|
|
var w_script = GDScript.new()
|
|
w_script.source_code = """
|
|
extends Node
|
|
func sample_height(x: float, z: float) -> float:
|
|
# Sloped terrain: height increases with x and z
|
|
return 100.0 + x * 0.1 + z * 0.05
|
|
"""
|
|
w_script.reload()
|
|
mock_world.set_script(w_script)
|
|
get_root().add_child(mock_world)
|
|
|
|
var pc := PlayerController.new()
|
|
get_root().add_child(pc)
|
|
pc.player = player
|
|
pc.world = mock_world
|
|
pc.active = true
|
|
|
|
# Set locked = true (as during skill cast)
|
|
pc.locked = true
|
|
player.position = Vector3(10.0, 50.0, 20.0) # Intentionally wrong y (50.0 instead of 102.0)
|
|
pc._process(0.016)
|
|
|
|
var expected_h: float = float(mock_world.call("sample_height", player.position.x, player.position.z))
|
|
check.call(absf(player.position.y - expected_h) < 0.001,
|
|
"player.position.y clamped to terrain even when pc.locked is true (%f vs %f)" % [player.position.y, expected_h])
|
|
|
|
# Move horizontally while locked (e.g. root motion / server push)
|
|
player.position.x = 50.0
|
|
player.position.z = 80.0
|
|
pc._process(0.016)
|
|
expected_h = float(mock_world.call("sample_height", player.position.x, player.position.z))
|
|
check.call(absf(player.position.y - expected_h) < 0.001,
|
|
"player.position.y follows slope terrain while locked (%f vs %f)" % [player.position.y, expected_h])
|
|
|
|
# --- Test 3: NetPlay _on_net_moved height sampling on position snap ---
|
|
var np := NetPlay.new()
|
|
get_root().add_child(np)
|
|
np.pc = pc
|
|
np.client = client_mock
|
|
np._main_vid = 1
|
|
|
|
# MapCoord base is 409600, 896000
|
|
# Suppose player is at world (50, 109, 80)
|
|
# Server sends snap to world (100, y, 200) -> server pos (419600, 916000)
|
|
var snap_server_pos = Vector3(4196.0, 0.0, -9160.0) # net frame cm/100
|
|
var snap_entity = {
|
|
"vid": 1,
|
|
"pos": snap_server_pos
|
|
}
|
|
var client_script2 = GDScript.new()
|
|
client_script2.source_code = """
|
|
extends Node
|
|
var ent := {}
|
|
func get_entity(_vid: int) -> Dictionary: return ent
|
|
func is_in_game() -> bool: return false
|
|
"""
|
|
client_script2.reload()
|
|
var client_mock2 = Node.new()
|
|
client_mock2.set_script(client_script2)
|
|
client_mock2.set("ent", snap_entity)
|
|
get_root().add_child(client_mock2)
|
|
np.client = client_mock2
|
|
|
|
np._on_net_moved(1)
|
|
|
|
var target_world_pos: Vector3 = MapCoord.to_world(snap_server_pos)
|
|
var expected_snap_h: float = float(mock_world.call("sample_height", target_world_pos.x, target_world_pos.z))
|
|
check.call(absf(player.position.x - target_world_pos.x) < 0.01 and absf(player.position.z - target_world_pos.z) < 0.01,
|
|
"player snapped to target coords")
|
|
check.call(absf(player.position.y - expected_snap_h) < 0.001,
|
|
"player.position.y on snap was sampled from new terrain height (%f vs %f)" % [player.position.y, expected_snap_h])
|
|
|
|
print("=== Completed test_skill_ground_fix with %d failures ===" % fails)
|
|
quit(fails)
|