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 particleModeThe mode to use when spawning the particle
+ * @param particleTypeThe type of particle to spawn
+ * @param particleAmountThe amount of particles to spawn at once
+ * @param particleDensityThe density of the particles, if spawning a shape
+ * @param heightOffsetThe offset above the block to spawn the particle
+ * @param offsetXThe x-offset/spread of the spawned particles
+ * @param offsetYThe y-offset/spread of the spawned particles
+ * @param offsetZThe z-offset/spread of the spawned particles
+ * @param extraExtra 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) + * + * @returnThe particle mode
+ */ + public ParticleMode getParticleMode() { + return particleMode; + } + + /** + * The type of particle to spawn + * + * @returnThe particle type
+ */ + public Particle getParticleType() { + return particleType; + } + + /** + * The amount of particles to spawn + * + * @returnThe amount of particles
+ */ + public int getParticleAmount() { + return particleAmount; + } + + /** + * The density of particles to use in shapes closer to 0 causes larger density + * + * @returnThe particle density
+ */ + public double getParticleDensity() { + return particleDensity; + } + + /** + * The number of blocks above the block the particle(s) should spawn + * + * @returnThe y-offset
+ */ + public double getHeightOffset() { + return heightOffset; + } + + /** + * The offset/spread of particles in the x-direction + * + * @returnThe x-offset
+ */ + public double getOffsetX() { + return offsetX; + } + + /** + * The offset/spread of particles in the y-direction + * + * @returnThe y-offset
+ */ + public double getOffsetY() { + return offsetY; + } + + /** + * The offset/spread of particles in the z-direction + * + * @returnThe z-offset
+ */ + public double getOffsetZ() { + return offsetZ; + } + + /** + * The extra value to set for the particle. Exactly what it does depends on the particle. + * + * @returnThe 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 MapThe configuration for the particle to spawn
+ * @param materialConfigsExtra particle configurations for specific materials
+ * @param blocksThe blocks to spawn particles on
+ */ + public ParticleSpawner(@NotNull ParticleConfig particleConfig, + @NotNull MapThe configuration for the particle to spawn
+ * @param materialConfigsExtra particle configurations for specific materials
+ * @param blockSupplierThe supplier supplying the blocks to spawn particles on
+ */ + public ParticleSpawner(@NotNull ParticleConfig particleConfig, + @NotNull MapYou should run ParticleHelper.clearStoredCalculations(id) with this id when discarding this particle spawner + * to prevent a memory leak.
+ * + * @returnThe 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 + CollectionThe block to spawn a particle effect for
+ * @param worldThe world the block belongs to
+ * @param locationA 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 + * + * @returnThe 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 SetThe type of particle used for the trail
+ * @param randomTrailTypeWhether to use a random trail type each time a player is launched
+ * @param randomTrailTypesThe types of particles to use for random trails
+ */ + public ParticleTrailSpawner(@NotNull Particle particle, boolean randomTrailType, + @NotNull ListThe 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 playerIdThe 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 + * + * @returnA 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 MapIf you frequently change the id without clearing, you'll create a memory leak!
+ * + * @param idThe 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 worldThe world to spawn the particles in
+ * @param locationThe location of the block to spawn the particles at
+ * @param particleConfigThe configuration describing the particle to spawn
+ * @param blockHeightThe 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 worldThe world to spawn the particles in
+ * @param locationThe location of the block to spawn the particles at
+ * @param particleConfigThe configuration describing the particle to spawn
+ * @param blockHeightThe height of the block to spawn the particle above
+ * @param storedCalculationsIdAn 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 worldThe world to spawn the particles in
+ * @param locationThe location of the block to spawn the particles at
+ * @param particleConfigThe configuration describing the particle to spawn
+ * @param blockHeightThe height of the block to spawn the particle above
+ * @param storedCalculationsIdAn 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 worldThe world to spawn the particles in
+ * @param locationThe location of the block to spawn the particles at
+ * @param particleConfigThe configuration describing the particle to spawn
+ * @param blockHeightThe height of the block to spawn the particle above
+ * @param storedCalculationsIdAn 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 worldThe world to spawn the particles in
+ * @param locationThe location of the block to spawn the particles at
+ * @param particleConfigThe configuration describing the particle to spawn
+ * @param blockHeightThe 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 worldThe world to spawn the particle in
+ * @param locationThe location to spawn the particle at
+ * @param particleConfigThe configuration describing the particle to spawn
+ * @param blockHeightThe 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 blockThe block to check
+ * @returnThe 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; + } + +}