extends SceneTree const Anchor = preload("res://fx/target_effect_anchor.gd") const GameScene = preload("res://game_scene.gd") class Client extends Node: func get_main_vid() -> int: return 1 class World extends Node: var target: Node3D func node_for(_vid: int) -> Node3D: return target class Registry extends RefCounted: var seen: Array = [] func spawn_target(event: Dictionary, target: Node3D, _world: Node3D): seen.append([event, target]) var failures := 0 func check(ok: bool, label: String) -> void: if not ok: failures += 1 printerr("FAIL: " + label) func _init() -> void: call_deferred("run") func run() -> void: var target := Node3D.new() root.add_child(target) target.position = Vector3(8, 2, -5) target.rotation.y = PI * 0.5 var effect := Node3D.new() root.add_child(effect) var anchor := Anchor.new() effect.add_child(anchor) check(anchor.configure(target, Vector3(100, 0, 100), true), "following target configured") check(effect.global_position.distance_to(target.to_global(Vector3(1, 1, 0))) < 0.00001, "source Z-up/cm and target rotation") target.position.x += 3 anchor._process(0) check(effect.global_position.distance_to(target.to_global(Vector3(1, 1, 0))) < 0.00001, "follows target movement") anchor.configure(target, Vector3(100, 0, 100), false) check(effect.global_position.distance_to(target.global_position + Vector3(1, 1, 0)) < 0.00001, "snapshot offset is world-aligned") var position := effect.global_position target.position.x += 2 await process_frame check(effect.global_position == position, "snapshot does not follow") var game := GameScene.new() root.add_child(game) game.client = Client.new() game.add_child(game.client) var world := World.new() game.add_child(world) world.target = target game.net_world = world var registry := Registry.new() game.fx = registry game._on_skill_cast_started(1, 2) world.target = null # selected target later changes, original cast remains. game._on_local_motion_event_detailed({"type": 10, "effect": "full/path.mse", "sound": "", "pos": Vector3.ZERO}) check(registry.seen.size() == 1 and registry.seen[0][1] == target, "event keeps cast-time target and full path") check(registry.seen[0][0].effect == "full/path.mse", "path not reduced to basename") target.free() game._on_local_motion_event_detailed({"type": 10, "effect": "full/path.mse", "sound": "", "pos": Vector3.ZERO}) check(registry.seen.size() == 1, "despawned target not replaced by caster") game._on_skill_cast_started(1, 0) game._on_local_motion_event_detailed({"type": 10, "effect": "full/path.mse", "sound": "", "pos": Vector3.ZERO}) check(registry.seen.size() == 1, "cast without a target spawns no target effect") # A following effect whose target dies/despawns mid-life is freed, never left at a stale position. var victim := Node3D.new() root.add_child(victim) var outside := Node3D.new() var outside_anchor := Anchor.new() check(not outside_anchor.configure(outside, Vector3.ZERO, true), "target outside the tree is rejected") outside_anchor.free() outside.free() var follower := Node3D.new() root.add_child(follower) var follow_anchor := Anchor.new() follower.add_child(follow_anchor) check(follow_anchor.configure(victim, Vector3.ZERO, true), "follower configured") var follower_ref: WeakRef = weakref(follower) victim.free() await process_frame await process_frame check(follower_ref.get_ref() == null, "follower freed after its target left the tree") game.free() effect.free() await process_frame print("target_effect_test: failures=%d" % failures) quit(1 if failures else 0)