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