- item_tooltip_view.gd: 新增 avoid_rect 属性,tooltip 与装备窗口重叠时自动推到左侧 - inventory_ui.gd: 悬停装备时传入窗口矩形作为避让区域 - 包含其他累积的功能开发和测试文件
214 lines
7.0 KiB
GDScript
214 lines
7.0 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 != 1:
|
|
printerr("FAIL: FLY_EXP 应为 1,实际为: ", FlyObject.FLY_EXP)
|
|
fails += 1
|
|
if FlyObject.FLY_HP_SMALL != 15 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) 按 40250 CreateIndexedFly 生成 1 颗聚能光球
|
|
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.set_local_node(player_node)
|
|
nw.set_local_vid(1)
|
|
# The local actor is owned by GameScene rather than _by_vid. During the
|
|
# first GC_CREATE_FLY after a phase transition the EntityStore row may not
|
|
# have been mirrored yet; 40250 still resolves the main instance directly.
|
|
fake_client.ents.erase(1)
|
|
|
|
# 触发 40250 官方 _on_fly(FLY_EXP, 100, 1)
|
|
nw._on_fly(FlyObject.FLY_EXP, 100, 1)
|
|
if mgr.active_count() != 1:
|
|
printerr("FAIL: _on_fly(FLY_EXP) 应按一次 GC_CREATE_FLY 发射 1 颗经验聚能光球,实际: ", mgr.active_count())
|
|
fails += 1
|
|
for orb in mgr._instances:
|
|
if not orb._is_object or orb._target_obj != player_node:
|
|
printerr("FAIL: 经验球没有锁定本地主角节点,无法产生吸收效果")
|
|
fails += 1
|
|
break
|
|
|
|
# 40250 RecvCreateFlyPacket requires both CInstanceBase pointers. A stale
|
|
# EntityStore row is not enough to create a visual projectile when the
|
|
# remote scene node has not been built yet.
|
|
mgr.clear_for_map_change()
|
|
fake_client.ents[2] = {"vid": 2, "pos": Vector3(6, 0, 0)}
|
|
nw._on_fly(FlyObject.FLY_EXP, 100, 2)
|
|
if mgr.active_count() != 0:
|
|
printerr("FAIL: 远端终点节点缺失时不应仅凭 EntityStore 坐标创建飞行物")
|
|
fails += 1
|
|
fake_client.ents.erase(2)
|
|
|
|
var saved_mob_node: Node3D = nw._by_vid[100]
|
|
nw._by_vid.erase(100)
|
|
nw._on_fly(FlyObject.FLY_EXP, 100, 1)
|
|
if mgr.active_count() != 0:
|
|
printerr("FAIL: 远端起点节点缺失时不应创建飞行物")
|
|
fails += 1
|
|
nw._by_vid[100] = saved_mob_node
|
|
|
|
# 清空飞行物
|
|
for i in range(50):
|
|
mgr.step(0.02)
|
|
if mgr.active_count() == 0:
|
|
break
|
|
# The indexed EXP resource intentionally starts with a spreading phase and
|
|
# only enables homing at one second; isolate the offline helper assertion
|
|
# from that server-created instance instead of assuming the old synthetic
|
|
# speed reaches the target in 1s.
|
|
mgr.clear_for_map_change()
|
|
|
|
# 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 经验:视觉只由上面的 GC_CREATE_FLY 路径生成。
|
|
np._last_dead_mob_pos = Vector3(3, 0, 0)
|
|
np._on_points({"exp": 1500, "next_exp": 5000, "level": 10})
|
|
if mgr.active_count() != 0:
|
|
printerr("FAIL: POINT_EXP 不应重复生成经验光球,实际数量: ", mgr.active_count())
|
|
fails += 1
|
|
|
|
# All flying instances are now test-owned and no longer needed. Break the
|
|
# manager's handler/target cycles before synchronously destroying the fixture;
|
|
# this mirrors CFlyingManager::DeleteAllInstances before application exit.
|
|
mgr.clear_for_map_change()
|
|
if is_instance_valid(pc):
|
|
pc.free()
|
|
mount.free()
|
|
# Do not keep RefCounted fixtures alive on the _init stack until quit().
|
|
d = null
|
|
inst = null
|
|
player_target = null
|
|
nw = null
|
|
np = null
|
|
pc = null
|
|
mgr = null
|
|
mount = null
|
|
await process_frame
|
|
|
|
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()
|