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

164 lines
4.9 KiB
GDScript

# test_exp_fly_parity.gd
# 检验 40250 怪物阵亡经验光球飞行、自导向追踪与吸收动效
extends SceneTree
const FlyObject = preload("res://fly_object.gd")
const NetPlay = preload("res://net_play.gd")
const NetWorld = preload("res://net_world.gd")
const PlayerCtl = preload("res://player_controller.gd")
func _init() -> void:
print("--- 开始测试 40250 怪物阵亡经验光球飞行与吸收动效 (test_exp_fly_parity) ---")
var fails: int = 0
# 1. 验证 40250 FLY 常量与编号映射
if FlyObject.FLY_EXP != 0:
printerr("FAIL: FLY_EXP 应为 0,实际为: ", FlyObject.FLY_EXP)
fails += 1
if FlyObject.FLY_HP_SMALL != 1 or FlyObject.FLY_SP_SMALL != 4:
printerr("FAIL: FLY_HP/SP 常量映射错误")
fails += 1
# 2. 模拟 FlyObject 经验球追踪、命中与吸收信号
var mount := Node3D.new()
get_root().add_child(mount)
var mgr := FlyObject.new()
mgr.manual_step = true
mount.add_child(mgr)
mgr.setup(mount)
var player_target := Node3D.new()
mount.add_child(player_target)
player_target.position = Vector3(5, 1, 0)
var exp_absorbed_count := [0]
mgr.exp_absorbed.connect(func(_wp: Vector3):
exp_absorbed_count[0] += 1
)
var d := FlyObject.FlyData.new()
d.init_vel = 15.0
d.flat_range = 30.0
d.bomb_range = 0.8
d.is_homing = true
d.homing_start_time = 0.05
d.homing_max_angle = 140.0
d.maintain_parallel = true
var start_pos := Vector3(0, 1, 0)
var inst = mgr.spawn(start_pos, player_target, false, d, FlyObject.FLY_EXP)
if mgr.active_count() != 1:
printerr("FAIL: 经验球未成功注册进飞行物管理器!")
fails += 1
if inst.skill_index != FlyObject.FLY_EXP:
printerr("FAIL: 实例技能索引不符: ", inst.skill_index)
fails += 1
# 推进仿真时间步,让光球命中玩家目标
for i in range(40):
mgr.step(0.02)
if mgr.active_count() == 0:
break
if exp_absorbed_count[0] != 1:
printerr("FAIL: 经验球到达玩家时未发出 exp_absorbed 信号,次数: ", exp_absorbed_count[0])
fails += 1
if mgr.active_count() != 0:
printerr("FAIL: 经验球命中后未从活跃队列销毁!")
fails += 1
# 3. 验证 net_world._on_fly(FLY_EXP) 生成 3 颗聚能光球
var nw := NetWorld.new()
mount.add_child(nw)
nw.parent = mount
nw._fly_mgr = mgr
# 模拟注册实体坐标
var fake_client := _FakeClient.new()
fake_client.ents[100] = {"vid": 100, "pos": Vector3(0, 0, 0)} # 怪物
fake_client.ents[1] = {"vid": 1, "pos": Vector3(4, 0, 0)} # 玩家
nw.client = fake_client
var mob_node := Node3D.new()
mob_node.position = Vector3(0, 0, 0)
mount.add_child(mob_node)
nw._by_vid[100] = mob_node
var player_node := Node3D.new()
player_node.position = Vector3(4, 0, 0)
mount.add_child(player_node)
nw._by_vid[1] = player_node
# 触发 40250 官方 _on_fly(0, 100, 1)
nw._on_fly(FlyObject.FLY_EXP, 100, 1)
if mgr.active_count() != 3:
printerr("FAIL: _on_fly(FLY_EXP) 应发射 3 颗经验聚能光球,实际: ", mgr.active_count())
fails += 1
# 清空飞行物
for i in range(50):
mgr.step(0.02)
if mgr.active_count() == 0:
break
# 4. 验证 net_world.spawn_exp_fly 接口
nw.spawn_exp_fly(Vector3(2, 0, 2), player_node, 3)
if mgr.active_count() != 3:
printerr("FAIL: spawn_exp_fly 未成功生成 3 颗光球,实际: ", mgr.active_count())
fails += 1
for i in range(50):
mgr.step(0.02)
if mgr.active_count() == 0:
break
# 5. 验证 net_play 经验变动时触发经验获取流程
var np := NetPlay.new()
mount.add_child(np)
var pc := PlayerCtl.new()
pc.player = player_node
np.setup(fake_client, pc, nw, null)
# 初始经验
np._on_points({"exp": 1000, "next_exp": 5000, "level": 10})
if mgr.active_count() != 0:
printerr("FAIL: 初始点数同步不应触发获取经验!")
fails += 1
# 杀死怪物获得 500 经验
np._last_dead_mob_pos = Vector3(3, 0, 0)
np._on_points({"exp": 1500, "next_exp": 5000, "level": 10})
if mgr.active_count() != 3:
printerr("FAIL: 经验增加时未能通过 net_world 生成经验光球!实际数量: ", mgr.active_count())
fails += 1
mount.queue_free()
if fails == 0:
print("PASS: test_exp_fly_parity (6/6 全部通过)")
quit(0)
else:
printerr("FAILED: test_exp_fly_parity (", fails, " 项失败)")
quit(1)
class _FakeClient extends Node:
signal entity_spawned(entity: Dictionary)
signal entity_despawned(vid: int)
signal entity_moved(vid: int)
signal entity_main_set(vid: int)
signal entity_dead(vid: int)
signal vitals_changed(vid: int)
signal points_changed(points: Dictionary)
signal chat(type: int, vid: int, text: String)
signal damage(vid: int, amount: int, flag: int)
signal target_info(t: Dictionary)
signal fly_cue(type: int, start_vid: int, end_vid: int)
signal fly_targeting(s_vid: int, t_vid: int, pos: Vector2, append: bool)
var ents := {}
func is_in_game() -> bool: return true
func get_main_vid() -> int: return 1
func get_entity(vid: int) -> Dictionary: return ents.get(vid, {})
func get_entities() -> Array: return ents.values()