Add the trail particle with duration and target

Trail particles fly from where they spawn to a target offset over the
chosen number of ticks, in the entry's colour. The target follows the
body like other offsets when the entry is placed relative to it.
This commit is contained in:
2026-10-26 10:10:17 +01:00
parent 8009098965
commit aef26b5e9f
8 changed files with 58 additions and 16 deletions
@@ -138,6 +138,10 @@ PARTICLE_PARAM_SPREAD_Z = §fSpread Z
PARTICLE_PARAM_SPEED = §fSpeed
PARTICLE_PARAM_COLORS = §fColours
PARTICLE_PARAM_BLOCK = §fBlock
PARTICLE_PARAM_TRAIL_DURATION = §fTrail duration (ticks)
PARTICLE_PARAM_TRAIL_TARGET_X = §fTrail target X
PARTICLE_PARAM_TRAIL_TARGET_Y = §fTrail target Y
PARTICLE_PARAM_TRAIL_TARGET_Z = §fTrail target Z
PARTICLE_PARAM_OFFSET_X = §fOffset X
PARTICLE_PARAM_OFFSET_Y = §fOffset Y
PARTICLE_PARAM_OFFSET_Z = §fOffset Z
@@ -193,6 +197,7 @@ PARTICLE_TYPE_BLOCK_MARKER = §fBlock marker
PARTICLE_TYPE_DUST_PILLAR = §fDust pillar
PARTICLE_TYPE_FALLING_DUST = §fFalling dust
PARTICLE_TYPE_ITEM = §fItem breaking
PARTICLE_TYPE_TRAIL = §fTrail
PARTICLE_TYPE_FLAME = §fFlame
PARTICLE_TYPE_SMALL_FLAME = §fSmall flame
PARTICLE_TYPE_SOUL_FIRE_FLAME = §fSoul fire flame
@@ -138,6 +138,10 @@ PARTICLE_PARAM_SPREAD_Z = §fStreuung Z
PARTICLE_PARAM_SPEED = §fGeschwindigkeit
PARTICLE_PARAM_COLORS = §fFarben
PARTICLE_PARAM_BLOCK = §fBlock
PARTICLE_PARAM_TRAIL_DURATION = §fSpurdauer (Ticks)
PARTICLE_PARAM_TRAIL_TARGET_X = §fSpurziel X
PARTICLE_PARAM_TRAIL_TARGET_Y = §fSpurziel Y
PARTICLE_PARAM_TRAIL_TARGET_Z = §fSpurziel Z
PARTICLE_PARAM_OFFSET_X = §fVersatz X
PARTICLE_PARAM_OFFSET_Y = §fVersatz Y
PARTICLE_PARAM_OFFSET_Z = §fVersatz Z
@@ -193,6 +197,7 @@ PARTICLE_TYPE_BLOCK_MARKER = §fBlockmarkierung
PARTICLE_TYPE_DUST_PILLAR = §fStaubsäule
PARTICLE_TYPE_FALLING_DUST = §fFallender Staub
PARTICLE_TYPE_ITEM = §fGegenstandssplitter
PARTICLE_TYPE_TRAIL = §fSpur
PARTICLE_TYPE_FLAME = §fFlamme
PARTICLE_TYPE_SMALL_FLAME = §fKleine Flamme
PARTICLE_TYPE_SOUL_FIRE_FLAME = §fSeelenfeuerflamme
@@ -1 +0,0 @@
Relative zur rotation den spielers und co
@@ -189,6 +189,11 @@ public class EntryParameters {
public final DoubleParameter OFFSET_Y = new DoubleParameter("offsetY", "PARTICLE_PARAM_OFFSET_Y", 0, -3, 3, 0.1);
public final DoubleParameter OFFSET_Z = new DoubleParameter("offsetZ", "PARTICLE_PARAM_OFFSET_Z", 0, -3, 3, 0.1);
public final IntParameter INTERVAL = new IntParameter("interval", "PARTICLE_PARAM_INTERVAL", 1, 1, 100);
public final IntParameter TRAIL_DURATION = new IntParameter("trailDuration", "PARTICLE_PARAM_TRAIL_DURATION", 20, 1, 200);
// Where a trail particle flies to, relative to where it spawns
public final DoubleParameter TRAIL_TARGET_X = new DoubleParameter("trailTargetX", "PARTICLE_PARAM_TRAIL_TARGET_X", 0, -5, 5, 0.1);
public final DoubleParameter TRAIL_TARGET_Y = new DoubleParameter("trailTargetY", "PARTICLE_PARAM_TRAIL_TARGET_Y", 1, -5, 5, 0.1);
public final DoubleParameter TRAIL_TARGET_Z = new DoubleParameter("trailTargetZ", "PARTICLE_PARAM_TRAIL_TARGET_Z", 0, -5, 5, 0.1);
public final ChoiceParameter<Placement> PLACEMENT = ChoiceParameter.ofEnum("placement", "PARTICLE_PARAM_PLACEMENT", Placement.WORLD);
public final ChoiceParameter<TickMode> TICK_MODE = ChoiceParameter.ofEnum("tickMode", "PARTICLE_PARAM_TICK_MODE", TickMode.MOVE);
public final ChoiceParameter<Movement> MOVEMENT = ChoiceParameter.ofEnum("movement", "PARTICLE_PARAM_MOVEMENT", Movement.ANY);
@@ -199,6 +204,7 @@ public class EntryParameters {
public final List<Parameter<?>> ALL = List.of(
PARTICLE, COUNT, SPREAD_X, SPREAD_Y, SPREAD_Z, SPEED, COLORS, BLOCK,
TRAIL_DURATION, TRAIL_TARGET_X, TRAIL_TARGET_Y, TRAIL_TARGET_Z,
OFFSET_X, OFFSET_Y, OFFSET_Z, PLACEMENT, INTERVAL, TICK_MODE,
MOVEMENT, GLIDING, GROUND, SNEAK, VISIBILITY
);
@@ -25,7 +25,7 @@ import org.bukkit.Material;
import org.bukkit.Particle;
// Left out: ELDER_GUARDIAN, FLASH and the explosion/gust emitters cover other players' screens,
// BUBBLE, BUBBLE_COLUMN_UP and CURRENT_DOWN only show under water, VIBRATION and TRAIL need a target
// BUBBLE, BUBBLE_COLUMN_UP and CURRENT_DOWN only show under water, VIBRATION needs a block or entity to fly to
@AllArgsConstructor
@Getter
public enum ParticleType {
@@ -40,6 +40,7 @@ public enum ParticleType {
DUST_PILLAR(Material.MUD, Particle.DUST_PILLAR, Data.BLOCK, true),
FALLING_DUST(Material.SAND, Particle.FALLING_DUST, Data.BLOCK, true),
ITEM(Material.ITEM_FRAME, Particle.ITEM, Data.ITEM, true),
TRAIL(Material.LEAD, Particle.TRAIL, Data.TRAIL, true),
FLAME(Material.FLINT_AND_STEEL, Particle.FLAME, Data.NONE, true),
SMALL_FLAME(Material.TORCH, Particle.SMALL_FLAME, Data.NONE, true),
@@ -153,6 +154,8 @@ public enum ParticleType {
BLOCK,
// An item of the entry's block
ITEM,
// Flies to the entry's trail target in its colour, taking the trail duration
TRAIL,
ZERO_INT,
ZERO_FLOAT,
}
@@ -171,7 +174,11 @@ public enum ParticleType {
}
public boolean usesColors() {
return data == Data.DUST || data == Data.COLOR || data == Data.COLOR_TRANSITION;
return data == Data.DUST || data == Data.COLOR || data == Data.COLOR_TRANSITION || data == Data.TRAIL;
}
public boolean isTrail() {
return data == Data.TRAIL;
}
public boolean usesBlock() {
@@ -26,9 +26,9 @@ import org.bukkit.Color;
import org.bukkit.Location;
import org.bukkit.Particle;
import java.util.function.Function;
import java.util.function.BiFunction;
// A particle that needs extra data, computed per tick so colours can follow a gradient
// A particle that needs extra data, computed per tick from the tick and spawn location so colours can follow a gradient
public class DataParticle implements ParticleElement {
private final Particle particle;
@@ -37,9 +37,9 @@ public class DataParticle implements ParticleElement {
private final float vz;
private final double speed;
private final int count;
private final Function<ParticleTickData, Object> data;
private final BiFunction<ParticleTickData, Location, Object> data;
public DataParticle(Particle particle, float vx, float vy, float vz, double speed, int count, Function<ParticleTickData, Object> data) {
public DataParticle(Particle particle, float vx, float vy, float vz, double speed, int count, BiFunction<ParticleTickData, Location, Object> data) {
this.particle = particle;
this.vx = vx;
this.vy = vy;
@@ -61,7 +61,7 @@ public class DataParticle implements ParticleElement {
@Override
public void tick(ParticleTickData particleTickData) {
Location location = particleTickData.getLocation().add(0.0, 0.2, 0.0);
Object value = data.apply(particleTickData);
Object value = data.apply(particleTickData, location);
display(particleTickData, player -> player.spawnParticle(particle, location, count, vx, vy, vz, speed, value));
}
}
@@ -70,6 +70,10 @@ class EntryEditor {
Map.entry(INTERVAL, Material.CLOCK),
Map.entry(TICK_MODE, Material.REPEATER),
Map.entry(PLACEMENT, Material.PLAYER_HEAD),
Map.entry(TRAIL_DURATION, Material.CLOCK),
Map.entry(TRAIL_TARGET_X, Material.LEAD),
Map.entry(TRAIL_TARGET_Y, Material.LEAD),
Map.entry(TRAIL_TARGET_Z, Material.LEAD),
Map.entry(MOVEMENT, Material.LEATHER_BOOTS),
Map.entry(GLIDING, Material.ELYTRA),
Map.entry(GROUND, Material.GRASS_BLOCK),
@@ -119,6 +123,12 @@ class EntryEditor {
} else if (type.usesBlock()) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 2, parameterItem(context, BLOCK));
}
if (type.isTrail()) {
List<Parameter<?>> trail = List.of(TRAIL_DURATION, TRAIL_TARGET_X, TRAIL_TARGET_Y, TRAIL_TARGET_Z);
for (int i = 0; i < trail.size(); i++) {
inventory.setItem(ROW_DESIGN * 9 + FIRST_SETTING + 3 + i, parameterItem(context, trail.get(i)));
}
}
row(inventory, context, ROW_PARTICLE, List.of(COUNT, SPEED, SPREAD_X, SPREAD_Y, SPREAD_Z));
} else {
inventory.setItem(ROW_PARTICLE * 9 + FIRST_SETTING, new SWItem(Material.STRUCTURE_VOID, msg("PARTICLE_FIXED_LOOK", player), clickType -> {
@@ -27,15 +27,17 @@ import de.steamwar.lobby.particle.ParticleTickData;
import de.steamwar.lobby.particle.design.*;
import de.steamwar.lobby.particle.elements.*;
import lombok.Getter;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.Particle;
import org.bukkit.block.data.BlockData;
import org.bukkit.inventory.ItemStack;
import org.bukkit.util.Vector;
import java.awt.Color;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.BiFunction;
import static de.steamwar.lobby.particle.design.EntryParameters.*;
@@ -109,24 +111,32 @@ public class ParticleEntry {
List<Color> colors = values.get(COLORS);
Gradient gradient = colors.isEmpty() ? null : new Gradient(colors.toArray(new Color[0]));
Material block = values.get(BLOCK);
Function<ParticleTickData, Object> data = switch (type.getData()) {
BiFunction<ParticleTickData, Location, Object> data = switch (type.getData()) {
// Dust keeps its own element below, so presets look exactly as before
case NONE, DUST -> null;
case COLOR -> tick -> DataParticle.color(gradient, tick);
case COLOR_TRANSITION -> tick -> new Particle.DustTransition(
case COLOR -> (tick, location) -> DataParticle.color(gradient, tick);
case COLOR_TRANSITION -> (tick, location) -> new Particle.DustTransition(
colors.isEmpty() ? DataParticle.color(null, tick) : bukkitColor(colors.get(0)),
colors.size() < 2 ? DataParticle.color(null, tick) : bukkitColor(colors.get(1)),
1);
case BLOCK -> {
BlockData blockData = block.createBlockData();
yield tick -> blockData;
yield (tick, location) -> blockData;
}
case ITEM -> {
ItemStack item = new ItemStack(block);
yield tick -> item;
yield (tick, location) -> item;
}
case ZERO_INT -> tick -> 0;
case ZERO_FLOAT -> tick -> 0f;
case TRAIL -> {
int duration = values.get(TRAIL_DURATION);
Vector target = new Vector(values.get(TRAIL_TARGET_X), values.get(TRAIL_TARGET_Y), values.get(TRAIL_TARGET_Z));
// The target follows the body like offsets do when the entry is placed relative to it
yield (tick, location) -> new Particle.Trail(
location.clone().add(tick.getFrame() == null ? target : tick.getFrame().local(target.getX(), target.getY(), target.getZ())),
DataParticle.color(gradient, tick), duration);
}
case ZERO_INT -> (tick, location) -> 0;
case ZERO_FLOAT -> (tick, location) -> 0f;
};
if (type.isDust()) {