diff --git a/src/main/java/net/knarcraft/knarlib/particle/ParticleConfig.java b/src/main/java/net/knarcraft/knarlib/particle/ParticleConfig.java new file mode 100644 index 0000000..f634a58 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/particle/ParticleConfig.java @@ -0,0 +1,167 @@ +package net.knarcraft.knarlib.particle; + +import org.bukkit.Particle; +import org.bukkit.configuration.ConfigurationSection; +import org.jetbrains.annotations.NotNull; + +/** + * A configuration describing a particle + */ +@SuppressWarnings("unused") +public class ParticleConfig { + + private final ParticleMode particleMode; + private final Particle particleType; + private final int particleAmount; + private final double particleDensity; + private final double heightOffset; + private final double offsetX; + private final double offsetY; + private final double offsetZ; + private final double extra; + + /** + * Instantiates a new particle config + * + * @param particlesSection

The configuration section containing the particle's settings

+ */ + public ParticleConfig(@NotNull ConfigurationSection particlesSection) { + @NotNull Particle particleType; + try { + particleType = Particle.valueOf(particlesSection.getString("type")); + } catch (IllegalArgumentException | NullPointerException exception) { + particleType = Particle.ASH; + } + this.particleType = particleType; + this.particleAmount = particlesSection.getInt("amount", 30); + this.offsetX = particlesSection.getDouble("offsetX", 0.5); + this.offsetY = particlesSection.getDouble("offsetY", 1); + this.offsetZ = particlesSection.getDouble("offsetZ", 0.5); + this.heightOffset = particlesSection.getDouble("heightOffset", 0.5); + this.extra = particlesSection.getDouble("extra", 0); + ParticleMode particleMode; + try { + particleMode = ParticleMode.valueOf(particlesSection.getString("mode")); + } catch (IllegalArgumentException | NullPointerException exception) { + particleMode = ParticleMode.SINGLE; + } + this.particleMode = particleMode; + + // Make sure particle density is between 1 (inclusive) and 0 (exclusive) + double particleDensity = particlesSection.getDouble("particleDensity", 0.1); + if (particleDensity <= 0) { + particleDensity = 0.1; + } else if (particleDensity > 360) { + particleDensity = 360; + } + this.particleDensity = particleDensity; + } + + /** + * Instantiates a new particle config + * + * @param particleMode

The mode to use when spawning the particle

+ * @param particleType

The type of particle to spawn

+ * @param particleAmount

The amount of particles to spawn at once

+ * @param particleDensity

The density of the particles, if spawning a shape

+ * @param heightOffset

The offset above the block to spawn the particle

+ * @param offsetX

The x-offset/spread of the spawned particles

+ * @param offsetY

The y-offset/spread of the spawned particles

+ * @param offsetZ

The z-offset/spread of the spawned particles

+ * @param extra

Extra data for the particle. Usage depends on the particle type.

+ */ + public ParticleConfig(ParticleMode particleMode, Particle particleType, int particleAmount, double particleDensity, + double heightOffset, double offsetX, double offsetY, double offsetZ, double extra) { + this.particleMode = particleMode; + this.particleType = particleType; + this.particleAmount = particleAmount; + this.particleDensity = particleDensity; + this.heightOffset = heightOffset; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.offsetZ = offsetZ; + this.extra = extra; + } + + /** + * The mode to use when drawing/spawning the particle(s) + * + * @return

The particle mode

+ */ + public ParticleMode getParticleMode() { + return particleMode; + } + + /** + * The type of particle to spawn + * + * @return

The particle type

+ */ + public Particle getParticleType() { + return particleType; + } + + /** + * The amount of particles to spawn + * + * @return

The amount of particles

+ */ + public int getParticleAmount() { + return particleAmount; + } + + /** + * The density of particles to use in shapes closer to 0 causes larger density + * + * @return

The particle density

+ */ + public double getParticleDensity() { + return particleDensity; + } + + /** + * The number of blocks above the block the particle(s) should spawn + * + * @return

The y-offset

+ */ + public double getHeightOffset() { + return heightOffset; + } + + /** + * The offset/spread of particles in the x-direction + * + * @return

The x-offset

+ */ + public double getOffsetX() { + return offsetX; + } + + /** + * The offset/spread of particles in the y-direction + * + * @return

The y-offset

+ */ + public double getOffsetY() { + return offsetY; + } + + /** + * The offset/spread of particles in the z-direction + * + * @return

The z-offset

+ */ + public double getOffsetZ() { + return offsetZ; + } + + /** + * The extra value to set for the particle. Exactly what it does depends on the particle. + * + * @return

The particle's extra value

+ */ + public double getExtra() { + return extra; + } + +} diff --git a/src/main/java/net/knarcraft/knarlib/particle/ParticleMode.java b/src/main/java/net/knarcraft/knarlib/particle/ParticleMode.java new file mode 100644 index 0000000..74634f5 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/particle/ParticleMode.java @@ -0,0 +1,38 @@ +package net.knarcraft.knarlib.particle; + +/** + * The mode used for spawning one or more particle(s) + */ +public enum ParticleMode { + + /** + * Spawns the set amount of particles on a single point in the world + */ + SINGLE, + + /** + * Spawns the set amount of particles in a square around the block + */ + SQUARE, + + /** + * Spawns the set amount of particles in a circle around the block + */ + CIRCLE, + + /** + * Spawns the set amount of particles in a pyramid centered on the block + */ + PYRAMID, + + /** + * Spawns the set amount of particles in a sphere centered on the block + */ + SPHERE, + + /** + * Spawns the set amount of particles in a cube centered on the block + */ + CUBE, + +} diff --git a/src/main/java/net/knarcraft/knarlib/particle/ParticleSpawner.java b/src/main/java/net/knarcraft/knarlib/particle/ParticleSpawner.java new file mode 100644 index 0000000..bddaf00 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/particle/ParticleSpawner.java @@ -0,0 +1,142 @@ +package net.knarcraft.knarlib.particle; + +import net.knarcraft.knarlib.util.ParticleHelper; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Map; +import java.util.UUID; +import java.util.function.Supplier; + +/** + * A runnable tasks that spawns particles at the given blocks + */ +@SuppressWarnings("unused") +public class ParticleSpawner implements Runnable { + + private final ParticleConfig particleConfig; + private final Map materialConfigs; + + private Block processingBlock; + private final Collection blocks; + private final Supplier> blockSupplier; + private final UUID storedCalculationId; + + /** + * Instantiates a new particle spawner + * + * @param particleConfig

The configuration for the particle to spawn

+ * @param materialConfigs

Extra particle configurations for specific materials

+ * @param blocks

The blocks to spawn particles on

+ */ + public ParticleSpawner(@NotNull ParticleConfig particleConfig, + @NotNull Map materialConfigs, @NotNull Collection blocks) { + this.particleConfig = particleConfig; + this.materialConfigs = materialConfigs; + this.blocks = blocks; + this.blockSupplier = null; + + this.storedCalculationId = UUID.randomUUID(); + } + + /** + * Instantiates a new particle spawner + * + * @param particleConfig

The configuration for the particle to spawn

+ * @param materialConfigs

Extra particle configurations for specific materials

+ * @param blockSupplier

The supplier supplying the blocks to spawn particles on

+ */ + public ParticleSpawner(@NotNull ParticleConfig particleConfig, + @NotNull Map materialConfigs, + @NotNull Supplier> blockSupplier) { + this.particleConfig = particleConfig; + this.materialConfigs = materialConfigs; + this.blocks = null; + this.blockSupplier = blockSupplier; + + this.storedCalculationId = UUID.randomUUID(); + } + + /** + * Gets the id used for stored calculations + * + *

You should run ParticleHelper.clearStoredCalculations(id) with this id when discarding this particle spawner + * to prevent a memory leak.

+ * + * @return

The id used for stored calculations

+ */ + public UUID getStoredCalculationId() { + return this.storedCalculationId; + } + + @Override + public void run() { + // Use the static collection of blocks, or a dynamically changing supplier + Collection blocksToSpawnOn; + if (blocks != null) { + blocksToSpawnOn = blocks; + } else if (blockSupplier != null) { + blocksToSpawnOn = blockSupplier.get(); + } else { + throw new RuntimeException("There is a bug in the plugin code. Please contact the developer!"); + } + + for (Block block : blocksToSpawnOn) { + // Ignore blocks in unloaded chunks + if (!block.getChunk().isLoaded()) { + continue; + } + + Location location = block.getLocation().clone(); + World world = location.getWorld(); + if (world == null) { + continue; + } + + spawnParticleForBlock(block, world, location); + } + } + + /** + * Spawns the defined particle for the given block + * + * @param block

The block to spawn a particle effect for

+ * @param world

The world the block belongs to

+ * @param location

A clone of the block's location

+ */ + private void spawnParticleForBlock(@NotNull Block block, @NotNull World world, @NotNull Location location) { + // Store the currently processed block for height calculation + processingBlock = block; + + double blockHeight = ParticleHelper.getBlockHeight(block); + ParticleConfig activeConfig = getParticleConfig(); + + switch (getParticleConfig().getParticleMode()) { + case SINGLE -> ParticleHelper.spawnParticle(world, location.clone().add(0.5, + activeConfig.getHeightOffset(), 0.5), activeConfig, blockHeight); + case SQUARE -> ParticleHelper.drawSquare(world, location, activeConfig, blockHeight); + case CIRCLE -> ParticleHelper.drawCircle(world, location, activeConfig, blockHeight, storedCalculationId); + case PYRAMID -> ParticleHelper.drawPyramid(world, location, activeConfig, blockHeight, storedCalculationId); + case SPHERE -> ParticleHelper.drawSphere(world, location, activeConfig, blockHeight, storedCalculationId); + case CUBE -> ParticleHelper.drawCube(world, location, activeConfig, blockHeight); + } + } + + /** + * Gets the particle config to use for the current block's material + * + * @return

The particle config to use

+ */ + private ParticleConfig getParticleConfig() { + ParticleConfig materialConfig = this.materialConfigs.get(processingBlock.getType()); + if (materialConfig != null) { + return materialConfig; + } + return this.particleConfig; + } + +} diff --git a/src/main/java/net/knarcraft/knarlib/particle/ParticleTrailSpawner.java b/src/main/java/net/knarcraft/knarlib/particle/ParticleTrailSpawner.java new file mode 100644 index 0000000..642da57 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/particle/ParticleTrailSpawner.java @@ -0,0 +1,116 @@ +package net.knarcraft.knarlib.particle; + +import net.knarcraft.knarlib.util.ParticleHelper; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.UUID; + +/** + * A task for spawning trails behind players + */ +@SuppressWarnings("unused") +public class ParticleTrailSpawner implements Runnable { + + private final Set playersWithTrails = new HashSet<>(); + private final Map playerParticles = new HashMap<>(); + private final Random random = new Random(); + private final Particle particle; + private final boolean randomTrailType; + private final List randomTrailTypes; + + /** + * Instantiates a new particle trail spawner + * + * @param particle

The type of particle used for the trail

+ * @param randomTrailType

Whether to use a random trail type each time a player is launched

+ * @param randomTrailTypes

The types of particles to use for random trails

+ */ + public ParticleTrailSpawner(@NotNull Particle particle, boolean randomTrailType, + @NotNull List randomTrailTypes) { + this.particle = particle; + this.randomTrailType = randomTrailType; + this.randomTrailTypes = randomTrailTypes; + } + + @Override + public void run() { + Set offlinePlayers = new HashSet<>(); + for (UUID playerId : playersWithTrails) { + // Clear offline players from the list + Player player = Bukkit.getPlayer(playerId); + if (player == null) { + offlinePlayers.add(playerId); + continue; + } + + Location playerLocation = player.getLocation(); + World playerWorld = playerLocation.getWorld(); + if (playerWorld == null) { + continue; + } + + // Decide on which type of particle to spawn + Particle spawnParticle; + if (randomTrailType) { + spawnParticle = playerParticles.get(playerId); + if (spawnParticle == null) { + spawnParticle = this.particle; + } + } else { + spawnParticle = this.particle; + } + + // Spawn a trail particle + ParticleConfig particleConfig = new ParticleConfig(ParticleMode.SINGLE, spawnParticle, + 1, 1, 0, 0, 0, 0, 0); + ParticleHelper.spawnParticle(playerWorld, playerLocation, particleConfig, 0); + } + playersWithTrails.removeAll(offlinePlayers); + + } + + /** + * Removes the trail behind the player with the given id + * + * @param playerId

The id of the player to remove the trail for

+ */ + public void removeTrail(UUID playerId) { + this.playersWithTrails.remove(playerId); + this.playerParticles.remove(playerId); + } + + /** + * Adds a trail behind the player with the given id + * + * @param playerId

The id of the player to add the trail to

+ */ + public void startTrail(UUID playerId) { + this.playerParticles.put(playerId, randomParticle()); + this.playersWithTrails.add(playerId); + } + + /** + * Gets a random particle in the whitelist + * + * @return

A random particle

+ */ + private Particle randomParticle() { + Particle spawnParticle = null; + while (spawnParticle == null || spawnParticle.getDataType() != Void.class) { + spawnParticle = randomTrailTypes.get(random.nextInt(randomTrailTypes.size())); + } + return spawnParticle; + } + +} diff --git a/src/main/java/net/knarcraft/knarlib/util/ParticleHelper.java b/src/main/java/net/knarcraft/knarlib/util/ParticleHelper.java new file mode 100644 index 0000000..0ad6dc6 --- /dev/null +++ b/src/main/java/net/knarcraft/knarlib/util/ParticleHelper.java @@ -0,0 +1,242 @@ +package net.knarcraft.knarlib.util; + +import net.knarcraft.knarlib.particle.ParticleConfig; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.block.Block; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * A helper class for spawning particle effects + */ +@SuppressWarnings("unused") +public final class ParticleHelper { + + private static final Map pyramidVectors = new HashMap<>(); + private static final Map circleCoordinates = new HashMap<>(); + private static final Map sphereCoordinates = new HashMap<>(); + + private ParticleHelper() { + + } + + /** + * Removes any stored calculations for the given id + * + *

If you frequently change the id without clearing, you'll create a memory leak!

+ * + * @param id

The id specified when generating a shape

+ */ + public static void clearStoredCalculations(UUID id) { + pyramidVectors.remove(id); + circleCoordinates.remove(id); + sphereCoordinates.remove(id); + } + + /** + * Spawns a cube of particles at the given location + * + *

It is recommended to only spawn one particle with no spread, and a density between 1 and 0.01

+ * + * @param world

The world to spawn the particles in

+ * @param location

The location of the block to spawn the particles at

+ * @param particleConfig

The configuration describing the particle to spawn

+ * @param blockHeight

The height of the block to spawn the particle above

+ */ + public static void drawCube(@NotNull World world, @NotNull Location location, @NotNull ParticleConfig particleConfig, + double blockHeight) { + // Draw the top and bottom of the cube + drawSquare(world, location, particleConfig, blockHeight); + drawSquare(world, location.clone().add(0, 1, 0), particleConfig, blockHeight); + + for (float y = 0; y <= 1; y += particleConfig.getParticleDensity()) { + double height = particleConfig.getHeightOffset() + y; + spawnParticle(world, location.clone().add(0, height, 0), particleConfig, blockHeight); + spawnParticle(world, location.clone().add(0, height, 1), particleConfig, blockHeight); + spawnParticle(world, location.clone().add(1, height, 0), particleConfig, blockHeight); + spawnParticle(world, location.clone().add(1, height, 1), particleConfig, blockHeight); + } + } + + /** + * Spawns a sphere of particles at the given location + * + *

It is recommended to only spawn one particle with no spread, and a density between 4 and 0.1

+ * + * @param world

The world to spawn the particles in

+ * @param location

The location of the block to spawn the particles at

+ * @param particleConfig

The configuration describing the particle to spawn

+ * @param blockHeight

The height of the block to spawn the particle above

+ * @param storedCalculationsId

An id for stored calculations. Use clearStoredCalculations with this id later.

+ */ + public static void drawSphere(@NotNull World world, @NotNull Location location, @NotNull ParticleConfig particleConfig, + double blockHeight, UUID storedCalculationsId) { + // For spheres, densities below 0.1 has weird bugs such as blinking in and out of existence, and floating point + // errors when calculating the length of circleCoordinates + double density = Math.max(1, particleConfig.getParticleDensity()); + // Store calculations for improved efficiency + if (sphereCoordinates.get(storedCalculationsId) == null) { + int length = (int) Math.ceil((180 / density)); + Double[][] coordinates = new Double[length * 3][]; + double height = particleConfig.getHeightOffset() + 0.5; + int i = 0; + for (float x = 0; x < 180; x += density) { + if (i >= coordinates.length) { + continue; + } + + double cos = 0.5 * Math.cos(x); + double sin = 0.5 * Math.sin(x); + coordinates[i++] = new Double[]{sin + 0.5, height, cos + 0.5}; + coordinates[i++] = new Double[]{sin + 0.5, height + cos, 0.5}; + coordinates[i++] = new Double[]{0.5, height + sin, cos + 0.5}; + } + sphereCoordinates.put(storedCalculationsId, coordinates); + } + + // Spawn particles on the stored locations, relative to the launchpad + for (Double[] sphereCoordinate : sphereCoordinates.get(storedCalculationsId)) { + spawnParticle(world, location.clone().add(sphereCoordinate[0], sphereCoordinate[1], sphereCoordinate[2]), + particleConfig, blockHeight); + } + } + + /** + * Spawns a pyramid of particles at the given location + * + *

It is recommended to only spawn one particle with no spread, and a density between 1 and 0.01

+ * + * @param world

The world to spawn the particles in

+ * @param location

The location of the block to spawn the particles at

+ * @param particleConfig

The configuration describing the particle to spawn

+ * @param blockHeight

The height of the block to spawn the particle above

+ * @param storedCalculationsId

An id for stored calculations. Use clearStoredCalculations with this id later.

+ */ + public static void drawPyramid(@NotNull World world, @NotNull Location location, @NotNull ParticleConfig particleConfig, + double blockHeight, UUID storedCalculationsId) { + // Draw the bottom of the pyramid + drawSquare(world, location, particleConfig, blockHeight); + + // Store calculations for improved efficiency + if (pyramidVectors.get(storedCalculationsId) == null) { + // The 0.5 offsets are required for the angle of the pyramid's 4 lines to be correct + double height = particleConfig.getHeightOffset(); + double coordinateMin = -0.5 * height; + double coordinateMax = 1 + (0.5 * height); + + Vector[] vectors = new Vector[5]; + // The vector from the origin to the top of the pyramid + vectors[0] = new Vector(0.5, 1 + height, 0.5); + // The vectors from the top of the pyramid towards each corner + vectors[1] = new Vector(coordinateMin, 0, coordinateMin).subtract(vectors[0]).normalize(); + vectors[2] = new Vector(coordinateMax, 0, coordinateMin).subtract(vectors[0]).normalize(); + vectors[3] = new Vector(coordinateMin, 0, coordinateMax).subtract(vectors[0]).normalize(); + vectors[4] = new Vector(coordinateMax, 0, coordinateMax).subtract(vectors[0]).normalize(); + pyramidVectors.put(storedCalculationsId, vectors); + } + + Vector[] storedVectors = pyramidVectors.get(storedCalculationsId); + Location topLocation = location.clone().add(storedVectors[0]); + for (double x = 0; x <= 1.2; x += particleConfig.getParticleDensity()) { + spawnParticle(world, topLocation.clone().add(storedVectors[1].clone().multiply(x)), particleConfig, blockHeight); + spawnParticle(world, topLocation.clone().add(storedVectors[2].clone().multiply(x)), particleConfig, blockHeight); + spawnParticle(world, topLocation.clone().add(storedVectors[3].clone().multiply(x)), particleConfig, blockHeight); + spawnParticle(world, topLocation.clone().add(storedVectors[4].clone().multiply(x)), particleConfig, blockHeight); + } + } + + /** + * Spawns a circle of particles at the given location + * + *

It is recommended to only spawn one particle with no spread, and a density between 4 and 0.1

+ * + * @param world

The world to spawn the particles in

+ * @param location

The location of the block to spawn the particles at

+ * @param particleConfig

The configuration describing the particle to spawn

+ * @param blockHeight

The height of the block to spawn the particle above

+ * @param storedCalculationsId

An id for stored calculations. Use clearStoredCalculations with this id later.

+ */ + public static void drawCircle(@NotNull World world, @NotNull Location location, @NotNull ParticleConfig particleConfig, + double blockHeight, UUID storedCalculationsId) { + // For circles, densities below 0.1 has weird bugs such as blinking in and out of existence, and floating point + // errors when calculating the length of circleCoordinates + double density = Math.max(1, particleConfig.getParticleDensity()); + // Store calculations for improved efficiency + if (circleCoordinates.get(storedCalculationsId) == null) { + Double[][] coordinates = new Double[(int) Math.ceil((180 / density))][]; + int i = 0; + for (float x = 0; x < 180; x += density) { + if (i >= coordinates.length) { + continue; + } + coordinates[i++] = new Double[]{(0.5 * Math.sin(x)) + 0.5, (0.5 * Math.cos(x)) + 0.5}; + } + circleCoordinates.put(storedCalculationsId, coordinates); + } + + // Spawn particles on the stored locations, relative to the launchpad + for (Double[] circleCoordinate : circleCoordinates.get(storedCalculationsId)) { + spawnParticle(world, location.clone().add(circleCoordinate[0], particleConfig.getHeightOffset(), + circleCoordinate[1]), particleConfig, blockHeight); + } + } + + /** + * Spawns a square of particles at the given location + * + *

It is recommended to only spawn one particle with no spread, and a density between 1 and 0.01

+ * + * @param world

The world to spawn the particles in

+ * @param location

The location of the block to spawn the particles at

+ * @param particleConfig

The configuration describing the particle to spawn

+ * @param blockHeight

The height of the block to spawn the particle above

+ */ + public static void drawSquare(@NotNull World world, @NotNull Location location, @NotNull ParticleConfig particleConfig, + double blockHeight) { + for (float x = 0; x <= 1; x += particleConfig.getParticleDensity()) { + spawnParticle(world, location.clone().add(x, particleConfig.getHeightOffset(), 0), particleConfig, blockHeight); + spawnParticle(world, location.clone().add(x, particleConfig.getHeightOffset(), 1), particleConfig, blockHeight); + spawnParticle(world, location.clone().add(0, particleConfig.getHeightOffset(), x), particleConfig, blockHeight); + spawnParticle(world, location.clone().add(1, particleConfig.getHeightOffset(), x), particleConfig, blockHeight); + } + } + + /** + * Spawns the specified particle at the given location + * + * @param world

The world to spawn the particle in

+ * @param location

The location to spawn the particle at

+ * @param particleConfig

The configuration describing the particle to spawn

+ * @param blockHeight

The height of the block to spawn the particle above

+ */ + public static void spawnParticle(@NotNull World world, @NotNull Location location, + @NotNull ParticleConfig particleConfig, double blockHeight) { + world.spawnParticle(particleConfig.getParticleType(), + location.add(0, blockHeight, 0), particleConfig.getParticleAmount(), + particleConfig.getOffsetX(), particleConfig.getOffsetY(), particleConfig.getOffsetZ(), + particleConfig.getExtra()); + } + + /** + * Gets the height of the block at the given location + * + * @param block

The block to check

+ * @return

The height of the block

+ */ + public static double getBlockHeight(Block block) { + double maxY = 0; + for (BoundingBox boundingBox : block.getCollisionShape().getBoundingBoxes()) { + if (boundingBox.getMaxY() > maxY) { + maxY = boundingBox.getMaxY(); + } + } + return maxY; + } + +}