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 客户端对拍缺陷发现与修复工程指南
This commit is contained in:
shen
2026-09-19 08:51:25 -07:00
parent 1db9e9a129
commit 66d217b313
650 changed files with 53046 additions and 1586 deletions
+99
View File
@@ -0,0 +1,99 @@
# test_bridge_height_parity.gd
# 验证 40250 规范:桥梁与静态物体 (.mdatr) 高度采样 1:1 对齐测试。
# 验证点:
# 1. 在桥梁所在位置,world.sample_height 返回桥面高度(由 .mdatr 中的 height 网格计算),
# 而不是桥下方河床/水面的裸地形高度。
# 2. 桥梁外侧(偏离桥面)的点,正确回退到裸地形采样高度。
# 3. 角色踏上桥梁时,Y 坐标与桥面高度一致,不再掉落穿透到桥底水下。
extends SceneTree
const ASSETS_PATH := "res://../assets"
const MAP_PATH := "OutdoorA1/metin2_map_a1"
func _init() -> void:
call_deferred("_run")
func _run() -> void:
print("=== Starting test_bridge_height_parity ===")
var world: Node = null
if ClassDB.class_exists("Metin2World"):
world = ClassDB.instantiate("Metin2World")
else:
printerr("FAIL: Metin2World not available")
quit(1)
return
get_root().add_child(world)
var assets_abs := ProjectSettings.globalize_path(ASSETS_PATH)
world.set("assets_root", assets_abs)
world.set("map_path", MAP_PATH)
world.set("objects_enabled", true)
world.set("water_enabled", true)
world.set("load_radius_tiles", -1) # load all chunks
var ok: bool = bool(world.call("load_map"))
if not ok:
printerr("FAIL: load_map failed")
quit(1)
return
# 测试桥梁 1: a1-024-10m-bridge (位于 Chunk 002002)
# 原始 areadata: x=76156.88 y=-59454.84 z=19693.11 bias=-290.0
# Godot 坐标: x = 761.57, z = 594.55
var b1_x := 761.57
var b1_z := 594.55
var h1: float = float(world.call("sample_height", b1_x, b1_z))
print("Bridge 1 (a1-024-10m-bridge) at (%f, %f) sample_height = %f m" % [b1_x, b1_z, h1])
# 桥面应在 205m ~ 207m 区间,而河床地形在 ~198.2m
if h1 < 204.0 or h1 > 208.0:
printerr("FAIL: Bridge 1 deck height expected ~206m, got %f m (fell through to riverbed?)" % h1)
quit(1)
return
print("PASS: Bridge 1 deck height correctly sampled at %f m (> 198.2m riverbed)" % h1)
# 偏离桥面的点 (向河道侧移 15米): (761.57, 570.0) 应为河床高度 ~198m
var off_z := 570.0
var h_off: float = float(world.call("sample_height", b1_x, off_z))
print("Off-bridge riverbed at (%f, %f) sample_height = %f m" % [b1_x, off_z, h_off])
if h_off > 202.0:
printerr("FAIL: Off-bridge point expected riverbed terrain height (< 202m), got %f m" % h_off)
quit(1)
return
print("PASS: Off-bridge point correctly falls back to terrain height %f m" % h_off)
# 测试桥梁 2: general_obj_suspension bridge01 (位于 Chunk 000002)
# 原始 areadata: pos=21390.722656 -70045.242188 13467.375000 rot=roll 270.0
# Godot 坐标: x = 213.91, z = 700.45
var b2_x := 213.91
var b2_z := 700.45
var h2: float = float(world.call("sample_height", b2_x, b2_z))
print("Bridge 2 (suspension bridge01) at (%f, %f) sample_height = %f m" % [b2_x, b2_z, h2])
# 悬索吊桥高度采样验证
if h2 <= 0.0:
printerr("FAIL: Bridge 2 sample_height returned invalid height %f" % h2)
quit(1)
return
print("PASS: Bridge 2 height correctly sampled at %f m" % h2)
# 模拟角色走上桥梁:角色位置沿桥梁中轴线前进
# 桥梁走向从 x=750 到 x=770z=594.55
var step_ok := true
for x_offset in range(-10, 11, 2):
var test_x := b1_x + float(x_offset)
var test_h := float(world.call("sample_height", test_x, b1_z))
if test_h < 203.0:
printerr("FAIL: Character fell through bridge deck at x=%f! height=%f" % [test_x, test_h])
step_ok = false
break
if not step_ok:
quit(1)
return
print("PASS: Character walking along bridge deck maintained deck elevation across entire span!")
world.call("unload_map")
world.queue_free()
print("ALL BRIDGE HEIGHT PARITY CHECKS PASSED (40250 STRICT)")
quit(0)