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
+29 -2
View File
@@ -50,8 +50,11 @@ class FakePC extends Node:
var server_speed := 1.0
var walk_calls := []
var frozen := false
var going := false
func set_server_speed(moving_speed: int) -> void: server_speed = float(moving_speed) / 100.0
func walk_to(p: Vector3) -> void: walk_calls.append(p)
func walk_to(p: Vector3) -> void: walk_calls.append(p); going = true
func is_going() -> bool: return going
func stop() -> void: going = false
class FakeHud extends Node:
var vitals := []
@@ -126,7 +129,7 @@ func _run() -> void:
pc.player.rotation.y = 0.0
fc.calls.fishing.clear()
np.set_attack_key(true)
_ck(fc.calls.fishing.size() == 1 and is_equal_approx(float(fc.calls.fishing[0]), 270.0),
_ck(fc.calls.fishing.size() == 1 and is_equal_approx(float(fc.calls.fishing[0]), 0.0),
"fishing attack key -> CG_FISHING with MapCoord heading")
_ck(not np._attack_key_down, "fishing attack key does not enable normal attack")
fc.fishing_event.emit(0, 1000, 0)
@@ -173,10 +176,34 @@ func _run() -> void:
# 窗口由服务端 cube_open 命令打开前的入口。
var npc := Node3D.new()
npc.set_meta("vid", 3000)
npc.position = Vector3(4, 0, -5) # 距玩家(3.3,0,-5) 0.7m -> 交互距离内(<= 5m
root.add_child(npc)
nw._by_vid[3000] = npc
fc.ents[3000] = {"vid": 3000, "ch_type": 1, "name": "Cube NPC", "pos": npc.position}
pc.target_selected.emit(npc)
_ck(fc.calls.click_npc == [3000], "NPC selection -> click_npc(3000)")
# 远距离点选 NPC(> 5m):预约走近,到达交互距离后停下并触发 click_npc
var far_npc := Node3D.new()
far_npc.set_meta("vid", 3010)
far_npc.position = Vector3(20, 0, -5) # 距玩家 16.7m -> 超出 5m 交互范围
root.add_child(far_npc)
nw._by_vid[3010] = far_npc
fc.ents[3010] = {"vid": 3010, "ch_type": 1, "name": "Far NPC", "pos": far_npc.position}
pc.target_selected.emit(far_npc)
_ck(np._reserved_mode == 2, "远距离点 NPC -> 预约走近 (CLICK_ACTOR)")
_ck(pc.is_going(), "远距离点 NPC -> 角色自动向 NPC 移动")
_ck(fc.calls.click_npc == [3000], "尚未到达时不发点击包")
# 玩家走近到 NPC 交互距离内
pc.player.position = Vector3(18, 0, -5)
np._reserve_process_click_actor()
_ck(fc.calls.click_npc == [3000, 3010], "走近到交互距离后 -> 触发 click_npc(3010)")
_ck(np._reserved_mode == 0, "触发交互后清空预约")
_ck(not pc.is_going(), "触发交互后停止移动")
nw._by_vid.erase(3010)
fc.ents.erase(3010)
far_npc.queue_free()
pc.player.position = Vector3(3.3, 0, -5)
# §5.4 / §8.8:木门不应退化为 PC;它可进入 CanViewTargetHP / 攻击分类。
_ck(np._entity_kind({"vid": 3001, "race": 13000}) == EntityRules.KIND_WOODEN_DOOR,
"race 13000 -> WOODEN_DOOR")