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

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)