feat(teleport): add warmup particles and fading departure clouds
Release / release (push) Successful in 2m56s
CI / build (push) Successful in 1m35s

This commit is contained in:
dmg
2026-09-06 21:11:24 -04:00
parent ecb07d48ad
commit 28dd5422d5
7 changed files with 274 additions and 1 deletions
@@ -19,6 +19,82 @@ import org.bukkit.plugin.Plugin;
import org.junit.jupiter.api.Test;
final class BaseTeleportManagerTest {
@org.junit.jupiter.params.ParameterizedTest
@org.junit.jupiter.params.provider.CsvSource({
"false,true,false", "true,true,false", "false,false,false", "true,false,false",
"false,false,true", "true,false,true"
})
void particlesFollowHomeAndVisitorTeleportOutcome(boolean visit, boolean succeeds, boolean cancelled) {
UUID id = UUID.randomUUID();
UUID worldId = UUID.randomUUID();
World world = mock(World.class);
when(world.getUID()).thenReturn(worldId);
org.bukkit.Location origin = new org.bukkit.Location(world, 0, 64, 0);
org.bukkit.Location target = new org.bukkit.Location(world, 20, 64, 20);
Player player = mock(Player.class);
when(player.getUniqueId()).thenReturn(id);
when(player.getName()).thenReturn("Player");
when(player.getWorld()).thenReturn(world);
when(player.getLocation()).thenReturn(origin);
when(player.isOnline()).thenReturn(true);
when(player.teleport(eq(target), any(org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.class)))
.thenReturn(succeeds);
PlayerState owner = PlayerState.newPlayer(visit ? UUID.randomUUID() : id, "Owner")
.withGrassAndDirtProgress(500, 4)
.withBase(new BaseLocation(worldId, "world", 20, 64, 20, 0, 0), Instant.EPOCH)
.withVisitorsEnabled(true);
BaseStateManager states = mock(BaseStateManager.class);
when(states.player(id, "Player")).thenReturn(owner);
TeleportPolicy policy = mock(TeleportPolicy.class);
when(policy.warmup(owner)).thenReturn(Duration.ofSeconds(1));
when(policy.visitorWarmup(owner)).thenReturn(Duration.ofSeconds(1));
when(policy.cooldown(owner)).thenReturn(Duration.ZERO);
VisitorPolicy visitors = mock(VisitorPolicy.class);
SafeBaseDestination finder = mock(SafeBaseDestination.class);
when(finder.find(world, owner.base().orElseThrow())).thenReturn(Optional.of(target));
TeleportParticles particles = mock(TeleportParticles.class);
Plugin plugin = mock(Plugin.class);
org.bukkit.scheduler.BukkitScheduler scheduler = mock(org.bukkit.scheduler.BukkitScheduler.class);
when(scheduler.runTaskTimer(eq(plugin), any(Runnable.class), eq(0L), eq(20L)))
.thenReturn(mock(org.bukkit.scheduler.BukkitTask.class));
try (org.mockito.MockedStatic<org.bukkit.Bukkit> bukkit = org.mockito.Mockito.mockStatic(org.bukkit.Bukkit.class)) {
bukkit.when(org.bukkit.Bukkit::getScheduler).thenReturn(scheduler);
bukkit.when(() -> org.bukkit.Bukkit.getWorld(worldId)).thenReturn(world);
BaseTeleportManager manager = new BaseTeleportManager(plugin, states, policy,
visitors, finder, Clock.systemUTC(), particles);
if (visit) manager.startVisit(player, owner); else manager.start(player);
verify(particles).start(id, origin, 1);
org.mockito.ArgumentCaptor<Runnable> tick = org.mockito.ArgumentCaptor.forClass(Runnable.class);
verify(scheduler).runTaskTimer(eq(plugin), tick.capture(), eq(0L), eq(20L));
tick.getValue().run();
org.mockito.Mockito.verify(player, org.mockito.Mockito.never()).teleport(eq(target),
any(org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.class));
if (cancelled) {
org.bukkit.event.player.PlayerQuitEvent quit = mock(org.bukkit.event.player.PlayerQuitEvent.class);
when(quit.getPlayer()).thenReturn(player);
manager.onQuit(quit);
verify(particles).cancel(id);
org.mockito.Mockito.verify(particles, org.mockito.Mockito.never()).complete(id);
org.mockito.Mockito.verify(states, org.mockito.Mockito.never()).saveIfDirty();
manager.cancelAll();
verify(particles).clear();
return;
}
tick.getValue().run();
if (succeeds) {
verify(particles).complete(id);
org.mockito.Mockito.verify(particles, org.mockito.Mockito.never()).cancel(id);
verify(states).saveIfDirty();
} else {
verify(particles).cancel(id);
org.mockito.Mockito.verify(particles, org.mockito.Mockito.never()).complete(id);
org.mockito.Mockito.verify(states, org.mockito.Mockito.never()).saveIfDirty();
}
manager.cancelAll();
verify(particles).clear();
}
}
@Test
void visitFromAnotherWorldIsRejectedBeforeWarmupAndCooldownChecks() {
UUID visitorId = UUID.randomUUID();
@@ -0,0 +1,77 @@
package games.dmg.spigotbase;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.UUID;
import org.bukkit.Location;
import org.bukkit.Particle;
import org.bukkit.Server;
import org.bukkit.World;
import org.bukkit.plugin.Plugin;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scheduler.BukkitTask;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
final class TeleportParticlesTest {
@Test
void warmupIntensifiesThenFadesAtOriginalLocationAndStops() {
Plugin plugin = mock(Plugin.class);
Server server = mock(Server.class);
BukkitScheduler scheduler = mock(BukkitScheduler.class);
BukkitTask task = mock(BukkitTask.class);
World world = mock(World.class);
when(plugin.getServer()).thenReturn(server);
when(server.getScheduler()).thenReturn(scheduler);
when(scheduler.runTaskTimer(eq(plugin), any(Runnable.class), eq(0L), eq(2L)))
.thenReturn(task);
TeleportParticles effects = new TeleportParticles(plugin);
UUID id = UUID.randomUUID();
Location origin = new Location(world, 10, 64, 20);
effects.start(id, origin, 2);
origin.setX(999);
ArgumentCaptor<Runnable> runnable = ArgumentCaptor.forClass(Runnable.class);
verify(scheduler).runTaskTimer(eq(plugin), runnable.capture(), eq(0L), eq(2L));
Runnable tick = runnable.getValue();
for (int i = 0; i < 21; i++) tick.run();
ArgumentCaptor<Integer> counts = ArgumentCaptor.forClass(Integer.class);
verify(world, times(21)).spawnParticle(eq(Particle.END_ROD), any(Location.class),
counts.capture().intValue(), anyDouble(), anyDouble(), anyDouble(), anyDouble());
assertTrue(counts.getAllValues().getLast() > counts.getAllValues().getFirst());
clearInvocations(world);
effects.complete(id);
for (int i = 0; i < 21; i++) tick.run();
ArgumentCaptor<Location> locations = ArgumentCaptor.forClass(Location.class);
counts = ArgumentCaptor.forClass(Integer.class);
verify(world, times(20)).spawnParticle(eq(Particle.END_ROD), locations.capture(),
counts.capture().intValue(), anyDouble(), anyDouble(), anyDouble(), anyDouble());
assertTrue(counts.getAllValues().getFirst() > counts.getAllValues().getLast());
assertTrue(locations.getAllValues().stream().allMatch(p -> Math.abs(p.getX() - 10) < 2));
verify(task).cancel();
}
@Test
void cancellationAndShutdownStopWithoutAfterglow() {
Plugin plugin = mock(Plugin.class);
Server server = mock(Server.class);
BukkitScheduler scheduler = mock(BukkitScheduler.class);
BukkitTask task = mock(BukkitTask.class);
World world = mock(World.class);
when(plugin.getServer()).thenReturn(server);
when(server.getScheduler()).thenReturn(scheduler);
when(scheduler.runTaskTimer(eq(plugin), any(Runnable.class), eq(0L), eq(2L)))
.thenReturn(task);
TeleportParticles effects = new TeleportParticles(plugin);
UUID id = UUID.randomUUID();
effects.start(id, new Location(world, 0, 64, 0), 30);
effects.cancel(id);
effects.complete(id);
verify(task).cancel();
effects.start(id, new Location(world, 0, 64, 0), 30);
effects.clear();
verify(task, times(2)).cancel();
verifyNoInteractions(world);
}
}