Files
mtgodot-poc/project/test_click_target_effect_parity.gd
T

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])