184 lines
6.7 KiB
GDScript
184 lines
6.7 KiB
GDScript
# test_click_target_effect_parity.gd
|
|
# 严格按照《CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md》手段 2 与工程原则 3:
|
|
# 验证 40250 地面点选波纹 (EFFECT_PICK / click.mse)、目标光圈 (EFFECT_TARGET / click_glow_select.mse)
|
|
# 与飞行物建筑碰撞 (FCheckBackgroundDuringFlying) 1:1 对拍测试。
|
|
extends SceneTree
|
|
|
|
const PlayerController = preload("res://player_controller.gd")
|
|
const NetWorld = preload("res://net_world.gd")
|
|
const FlyObject = preload("res://fly_object.gd")
|
|
|
|
class MockFx extends RefCounted:
|
|
var spawned_effects: Array = []
|
|
func spawn(effect_name: String, parent_node: Node3D, looping: bool) -> Node3D:
|
|
spawned_effects.append({"name": effect_name, "parent": parent_node, "looping": looping})
|
|
var eft := Node3D.new()
|
|
eft.name = "mock_fx_" + effect_name.get_file().get_basename()
|
|
parent_node.add_child(eft)
|
|
return eft
|
|
func spawn_at(effect_name: String, parent_node: Node3D, pos: Vector3, looping: bool) -> Node3D:
|
|
spawned_effects.append({"name": effect_name, "parent": parent_node, "pos": pos, "looping": looping})
|
|
var eft := Node3D.new()
|
|
eft.name = "mock_fx_" + effect_name.get_file().get_basename()
|
|
parent_node.add_child(eft)
|
|
eft.position = pos
|
|
return eft
|
|
|
|
func _init() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _run() -> void:
|
|
print("=== Starting test_click_target_effect_parity ===")
|
|
var fails := [0]
|
|
var check = func(cond: bool, msg: String):
|
|
if not cond:
|
|
fails[0] += 1
|
|
printerr("FAIL: ", msg)
|
|
else:
|
|
print("PASS: ", msg)
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 1: PlayerController ground_clicked 信号与坐标
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 1: Ground Click Signal & Position ---")
|
|
var pc := PlayerController.new()
|
|
root.add_child(pc)
|
|
var player_node := Node3D.new()
|
|
root.add_child(player_node)
|
|
player_node.position = Vector3(100.0, 50.0, 100.0)
|
|
pc.player = player_node
|
|
pc.active = true
|
|
|
|
# 模拟世界
|
|
var mock_world = Node.new()
|
|
var w_script = GDScript.new()
|
|
w_script.source_code = """
|
|
extends Node
|
|
func sample_height(x: float, z: float) -> float:
|
|
return 25.0
|
|
func is_blocked(_x: float, _z: float) -> bool:
|
|
return false
|
|
"""
|
|
w_script.reload()
|
|
mock_world.set_script(w_script)
|
|
root.add_child(mock_world)
|
|
pc.world = mock_world
|
|
|
|
var ground_clicked_pos := [Vector3.ZERO]
|
|
var ground_clicked_count := [0]
|
|
pc.ground_clicked.connect(func(pos: Vector3):
|
|
ground_clicked_count[0] += 1
|
|
ground_clicked_pos[0] = pos
|
|
)
|
|
|
|
# 模拟鼠标输入事件点地 (从上方向下发射射线)
|
|
# _ray_ground(from, dir): from=(110, 50, 110), dir=(0, -1, 0) -> 命中 y=25.0 处
|
|
var click_hit = pc._ray_ground(Vector3(110.0, 50.0, 110.0), Vector3(0.0, -1.0, 0.0))
|
|
check.call(click_hit != null and absf(click_hit.y - 25.0) < 0.01, "Mock ray_ground hits surface at y=25.0")
|
|
|
|
# 手动触发点地流程并验证信号发射
|
|
pc.ground_clicked.emit(click_hit)
|
|
check.call(ground_clicked_count[0] == 1, "ground_clicked signal emitted")
|
|
check.call(ground_clicked_pos[0].is_equal_approx(Vector3(110.0, 25.0, 110.0)), "ground_clicked emitted exact world position")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 2: NetWorld 目标光圈 AttachTargetEffect / DetachTargetEffect
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 2: NetWorld Target Effect (click_glow_select.mse) ---")
|
|
var nw := NetWorld.new()
|
|
root.add_child(nw)
|
|
|
|
var mock_fx := MockFx.new()
|
|
nw.fx = mock_fx
|
|
|
|
# 添加一个实体
|
|
var target_entity := Node3D.new()
|
|
root.add_child(target_entity)
|
|
target_entity.position = Vector3(200.0, 10.0, 300.0)
|
|
var vid := 5001
|
|
nw.set("_by_vid", {vid: target_entity})
|
|
|
|
# 选中目标: set_target_vid(5001)
|
|
nw.set_target_vid(vid)
|
|
check.call(mock_fx.spawned_effects.size() == 1, "Target effect spawned on set_target_vid")
|
|
var last_fx: Dictionary = mock_fx.spawned_effects[0]
|
|
check.call("click_glow_select.mse" in last_fx.name, "Spawned effect is click_glow_select.mse")
|
|
check.call(last_fx.parent == target_entity, "Target effect attached to target entity")
|
|
check.call(not last_fx.looping, "Target effect configured as looping (one_shot=false)")
|
|
|
|
# 取消选中: set_target_vid(0)
|
|
nw.set_target_vid(0)
|
|
check.call(nw._target_effect == null, "Target effect detached on set_target_vid(0)")
|
|
|
|
# 再次选中并在实体被 _fade_and_free 销毁时验证分离
|
|
nw.set_target_vid(vid)
|
|
check.call(nw._target_effect != null, "Target effect re-attached on selecting target again")
|
|
nw._fade_and_free(vid)
|
|
check.call(nw._target_effect == null, "Target effect cleanly detached on entity removal/death")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 3: Flying Instance 3D Static Obstacle Collision
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 3: Flying Instance 3D Static Obstacle Collision ---")
|
|
# 3. 飞行物与静态建筑/障碍物碰撞 (CFlyingInstance::FCheckBackgroundDuringFlying)
|
|
# 构造一个挂在场景树中的 Node3D 作为 world,以及一个 StaticBody3D 障碍物
|
|
var fly_mgr := FlyObject.new()
|
|
var fly_mount := Node3D.new()
|
|
var fly_world := Node3D.new()
|
|
root.add_child(fly_mount)
|
|
root.add_child(fly_world)
|
|
|
|
var wall := StaticBody3D.new()
|
|
wall.collision_layer = 2
|
|
wall.collision_mask = 0
|
|
var col_shape := CollisionShape3D.new()
|
|
var box := BoxShape3D.new()
|
|
box.size = Vector3(10.0, 20.0, 1.0) # x=10m, y=20m, z=1m 厚的墙
|
|
col_shape.shape = box
|
|
wall.add_child(col_shape)
|
|
wall.position = Vector3(0.0, 10.0, 50.0) # 位于 z=50m 处
|
|
fly_world.add_child(wall)
|
|
|
|
# 等待 2 帧让 PhysicsServer3D 完成场景树内静态碰撞体的注册
|
|
for _f in range(2):
|
|
await process_frame
|
|
|
|
fly_mgr.setup(fly_mount, fly_world)
|
|
fly_mgr.manual_step = true
|
|
var fly_data := FlyObject.FlyData.new()
|
|
fly_data.hit_on_background = true
|
|
fly_data.init_vel = 100.0 # 100 m/s
|
|
fly_data.flat_range = 200.0
|
|
|
|
# 发射一颗射弹从 z=0 射向 z=100 (穿过 z=50 的墙)
|
|
var recorded_events := []
|
|
var inst = fly_mgr.spawn(Vector3(0.0, 10.0, 0.0), Vector3(0.0, 10.0, 100.0), false, fly_data)
|
|
inst.handler = func(ev: String, p: Vector3, _v: int):
|
|
recorded_events.append({"ev": ev, "pos": p})
|
|
|
|
# 推进时间让射弹穿过墙体
|
|
for i in range(10):
|
|
if not inst.alive:
|
|
break
|
|
fly_mgr.step(0.1) # 10m 步长
|
|
|
|
var hit_bg = false
|
|
for e in recorded_events:
|
|
if e.ev == "at_background":
|
|
hit_bg = true
|
|
check.call(not inst.alive, "Projectile stopped and exploded")
|
|
check.call(hit_bg, "Projectile hit static obstacle and triggered at_background")
|
|
|
|
# 清理
|
|
pc.queue_free()
|
|
player_node.queue_free()
|
|
mock_world.queue_free()
|
|
target_entity.queue_free()
|
|
nw.queue_free()
|
|
wall.queue_free()
|
|
fly_mount.queue_free()
|
|
fly_world.queue_free()
|
|
|
|
print("\n=== All Click, Target & Fly Parity Tests finished with %d failures ===" % fails[0])
|
|
quit(fails[0])
|