From 9df3e31aec269897c2fc77d29b7dd479ff638cbd Mon Sep 17 00:00:00 2001 From: sovde <10354869+sovdeeth@users.noreply.github.com> Date: Fri, 23 Feb 2024 22:56:47 +0100 Subject: [PATCH 01/16] Initial Planning --- .../bukkit/particles/ParticleEffect.java | 61 +++++++++++++++++++ .../skript/bukkit/particles/PlayerEffect.java | 26 ++++++++ .../elements/effects/EffPlayEffect.java | 13 ++++ .../expressions/ExprParticleEffect.java | 21 +++++++ .../expressions/ExprPlayerEffect.java | 6 ++ 5 files changed, 127 insertions(+) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java new file mode 100644 index 00000000000..820f7b85357 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java @@ -0,0 +1,61 @@ +package org.skriptlang.skript.bukkit.particles; + + +import org.bukkit.Particle; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; + +/** + * A class to hold particle metadata prior to spawning + */ +public class ParticleEffect { + + /** + * The base {@link Particle} to use. This determines the properties and what data this {@link ParticleEffect} can accept. + */ + private Particle particle; + + /** + * This determines how many particles to spawn with the given properties. If set to 0, {@link ParticleEffect#offset} may + * be used to determine things like colour or velocity, rather than the area where the particles spawn. + */ + private int count; + + /** + * This, by default, determines a bounding box around the spawn location in which particles are randomly offset. + * Dimensions are multiplied by roughly 8, meaning an offset of (1, 1, 1) results in particles spawning in an + * 8x8x8 cuboid centered on the spawn location. + * Particles are distributed following a Gaussian distribution, clustering towards the center. + *
+ * When {@link ParticleEffect#count} is 0, however, this may instead act as a velocity vector, an RGB colour, + * or determine the colour of a note particle. + * See the wiki on the particle command for more information. + */ + private Vector offset; + + /** + * This, by default, determines the speed at which a particle moves. It must be positive. + *
+ * When {@link ParticleEffect#count} is 0, this instead acts as a multiplier to the velocity provided by {@link ParticleEffect#offset}, + * or if {@link ParticleEffect#particle} is {@link Particle#SPELL_MOB_AMBIENT} or {@link Particle#SPELL_MOB}, then + * this acts as an exponent to the RGB value provided by {@link ParticleEffect#offset}. + */ + private float extra; + + /** + * This field contains extra data that some particles require. For example, {@link Particle#REDSTONE} requires {@link org.bukkit.Particle.DustOptions} + * to determine its size and colour. + */ + @Nullable + private Object data; + + ParticleEffect(Particle particle) { + this.particle = particle; + this.count = 1; + this.extra = 0; + this.offset = new Vector(0,0,0); + } + + // TODO: Add parent interface for ParticleEffect, PlayerEffect, EntityEffect? Would make spawning easier, maybe. + // TODO: add getters, setters, maybe builder class? Add spawn method. +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java new file mode 100644 index 00000000000..d83c3c42a81 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java @@ -0,0 +1,26 @@ +package org.skriptlang.skript.bukkit.particles; + +import org.bukkit.Effect; +import org.jetbrains.annotations.Nullable; + +/** + * A class to hold metadata about {@link org.bukkit.Effect}s before playing. + */ +public class PlayerEffect { + /** + * The {@link Effect} that this object represents + */ + private Effect effect; + + /** + * The optional extra data that some {@link Effect}s require. + */ + @Nullable + private Object data; + + PlayerEffect(Effect effect) { + this.effect = effect; + } + + // TODO: add getters, setters, maybe builder class? Add spawn method. +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java new file mode 100644 index 00000000000..f9d23ef1d00 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -0,0 +1,13 @@ +package org.skriptlang.skript.bukkit.particles.elements.effects; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.Effect; + +// TODO: better terminology than "effects", as it's getting confusing. +public class EffPlayEffect extends Effect { + static { + Skript.registerEffect(EffPlayEffect.class, + "[:force] (play|show|draw) [%number% [of]] %particles/player effects% (on|%directions%) %entities/locations% [(to %players%|in [a] (radius|range) of %number%)]", + "(play|show|draw) [%number% [of]] %particles/player effects/entity effects% (on|at) %entities%)"); + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java new file mode 100644 index 00000000000..78d1d8e32b4 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java @@ -0,0 +1,21 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.lang.util.SimpleExpression; +import org.skriptlang.skript.bukkit.particles.ParticleEffect; + +public class ExprParticleEffect extends SimpleExpression { + // TODO: + // Syntax: + // count + (name + "particle" + data) + offset + extra + // # count: + // %number% [of] + // # offset: + // [with [an]] offset (of|by) ((%number%, %number%(,|[,] and) %number%)|%vector%) + // # extra: + // [(at|with) [a]] (speed|extra [value]) [of] %number% + // This expression should handle the common elements between all particles + // Specific data should be handled by something more dynamic, since data can vary wildly. + // Consider VisualEffect approach, or SkBee approach of various functions. + // Prefer something like VisualEffect for better readability + grammar, but needs to be + // better documented this time. +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java new file mode 100644 index 00000000000..6cea0959a1f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java @@ -0,0 +1,6 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +public class ExprPlayerEffect { + // TODO: This class is for Bukkit's Effect enum, or at least the options that require data. + // Syntax unsure, since it still need to be dynamic like particles +} From 47ad2802118543b4d4dd34d44a9bee1b54de627b Mon Sep 17 00:00:00 2001 From: sovde <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:45:45 +0100 Subject: [PATCH 02/16] rename to GameEffect, create syntaxes for those that need data. --- .../{PlayerEffect.java => GameEffect.java} | 4 +-- .../elements/effects/EffPlayEffect.java | 4 +-- .../elements/expressions/ExprGameEffect.java | 29 +++++++++++++++++++ .../expressions/ExprPlayerEffect.java | 6 ---- 4 files changed, 33 insertions(+), 10 deletions(-) rename src/main/java/org/skriptlang/skript/bukkit/particles/{PlayerEffect.java => GameEffect.java} (89%) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java delete mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java similarity index 89% rename from src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java rename to src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index d83c3c42a81..a6e440dabe7 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/PlayerEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -6,7 +6,7 @@ /** * A class to hold metadata about {@link org.bukkit.Effect}s before playing. */ -public class PlayerEffect { +public class GameEffect { /** * The {@link Effect} that this object represents */ @@ -18,7 +18,7 @@ public class PlayerEffect { @Nullable private Object data; - PlayerEffect(Effect effect) { + GameEffect(Effect effect) { this.effect = effect; } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index f9d23ef1d00..1f521da8c99 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -7,7 +7,7 @@ public class EffPlayEffect extends Effect { static { Skript.registerEffect(EffPlayEffect.class, - "[:force] (play|show|draw) [%number% [of]] %particles/player effects% (on|%directions%) %entities/locations% [(to %players%|in [a] (radius|range) of %number%)]", - "(play|show|draw) [%number% [of]] %particles/player effects/entity effects% (on|at) %entities%)"); + "[:force] (play|show|draw) [%number% [of]] %particles/game effects% (on|%directions%) %entities/locations% [(to %players%|in [a] (radius|range) of %number%)]", + "(play|show|draw) [%number% [of]] %particles/game effects/entity effects% (on|at) %entities%)"); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java new file mode 100644 index 00000000000..51680e32c2e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java @@ -0,0 +1,29 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.util.SimpleExpression; +import org.skriptlang.skript.bukkit.particles.GameEffect; + +public class ExprGameEffect extends SimpleExpression { + static { + // TODO: register the rest via the type parser + // make sure to not parse the ones with data, so this class can handle it + Skript.registerExpression(ExprGameEffect.class, GameEffect.class, ExpressionType.COMBINED, + "[record] song (of|using) %item%", + "[dispenser] [black|:white] smoke effect [(in|with|using) direction] %direction/vector%", + "[foot]step sound [effect] (on|of|using) %item/blockdata%", + "[:instant] [splash] potion break effect (with|of|using) [colour] %color%", // paper changes this type + "composter fill[ing] (succe[ss|ed]|fail:fail[ure]) sound [effect]", + "villager plant grow[th] effect [(with|using) %number% particles]", + "[fake] bone meal effect [(with|using) %number% particles]", + // post copper update (1.19?) + "(electric|lightning[ rod]|copper) spark effect [(in|using) the (1:x|2:y|3:z) axis]", + // paper only + "sculk (charge|spread) effect [(with|using) data %number%]", + "[finish] brush[ing] effect (with|using) %item/blockdata%", + // 1.20.3 + "trial spawner detect[ing|s] [%number%] player[s] effect" + ); + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java deleted file mode 100644 index 6cea0959a1f..00000000000 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprPlayerEffect.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.skriptlang.skript.bukkit.particles.elements.expressions; - -public class ExprPlayerEffect { - // TODO: This class is for Bukkit's Effect enum, or at least the options that require data. - // Syntax unsure, since it still need to be dynamic like particles -} From 217ad6021df17afb288b850005764003e0a6b83a Mon Sep 17 00:00:00 2001 From: sovde <10354869+sovdeeth@users.noreply.github.com> Date: Mon, 10 Jun 2024 18:49:08 +0200 Subject: [PATCH 03/16] begin implementing GameEffects --- src/main/java/ch/njol/util/VectorMath.java | 21 ++++ .../skript/bukkit/particles/GameEffect.java | 53 ++++++++- .../bukkit/particles/elements/Types.java | 83 +++++++++++++ .../elements/expressions/ExprGameEffect.java | 110 +++++++++++++++--- src/main/resources/lang/default.lang | 87 ++++++++++++++ 5 files changed, 335 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java diff --git a/src/main/java/ch/njol/util/VectorMath.java b/src/main/java/ch/njol/util/VectorMath.java index fca9785dc6a..e38027c95d7 100644 --- a/src/main/java/ch/njol/util/VectorMath.java +++ b/src/main/java/ch/njol/util/VectorMath.java @@ -18,6 +18,7 @@ */ package ch.njol.util; +import org.bukkit.block.BlockFace; import org.bukkit.util.Vector; /** @@ -191,6 +192,26 @@ public static float wrapAngleDeg(float angle) { } } + /** + * Calculates the nearest {@link BlockFace} to an arbitrary unit {@link Vector}. + * + * @param vector a normalized vector + * @return the block face most closely aligned to the input vector + */ + public static BlockFace toNearestBlockFace(Vector vector) { + double maxDot = -1; + double dot; + BlockFace nearest = BlockFace.NORTH; + for (BlockFace face : BlockFace.values()){ + dot = face.getDirection().dot(vector); + if (dot > maxDot) { + maxDot = dot; + nearest = face; + } + } + return nearest; + } + /** * Copies vector components of {@code vector2} into {@code vector1}. */ diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index a6e440dabe7..143247dd977 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -1,16 +1,23 @@ package org.skriptlang.skript.bukkit.particles; +import ch.njol.skript.util.EnumUtils; import org.bukkit.Effect; import org.jetbrains.annotations.Nullable; +import java.util.Arrays; +import java.util.Locale; + /** * A class to hold metadata about {@link org.bukkit.Effect}s before playing. */ public class GameEffect { + + public static final EnumUtils ENUM_UTILS = new EnumUtils<>(Effect.class, "game effect"); + /** * The {@link Effect} that this object represents */ - private Effect effect; + private final Effect effect; /** * The optional extra data that some {@link Effect}s require. @@ -18,9 +25,49 @@ public class GameEffect { @Nullable private Object data; - GameEffect(Effect effect) { + public GameEffect(Effect effect) { this.effect = effect; } - // TODO: add getters, setters, maybe builder class? Add spawn method. + public static GameEffect parse(String input) { + Effect effect = ENUM_UTILS.parse(input.toLowerCase(Locale.ENGLISH)); + if (effect == null || effect.getData() != null) { + return null; + } + return new GameEffect(effect); + } + + public Effect getEffect() { + return effect; + } + + @Nullable + public Object getData() { + return data; + } + + public boolean setData(Object data) { + if (effect.getData() != null && effect.getData().isInstance(data)) { + this.data = data; + return true; + } + return false; + } + + public String toString(int flags) { + if (effect.getData() != null) + return ENUM_UTILS.toString(getEffect(), flags); + return toString(); + } + + static final String[] namesWithoutData = (String[]) Arrays.stream(Effect.values()) + .filter(effect -> effect.getData() == null) + .map(Enum::name) + .toArray(); + public static String[] getAllNamesWithoutData(){ + return namesWithoutData.clone(); + } + + +// TODO: add getters, setters, maybe builder class? Add spawn method. } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java new file mode 100644 index 00000000000..ad2aed9e6f2 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java @@ -0,0 +1,83 @@ +package org.skriptlang.skript.bukkit.particles.elements; + +import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.Parser; +import ch.njol.skript.classes.Serializer; +import ch.njol.skript.expressions.base.EventValueExpression; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +import ch.njol.yggdrasil.Fields; +import org.bukkit.Effect; +import org.eclipse.jdt.annotation.Nullable; +import org.skriptlang.skript.bukkit.particles.GameEffect; + +import java.io.NotSerializableException; +import java.io.StreamCorruptedException; + +public class Types { + static { + + Classes.registerClass(new ClassInfo<>(GameEffect.class, "gameeffect") + .user("game ?effects?") + .since("INSERT VERSION") + .description("Various game effects that can be played for players, like record disc songs, splash potions breaking, or fake bone meal effects.") + .name("Game Effect") + .usage(GameEffect.getAllNamesWithoutData()) + .serializer(new Serializer<>() { + @Override + public Fields serialize(GameEffect effect) { + Fields fields = new Fields(); + fields.putPrimitive("name", effect.getEffect().name()); + fields.putObject("data", effect.getData()); + return fields; + } + + @Override + public void deserialize(GameEffect effect, Fields fields) throws StreamCorruptedException, NotSerializableException { + assert false; + } + + @Override + protected GameEffect deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + String name = fields.getAndRemovePrimitive("name", String.class); + GameEffect effect; + try { + effect = new GameEffect(Effect.valueOf(name)); + } catch (IllegalArgumentException e) { + return null; + } + effect.setData(fields.getObject("data")); + return effect; + } + + @Override + public boolean mustSyncDeserialization() { + return false; + } + + @Override + protected boolean canBeInstantiated() { + return false; + } + }) + .defaultExpression(new EventValueExpression<>(GameEffect.class)) + .parser(new Parser<>() { + @Override + @Nullable + public GameEffect parse(String input, ParseContext context) { + return GameEffect.parse(input); + } + + @Override + public String toString(GameEffect effect, int flags) { + return effect.toString(flags); + } + + @Override + public String toVariableNameString(GameEffect o) { + return o.getEffect().name(); + } + })); + + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java index 51680e32c2e..1a81225b7f3 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java @@ -1,29 +1,107 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Direction; +import ch.njol.skript.util.Patterns; +import ch.njol.util.Kleenean; +import ch.njol.util.VectorMath; +import org.bukkit.Effect; +import org.bukkit.block.BlockFace; +import org.bukkit.event.Event; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; public class ExprGameEffect extends SimpleExpression { + + private static final Patterns PATTERNS = new Patterns<>(new Object[][]{ + {"[record] song (of|using) %item%", Effect.RECORD_PLAY}, // shows the action bar too! + {"[dispenser] [black|1:white] smoke effect [(in|with|using) direction] %direction/vector%", Effect.SMOKE}, + {"[foot]step sound [effect] (on|of|using) %item/blockdata%", Effect.STEP_SOUND}, + {"[:instant] [splash] potion break effect (with|of|using) [colour] %color%", Effect.POTION_BREAK}, // paper changes this type + {"composter fill[ing] (succe[ss|ed]|1:fail[ure]) sound [effect]", Effect.COMPOSTER_FILL_ATTEMPT}, + {"villager plant grow[th] effect [(with|using) %number% particles]", Effect.VILLAGER_PLANT_GROW}, + {"[fake] bone meal effect [(with|using) %number% particles]", Effect.BONE_MEAL_USE}, + // post copper update (1.19?) + {"(electric|lightning[ rod]|copper) spark effect [(in|using) the (1:x|2:y|3:z) axis]", Effect.ELECTRIC_SPARK}, + // paper only + {"sculk (charge|spread) effect [(with|using) data %number%]", Effect.PARTICLES_SCULK_CHARGE}, // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 + {"[finish] brush[ing] effect (with|using) %item/blockdata%", Effect.PARTICLES_AND_SOUND_BRUSH_BLOCK_COMPLETE}, + // 1.20.3 + {"trial spawner detect[ing|s] [%number%] player[s] effect", Effect.TRIAL_SPAWNER_DETECT_PLAYER} + }); + static { // TODO: register the rest via the type parser // make sure to not parse the ones with data, so this class can handle it - Skript.registerExpression(ExprGameEffect.class, GameEffect.class, ExpressionType.COMBINED, - "[record] song (of|using) %item%", - "[dispenser] [black|:white] smoke effect [(in|with|using) direction] %direction/vector%", - "[foot]step sound [effect] (on|of|using) %item/blockdata%", - "[:instant] [splash] potion break effect (with|of|using) [colour] %color%", // paper changes this type - "composter fill[ing] (succe[ss|ed]|fail:fail[ure]) sound [effect]", - "villager plant grow[th] effect [(with|using) %number% particles]", - "[fake] bone meal effect [(with|using) %number% particles]", - // post copper update (1.19?) - "(electric|lightning[ rod]|copper) spark effect [(in|using) the (1:x|2:y|3:z) axis]", - // paper only - "sculk (charge|spread) effect [(with|using) data %number%]", - "[finish] brush[ing] effect (with|using) %item/blockdata%", - // 1.20.3 - "trial spawner detect[ing|s] [%number%] player[s] effect" - ); + Skript.registerExpression(ExprGameEffect.class, GameEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); + } + + private GameEffect gameEffect; + private Expression data; + private int variant; + + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + gameEffect = new GameEffect(PATTERNS.getInfo(matchedPattern)); + variant = parseResult.mark; + if (expressions.length > 0) + data = expressions[0]; + return true; + } + + @Override + protected GameEffect @Nullable [] get(Event event) { + switch (gameEffect.getEffect()) { + case SMOKE: + return handleSmokeEffects(gameEffect, variant, data.getSingle(event)); + default: + return setData(gameEffect, data.getSingle(event)); + } + } + + private GameEffect @Nullable [] handleSmokeEffects(GameEffect gameEffect, int variant, Object data) { + BlockFace blockFace; + if (data instanceof Vector) { + Vector vector = ((Vector) data); + if (vector.isZero()) + return new GameEffect[0]; + blockFace = VectorMath.toNearestBlockFace(vector.normalize()); + } else if (data instanceof Direction) { + Vector vector = ((Direction) data).getDirection(); + // what about relative directions + } + if (variant == 1) + gameEffect = new GameEffect(Effect.SHOOT_WHITE_SMOKE); + return setData(gameEffect, data); + } + + private GameEffect @Nullable [] setData(GameEffect gameEffect, Object data){ + if (data == null) + return new GameEffect[0]; // invalid data, must return nothing. + boolean success = gameEffect.setData(data); + if (!success) + return new GameEffect[0]; // invalid data + return new GameEffect[]{gameEffect}; + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return null; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return null; } } diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index efd0651babf..8275baeed5a 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -1980,6 +1980,93 @@ transform reasons: unknown: unknown infection: infection, villager infection +# -- Game Effects -- +game effects: + click1: + name: dispenser click sound + pattern: + enums: click1 + click2: menu click sound, gui click sound + bow_fire: fire bow sound, shoot bow sound + extinguish: extinguish fire sound, extinguish sound + ghast_shriek: ghast shriek sound + ghast_shoot: ghast shoot sound + blaze_shoot: blaze shoot sound + zombie_chew_wooden_door: zombie chewing on wooden door sound, zombie attacking wooden door sound + zombie_chew_iron_door: zombie chewing on iron door sound, zombie attacking iron door sound + zombie_destroy_door: zombie destroy door sound, zombie destroying door sound + ender_signal: ender eye break, eye of ender break, ender eye breaking, eye of ender breaking + mobspawner_flames: mob spawner flames, spawner flames + brewing_stand_brew: brewing sound, brewing stand sound + chorus_flower_grow: chorus flower growing sound, chorus plant growing sound + chorus_flower_death: chorus flower death sound + portal_travel: nether portal travel sound + endereye_launch: ender eye launch sound, eye of ender launch sound + firework_shoot: dispenser shoot sound + dragon_breath: dragon breath, dragon's breath + anvil_break: break anvil sound + anvil_use: use anvil sound + anvil_land: anvil land sound + enderdragon_shoot: ender dragon shoot sound, dragon shoot sound + enderdragon_growl: ender dragon growl sound, dragon growl sound + wither_break_block: wither breaking block sound + wither_shoot: wither shoot sound + zombie_infect: zombie infection sound + zombie_converted_villager: zombie converting villager sound + zombie_converted_to_drowned: zombie conversion to drowned sound + zombie_converts_to_drowned: zombie conversion to drowned sound + husk_converted_to_zombie: husk conversion to zombie sound + husk_converts_to_zombie: husk conversion to zombie sound + skeleton_converted_to_stray: skeleton conversion to stray sound + bat_takeoff: bat take off sound, bat take flight sound + end_gateway_spawn: end gateway spawn, new end gateway + phantom_bite: phantom bite sound + phantom_bites: phantom bites sound + grindstone_use: use grindstone sound + grindstone_used: used grindstone sound + book_page_turn: book page turn sound, turn page sound + book_page_turned: book page turned sound, turned page sound + smithing_table_use: use smithing table sound + pointed_dripstone_drip_lava_into_cauldron: lava dripping into cauldron sound + pointed_dripstone_drip_water_into_cauldron: water dripping into cauldron sound + dripping_dripstone: water dripping sound, lava dripping sound + lava_interact: lava interaction + lava_converts_block: lava converts block + redstone_torch_burnout: redstone touch burnout + redstone_torch_burns_out: redstone touch burns out + end_portal_frame_fill: fill end portal frame + ender_eye_placed: ender eye placed + ender_dragon_destroy_block: ender dragon breaking block, dragon breaking block + ender_dragon_destroys_block: ender dragon destroys block, dragon destroys block + sponge_dry: sponge dry, sponge dry out + copper_wax_on: apply wax, wax on + copper_wax_off: remove wax, wax off + oxidised_copper_scrape: scrape oxidised copper, scrape copper + wither_spawned: spawn wither sound + ender_dragon_death: ender dragon death sound, dragon death sound + end_portal_created_in_overworld: complete end portal sound, create end portal sound + composter_composts: composter compost + + + + + + + + + # these effect are deprecated in 1.19, they do nothing. + # their respective Sounds should be used instead. + door_toggle: toggle door sound + iron_door_toggle: toggle iron door sound + trapdoor_toggle: toggle trapdoor sound + iron_trapdoor_toggle: toggle iron trapdoor sound + fence_gate_toggle: toggle fence gate sound + door_close: close door sound + iron_door_close: close iron door sound + trapdoor_close: close trapdoor sound + iron_trapdoor_close: close iron trapdoor sound + fence_gate_close: close fence gate sound + # -- Boolean -- boolean: true: From 60415e5d1171d7c905abc98193864af52dc2060e Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 19 Apr 2025 14:01:21 -0400 Subject: [PATCH 04/16] Mostly finish GameEffects --- src/main/java/ch/njol/skript/Skript.java | 2 + .../java/ch/njol/skript/util/Direction.java | 20 ++ src/main/java/ch/njol/util/VectorMath.java | 21 -- .../skript/bukkit/particles/GameEffect.java | 35 ++- .../bukkit/particles/ParticleEffect.java | 14 ++ .../Types.java => ParticleModule.java} | 14 +- .../elements/effects/EffPlayEffect.java | 106 +++++++- .../elements/expressions/ExprGameEffect.java | 226 ++++++++++++++---- .../expressions/ExprParticleEffect.java | 29 +++ src/main/resources/lang/default.lang | 128 +++++----- 10 files changed, 446 insertions(+), 149 deletions(-) rename src/main/java/org/skriptlang/skript/bukkit/particles/{elements/Types.java => ParticleModule.java} (88%) diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 27a75d44e4d..501c9f420ae 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -79,6 +79,7 @@ import org.skriptlang.skript.bukkit.input.InputModule; import org.skriptlang.skript.bukkit.log.runtime.BukkitRuntimeErrorConsumer; import org.skriptlang.skript.bukkit.loottables.LootTableModule; +import org.skriptlang.skript.bukkit.particles.ParticleModule; import org.skriptlang.skript.bukkit.registration.BukkitRegistryKeys; import org.skriptlang.skript.bukkit.registration.BukkitSyntaxInfos; import org.skriptlang.skript.bukkit.tags.TagModule; @@ -560,6 +561,7 @@ public void onEnable() { TagModule.load(); FurnaceModule.load(); LootTableModule.load(); + ParticleModule.load(); } catch (final Exception e) { exception(e, "Could not load required .class files: " + e.getLocalizedMessage()); setEnabled(false); diff --git a/src/main/java/ch/njol/skript/util/Direction.java b/src/main/java/ch/njol/skript/util/Direction.java index d2f8e8fdf8c..178ea6d8a3d 100644 --- a/src/main/java/ch/njol/skript/util/Direction.java +++ b/src/main/java/ch/njol/skript/util/Direction.java @@ -264,6 +264,26 @@ public static Location[] getRelatives(final Location[] locations, final Directio } return r; } + + /** + * Calculates the nearest {@link BlockFace} to an arbitrary unit {@link Vector}. + * + * @param vector a normalized vector + * @return the block face most closely aligned to the input vector + */ + public static BlockFace toNearestBlockFace(Vector vector) { + double maxDot = -1; + double dot; + BlockFace nearest = BlockFace.NORTH; + for (BlockFace face : BlockFace.values()){ + dot = face.getDirection().dot(vector); + if (dot > maxDot) { + maxDot = dot; + nearest = face; + } + } + return nearest; + } @Override public String toString() { diff --git a/src/main/java/ch/njol/util/VectorMath.java b/src/main/java/ch/njol/util/VectorMath.java index 61f42b19183..9c937989a97 100644 --- a/src/main/java/ch/njol/util/VectorMath.java +++ b/src/main/java/ch/njol/util/VectorMath.java @@ -4,7 +4,6 @@ import ch.njol.skript.expressions.ExprVectorFromYawAndPitch; import ch.njol.skript.expressions.ExprVectorSpherical; import ch.njol.skript.expressions.ExprYawPitch; -import org.bukkit.block.BlockFace; import org.bukkit.util.Vector; @Deprecated(forRemoval = true) @@ -107,26 +106,6 @@ public static float wrapAngleDeg(float angle) { return ExprVectorFromYawAndPitch.wrapAngleDeg(angle); } - /** - * Calculates the nearest {@link BlockFace} to an arbitrary unit {@link Vector}. - * - * @param vector a normalized vector - * @return the block face most closely aligned to the input vector - */ - public static BlockFace toNearestBlockFace(Vector vector) { - double maxDot = -1; - double dot; - BlockFace nearest = BlockFace.NORTH; - for (BlockFace face : BlockFace.values()){ - dot = face.getDirection().dot(vector); - if (dot > maxDot) { - maxDot = dot; - nearest = face; - } - } - return nearest; - } - /** * Copies vector components of {@code vector2} into {@code vector1}. */ diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index 143247dd977..6bfbf0ce789 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -2,6 +2,10 @@ import ch.njol.skript.util.EnumUtils; import org.bukkit.Effect; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.Arrays; @@ -12,6 +16,7 @@ */ public class GameEffect { + // TODO: fix missing key error for effects that require data public static final EnumUtils ENUM_UTILS = new EnumUtils<>(Effect.class, "game effect"); /** @@ -54,20 +59,46 @@ public boolean setData(Object data) { return false; } + /** + * Plays the effect at the given location. The given location must have a world. + * @param location the location to play the effect at + * @param radius the radius to play the effect in, or null to use the default radius + */ + public void draw(@NotNull Location location, @Nullable Number radius) { + World world = location.getWorld(); + if (world == null) + return; + if (radius == null) { + location.getWorld().playEffect(location, effect, data); + } else { + location.getWorld().playEffect(location, effect, data, radius.intValue()); + } + } + + /** + * Plays the effect for the given player. + * @param location the location to play the effect at + * @param player the player to play the effect for + */ + public void drawForPlayer(Location location, @NotNull Player player) { + player.playEffect(location, effect, data); + } + public String toString(int flags) { if (effect.getData() != null) return ENUM_UTILS.toString(getEffect(), flags); return toString(); } - static final String[] namesWithoutData = (String[]) Arrays.stream(Effect.values()) + static final String[] namesWithoutData = Arrays.stream(Effect.values()) .filter(effect -> effect.getData() == null) .map(Enum::name) - .toArray(); + .toArray(String[]::new); public static String[] getAllNamesWithoutData(){ return namesWithoutData.clone(); } + // TODO: add getters, setters, maybe builder class? Add spawn method. } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java index 820f7b85357..a92dfbd96df 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java @@ -1,7 +1,10 @@ package org.skriptlang.skript.bukkit.particles; +import org.bukkit.Location; import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.entity.Player; import org.bukkit.util.Vector; import org.jetbrains.annotations.Nullable; @@ -56,6 +59,17 @@ public class ParticleEffect { this.offset = new Vector(0,0,0); } + public void draw(Location location, boolean force) { + World world = location.getWorld(); + if (world == null) + return; + world.spawnParticle(particle, location, count, offset.getX(), offset.getY(), offset.getZ(), extra, data, force); + } + + public void drawForPlayer(Location location, Player player, boolean force) { + player.spawnParticle(particle, location, count, offset.getX(), offset.getY(), offset.getZ(), extra, data, force); + } + // TODO: Add parent interface for ParticleEffect, PlayerEffect, EntityEffect? Would make spawning easier, maybe. // TODO: add getters, setters, maybe builder class? Add spawn method. } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java similarity index 88% rename from src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java rename to src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index ad2aed9e6f2..54af1d93227 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/Types.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -1,5 +1,6 @@ -package org.skriptlang.skript.bukkit.particles.elements; +package org.skriptlang.skript.bukkit.particles; +import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.Parser; import ch.njol.skript.classes.Serializer; @@ -8,15 +9,14 @@ import ch.njol.skript.registrations.Classes; import ch.njol.yggdrasil.Fields; import org.bukkit.Effect; -import org.eclipse.jdt.annotation.Nullable; -import org.skriptlang.skript.bukkit.particles.GameEffect; +import java.io.IOException; import java.io.NotSerializableException; import java.io.StreamCorruptedException; -public class Types { - static { +public class ParticleModule { + public static void load () throws IOException { Classes.registerClass(new ClassInfo<>(GameEffect.class, "gameeffect") .user("game ?effects?") .since("INSERT VERSION") @@ -63,7 +63,6 @@ protected boolean canBeInstantiated() { .defaultExpression(new EventValueExpression<>(GameEffect.class)) .parser(new Parser<>() { @Override - @Nullable public GameEffect parse(String input, ParseContext context) { return GameEffect.parse(input); } @@ -79,5 +78,8 @@ public String toVariableNameString(GameEffect o) { } })); + Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); + } + } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index 1f521da8c99..74810b0e3b3 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -2,12 +2,114 @@ import ch.njol.skript.Skript; import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.util.Direction; +import ch.njol.util.Kleenean; +import org.bukkit.EntityEffect; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.GameEffect; +import org.skriptlang.skript.bukkit.particles.ParticleEffect; // TODO: better terminology than "effects", as it's getting confusing. public class EffPlayEffect extends Effect { static { Skript.registerEffect(EffPlayEffect.class, - "[:force] (play|show|draw) [%number% [of]] %particles/game effects% (on|%directions%) %entities/locations% [(to %players%|in [a] (radius|range) of %number%)]", - "(play|show|draw) [%number% [of]] %particles/game effects/entity effects% (on|at) %entities%)"); + "[:force] (play|show|draw) %gameeffects% %directions% %locations%", + "[:force] (play|show|draw) %gameeffects% %directions% %locations% (for|to) %-players%", + "(play|show|draw) %gameeffects% %directions% %locations% (in|with) [a] [view] (radius|range) of %-number%)"); + // "(play|show|draw) %entityeffects% on %entities%)" + } + + private Expression toDraw; + private @Nullable Expression locations; + private @Nullable Expression toPlayers; + private @Nullable Expression radius; + private boolean force; + + // for entity effects + private @Nullable Expression entities; + + + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.force = parseResult.hasTag("force"); + this.toDraw = expressions[0]; + switch (matchedPattern) { + case 0 -> this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); + case 1 -> { + this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); + this.toPlayers = (Expression) expressions[3]; + } + case 2 -> { + this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); + this.radius = (Expression) expressions[3]; + } + case 3 -> this.entities = (Expression) expressions[1]; + } + return true; + } + + @Override + protected void execute(Event event) { + // entity effect + if (this.entities != null) { + Entity[] entities = this.entities.getArray(event); + EntityEffect[] effects = (EntityEffect[]) toDraw.getArray(event); + for (Entity entity : entities) { + for (EntityEffect effect : effects) { + entity.playEffect(effect); + } + } + return; + } + + // game effects / particles + assert this.locations != null; + Number radius = this.radius != null ? this.radius.getSingle(event) : null; + Location[] locations = this.locations.getArray(event); + Object[] toDraw = this.toDraw.getArray(event); + Player[] players = toPlayers != null ? toPlayers.getArray(event) : null; + + for (Object draw : toDraw) { + // Game effects + if (draw instanceof GameEffect gameEffect) { + // in radius + if (players == null) { + for (Location location : locations) + gameEffect.draw(location, radius); + // for players + } else { + for (Player player : players) { + for (Location location : locations) + gameEffect.drawForPlayer(location, player); + } + } + // Particles + } else if (draw instanceof ParticleEffect particleEffect) { + // to everyone + if (players == null) { + for (Location location : locations) + particleEffect.draw(location, force); + // for players + } else { + for (Player player : players) { + for (Location location : locations) + particleEffect.drawForPlayer(location, player, force); + } + } + } + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return ""; } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java index 1a81225b7f3..0c617886044 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java @@ -1,84 +1,131 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; -import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.util.Direction; import ch.njol.skript.util.Patterns; import ch.njol.util.Kleenean; -import ch.njol.util.VectorMath; +import org.bukkit.Axis; +import org.bukkit.Color; import org.bukkit.Effect; +import org.bukkit.Material; import org.bukkit.block.BlockFace; import org.bukkit.event.Event; -import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; +import java.util.ArrayList; +import java.util.List; + public class ExprGameEffect extends SimpleExpression { - private static final Patterns PATTERNS = new Patterns<>(new Object[][]{ - {"[record] song (of|using) %item%", Effect.RECORD_PLAY}, // shows the action bar too! - {"[dispenser] [black|1:white] smoke effect [(in|with|using) direction] %direction/vector%", Effect.SMOKE}, - {"[foot]step sound [effect] (on|of|using) %item/blockdata%", Effect.STEP_SOUND}, - {"[:instant] [splash] potion break effect (with|of|using) [colour] %color%", Effect.POTION_BREAK}, // paper changes this type - {"composter fill[ing] (succe[ss|ed]|1:fail[ure]) sound [effect]", Effect.COMPOSTER_FILL_ATTEMPT}, - {"villager plant grow[th] effect [(with|using) %number% particles]", Effect.VILLAGER_PLANT_GROW}, - {"[fake] bone meal effect [(with|using) %number% particles]", Effect.BONE_MEAL_USE}, - // post copper update (1.19?) - {"(electric|lightning[ rod]|copper) spark effect [(in|using) the (1:x|2:y|3:z) axis]", Effect.ELECTRIC_SPARK}, - // paper only - {"sculk (charge|spread) effect [(with|using) data %number%]", Effect.PARTICLES_SCULK_CHARGE}, // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 - {"[finish] brush[ing] effect (with|using) %item/blockdata%", Effect.PARTICLES_AND_SOUND_BRUSH_BLOCK_COMPLETE}, - // 1.20.3 - {"trial spawner detect[ing|s] [%number%] player[s] effect", Effect.TRIAL_SPAWNER_DETECT_PLAYER} - }); + private static final Patterns PATTERNS; + + private static final List REGISTERED_PATTERNS = new ArrayList<>(); + + private static void registerEffect(Effect effect, String pattern) { + registerEffect(effect, pattern, (event, expressions, parseResult) -> expressions[0].getSingle(event)); + } + + private static void registerEffect(Effect effect, String pattern, GetData getData) { + REGISTERED_PATTERNS.add(new EffectPattern(effect, pattern, getData)); + } static { - // TODO: register the rest via the type parser - // make sure to not parse the ones with data, so this class can handle it + registerEffect(Effect.RECORD_PLAY, "[record] song (of|using) %itemtype%", GetData::getMaterialData); + registerEffect(Effect.SMOKE, "[dispenser] black smoke effect [(in|with|using) direction] %direction%", + GetData::getBlockFaceData); + registerEffect(Effect.SHOOT_WHITE_SMOKE, "[dispenser] white smoke effect [(in|with|using) direction] %direction%", + GetData::getBlockFaceData); + registerEffect(Effect.STEP_SOUND, "[foot]step sound [effect] (on|of|using) %itemtype/blockdata%"); // handle version changes + registerEffect(Effect.POTION_BREAK, "[splash] potion break effect (with|of|using) [colour] %color%", + GetData::getColorData); // paper changes this type from potion data to color + registerEffect(Effect.INSTANT_POTION_BREAK, "instant [splash] potion break effect (with|of|using) [colour] %color%", + GetData::getColorData); + registerEffect(Effect.COMPOSTER_FILL_ATTEMPT, "[composter] fill[ing] (succe[ss|ed]|1:fail[ure]) sound [effect]", + (event, expressions, parseResult) -> parseResult.mark == 0); + + if (!Skript.isRunningMinecraft(1, 20, 5)) { + //noinspection removal + registerEffect(Effect.VILLAGER_PLANT_GROW, "villager plant grow[th] effect [(with|using) %-number% particles]", + GetData::defaultTo10Particles); + } + + registerEffect(Effect.BONE_MEAL_USE, "[fake] bone meal effect [(with|using) %-number% particles]", + GetData::defaultTo10Particles); + registerEffect(Effect.ELECTRIC_SPARK, "(electric|lightning[ rod]|copper) spark effect [(in|using|along) the (1:x|2:y|3:z) axis]", + (event, expressions, parseResult) -> (parseResult.mark == 0 ? null : Axis.values()[parseResult.mark - 1])); + + // All modern ones are Paper only + if (Skript.fieldExists(Effect.class, "PARTICLES_SCULK_CHARGE")) { + if (Skript.isRunningMinecraft(1, 20, 1)) { + registerEffect(Effect.PARTICLES_SCULK_CHARGE, "sculk (charge|spread) effect [(with|using) data %number%]"); // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 + registerEffect(Effect.PARTICLES_AND_SOUND_BRUSH_BLOCK_COMPLETE, "[finish] brush[ing] effect (with|using) %itemtype/blockdata%"); + } + if (Skript.isRunningMinecraft(1, 20, 4)) { + registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER, "trial spawner detect[ing|s] [%-number%] player[s] effect", + GetData::defaultTo1Player); + registerEffect(Effect.TRIAL_SPAWNER_SPAWN, "[:ominous] trial spawner spawn[ing] effect", + GetData::isOminous); + registerEffect(Effect.TRIAL_SPAWNER_SPAWN_MOB_AT, "[:ominous] trial spawner spawn[ing] mob effect with sound", + GetData::isOminous); + } + if (Skript.isRunningMinecraft(1, 20, 5)) { + registerEffect(Effect.BEE_GROWTH, "bee growth effect [(with|using) %-number% particles]", + GetData::defaultTo10Particles); + registerEffect(Effect.VAULT_ACTIVATE, "[:ominous] [trial] vault activate effect", + GetData::isOminous); + registerEffect(Effect.VAULT_DEACTIVATE, "[:ominous] [trial] vault deactivate effect", + GetData::isOminous); + registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS, "ominous trial spawner detect[ing|s] [%-number%] player[s] effect", + GetData::defaultTo1Player); + registerEffect(Effect.TRIAL_SPAWNER_BECOME_OMINOUS, "trial spawner become[ing] [:not] ominous effect", + (event, expressions, parseResult) -> !parseResult.hasTag("not")); + registerEffect(Effect.TRIAL_SPAWNER_SPAWN_ITEM, "[:ominous] trial spawner spawn[ing] item effect", + GetData::isOminous); + registerEffect(Effect.TURTLE_EGG_PLACEMENT, "place turtle egg effect [(with|using) %-number% particles]", + GetData::defaultTo10Particles); + registerEffect(Effect.SMASH_ATTACK, "[mace] smash attack effect [(with|using) %-number% particles]", + GetData::defaultTo10Particles); + } + } + + // create Patterns object + Object[][] patterns = new Object[REGISTERED_PATTERNS.size()][2]; + int i = 0; + for (EffectPattern effectPattern : REGISTERED_PATTERNS) { + patterns[i][0] = effectPattern.pattern; + patterns[i][1] = effectPattern; + i++; + } + PATTERNS = new Patterns<>(patterns); + Skript.registerExpression(ExprGameEffect.class, GameEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); } private GameEffect gameEffect; - private Expression data; - private int variant; + private GetData getData; + private Expression[] expressions; + private ParseResult parseResult; @Override - public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { - gameEffect = new GameEffect(PATTERNS.getInfo(matchedPattern)); - variant = parseResult.mark; - if (expressions.length > 0) - data = expressions[0]; + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + gameEffect = new GameEffect(PATTERNS.getInfo(matchedPattern).effect()); + getData = PATTERNS.getInfo(matchedPattern).getData(); + this.expressions = expressions; + this.parseResult = parseResult; return true; } @Override protected GameEffect @Nullable [] get(Event event) { - switch (gameEffect.getEffect()) { - case SMOKE: - return handleSmokeEffects(gameEffect, variant, data.getSingle(event)); - default: - return setData(gameEffect, data.getSingle(event)); - } - } - - private GameEffect @Nullable [] handleSmokeEffects(GameEffect gameEffect, int variant, Object data) { - BlockFace blockFace; - if (data instanceof Vector) { - Vector vector = ((Vector) data); - if (vector.isZero()) - return new GameEffect[0]; - blockFace = VectorMath.toNearestBlockFace(vector.normalize()); - } else if (data instanceof Direction) { - Vector vector = ((Direction) data).getDirection(); - // what about relative directions - } - if (variant == 1) - gameEffect = new GameEffect(Effect.SHOOT_WHITE_SMOKE); - return setData(gameEffect, data); + return setData(gameEffect, getData.getData(event, expressions, parseResult)); } private GameEffect @Nullable [] setData(GameEffect gameEffect, Object data){ @@ -92,16 +139,89 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is @Override public boolean isSingle() { - return false; + return true; } @Override public Class getReturnType() { - return null; + return GameEffect.class; } @Override public String toString(@Nullable Event event, boolean debug) { - return null; + // TODO: handle properly + return "game effect " + gameEffect.getEffect().name(); + } + + /** + * A helper class to store the effect and its pattern. + * + * @param effect The effect + * @param pattern The pattern + * @param getData The function to get the data from the event + */ + private record EffectPattern(Effect effect, String pattern, GetData getData) {} + + /** + * A functional interface to get the data from the event. + * + * @param The type of the data + */ + @FunctionalInterface + interface GetData { + /** + * Get the data from the event. + * + * @param event The event to evaluate with + * @param expressions Any expressions that are used in the pattern + * @param parseResult The parse result from parsing + * @return The data to use for the effect + */ + T getData(Event event, Expression[] expressions, ParseResult parseResult); + + // + // Helper functions for common data types + // + + private static @Nullable Material getMaterialData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof ItemType itemType)) + return null; + return itemType.getMaterial(); + } + + private static @Nullable BlockFace getBlockFaceData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Direction direction)) + return null; + return Direction.toNearestBlockFace(direction.getDirection()); + } + + private static @Nullable Color getColorData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof ch.njol.skript.util.Color color)) + return null; + return color.asBukkitColor(); + } + + private static boolean isOminous(Event event, Expression[] expressions, @NotNull ParseResult parseResult) { + return parseResult.hasTag("ominous"); + } + + private static int defaultTo10Particles(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Number number)) + return 10; + return number.intValue(); + } + + private static int defaultTo1Player(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Number number)) + return 1; + return number.intValue(); + } + } + } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java index 78d1d8e32b4..67c8780084a 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java @@ -1,9 +1,38 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser; import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.ParticleEffect; public class ExprParticleEffect extends SimpleExpression { + @Override + protected ParticleEffect @Nullable [] get(Event event) { + return new ParticleEffect[0]; + } + + @Override + public boolean isSingle() { + return false; + } + + @Override + public Class getReturnType() { + return null; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return ""; + } + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + return false; + } // TODO: // Syntax: // count + (name + "particle" + data) + offset + extra diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 97ce21589f4..2f993e704b2 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2412,71 +2412,69 @@ transform reasons: infection: infection, villager infection # -- Game Effects -- -game effects: - click1: - name: dispenser click sound - pattern: - enums: click1 - click2: menu click sound, gui click sound - bow_fire: fire bow sound, shoot bow sound - extinguish: extinguish fire sound, extinguish sound - ghast_shriek: ghast shriek sound - ghast_shoot: ghast shoot sound - blaze_shoot: blaze shoot sound - zombie_chew_wooden_door: zombie chewing on wooden door sound, zombie attacking wooden door sound - zombie_chew_iron_door: zombie chewing on iron door sound, zombie attacking iron door sound - zombie_destroy_door: zombie destroy door sound, zombie destroying door sound - ender_signal: ender eye break, eye of ender break, ender eye breaking, eye of ender breaking - mobspawner_flames: mob spawner flames, spawner flames - brewing_stand_brew: brewing sound, brewing stand sound - chorus_flower_grow: chorus flower growing sound, chorus plant growing sound - chorus_flower_death: chorus flower death sound - portal_travel: nether portal travel sound - endereye_launch: ender eye launch sound, eye of ender launch sound - firework_shoot: dispenser shoot sound - dragon_breath: dragon breath, dragon's breath - anvil_break: break anvil sound - anvil_use: use anvil sound - anvil_land: anvil land sound - enderdragon_shoot: ender dragon shoot sound, dragon shoot sound - enderdragon_growl: ender dragon growl sound, dragon growl sound - wither_break_block: wither breaking block sound - wither_shoot: wither shoot sound - zombie_infect: zombie infection sound - zombie_converted_villager: zombie converting villager sound - zombie_converted_to_drowned: zombie conversion to drowned sound - zombie_converts_to_drowned: zombie conversion to drowned sound - husk_converted_to_zombie: husk conversion to zombie sound - husk_converts_to_zombie: husk conversion to zombie sound - skeleton_converted_to_stray: skeleton conversion to stray sound - bat_takeoff: bat take off sound, bat take flight sound - end_gateway_spawn: end gateway spawn, new end gateway - phantom_bite: phantom bite sound - phantom_bites: phantom bites sound - grindstone_use: use grindstone sound - grindstone_used: used grindstone sound - book_page_turn: book page turn sound, turn page sound - book_page_turned: book page turned sound, turned page sound - smithing_table_use: use smithing table sound - pointed_dripstone_drip_lava_into_cauldron: lava dripping into cauldron sound - pointed_dripstone_drip_water_into_cauldron: water dripping into cauldron sound - dripping_dripstone: water dripping sound, lava dripping sound - lava_interact: lava interaction - lava_converts_block: lava converts block - redstone_torch_burnout: redstone touch burnout - redstone_torch_burns_out: redstone touch burns out - end_portal_frame_fill: fill end portal frame - ender_eye_placed: ender eye placed - ender_dragon_destroy_block: ender dragon breaking block, dragon breaking block - ender_dragon_destroys_block: ender dragon destroys block, dragon destroys block - sponge_dry: sponge dry, sponge dry out - copper_wax_on: apply wax, wax on - copper_wax_off: remove wax, wax off - oxidised_copper_scrape: scrape oxidised copper, scrape copper - wither_spawned: spawn wither sound - ender_dragon_death: ender dragon death sound, dragon death sound - end_portal_created_in_overworld: complete end portal sound, create end portal sound - composter_composts: composter compost +# all patterns should include the word 'effect' +game effect: + click1: dispenser click sound effect + click2: menu click sound effect, gui click sound effect + bow_fire: fire bow sound effect, shoot bow sound effect + extinguish: extinguish fire sound effect, extinguish sound effect + ghast_shriek: ghast shriek sound effect + ghast_shoot: ghast shoot sound effect + blaze_shoot: blaze shoot sound effect + zombie_chew_wooden_door: zombie chewing on wooden door sound effect, zombie attacking wooden door sound effect + zombie_chew_iron_door: zombie chewing on iron door sound effect, zombie attacking iron door sound effect + zombie_destroy_door: zombie destroy door sound effect, zombie destroying door sound effect + ender_signal: ender eye break effect, eye of ender break effect + mobspawner_flames: mob spawner flames effect, spawner flames effect + brewing_stand_brew: brewing sound effect + chorus_flower_grow: chorus flower growing sound effect + chorus_flower_death: chorus flower death sound effect + portal_travel: nether portal travel sound effect + endereye_launch: ender eye launch sound effect, eye of ender launch sound effect + firework_shoot: dispenser shoot sound effect + dragon_breath: dragon breath effect, dragon's breath effect + anvil_break: break anvil sound effect + anvil_use: use anvil sound effect + anvil_land: anvil land sound effect + enderdragon_shoot: ender dragon shoot sound effect, dragon shoot sound effect + enderdragon_growl: ender dragon growl sound effect, dragon growl sound effect + wither_break_block: wither breaking block sound effect + wither_shoot: wither shoot sound effect + zombie_infect: zombie infection sound effect + zombie_converted_villager: zombie converting villager sound effect + zombie_converted_to_drowned: zombie converting to drowned sound effect + zombie_converts_to_drowned: zombie converting to drowned sound effect + husk_converted_to_zombie: husk converting to zombie sound effect + husk_converts_to_zombie: husk converting to zombie sound effect + skeleton_converted_to_stray: skeleton converting to stray sound effect + bat_takeoff: bat take off sound effect + end_gateway_spawn: end gateway spawn effect + phantom_bite: phantom bite sound effect + phantom_bites: phantom bites sound effect + grindstone_use: use grindstone sound effect + grindstone_used: used grindstone sound effect + book_page_turn: turn page sound effect + book_page_turned: turned page sound effect + smithing_table_use: use smithing table sound effect + pointed_dripstone_drip_lava_into_cauldron: lava dripping into cauldron sound effect + pointed_dripstone_drip_water_into_cauldron: water dripping into cauldron sound effect + dripping_dripstone: water dripping sound effect, lava dripping sound effect + lava_interact: lava interaction effect + lava_converts_block: lava converts block effect + redstone_torch_burnout: redstone torch burnout effect + redstone_torch_burns_out: redstone torch burns out effect + end_portal_frame_fill: fill end portal frame effect + ender_eye_placed: ender eye placed effect, eye of ender placed effect + ender_dragon_destroy_block: ender dragon breaking block effect, dragon breaking block effect + ender_dragon_destroys_block: ender dragon destroys block effect, dragon destroys block effect + sponge_dry: sponge dry out effect + copper_wax_on: apply wax effect + copper_wax_off: remove wax effect + oxidised_copper_scrape: scrape oxidised copper effect + wither_spawned: spawn wither sound effect + ender_dragon_death: ender dragon death sound effect, dragon death sound effect + end_portal_created_in_overworld: complete end portal sound effect, create end portal sound effect + composter_composts: composter compost effect # these effect are deprecated in 1.19, they do nothing. # their respective Sounds should be used instead. From f20f50408bb17bf040c82f1bc8f712b659dc61ca Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 20 Apr 2025 20:25:25 -0400 Subject: [PATCH 05/16] Finish game effects --- .../skript/bukkit/particles/GameEffect.java | 10 +++- src/main/resources/lang/default.lang | 60 +++++++++++++++---- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index 6bfbf0ce789..7aec4912ac2 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -1,5 +1,7 @@ package org.skriptlang.skript.bukkit.particles; +import ch.njol.skript.Skript; +import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.EnumUtils; import org.bukkit.Effect; import org.bukkit.Location; @@ -16,8 +18,7 @@ */ public class GameEffect { - // TODO: fix missing key error for effects that require data - public static final EnumUtils ENUM_UTILS = new EnumUtils<>(Effect.class, "game effect"); + public static final EnumUtils ENUM_UTILS = new EnumUtils<>(Effect.class, "game effect"); // exclude effects that require data /** * The {@link Effect} that this object represents @@ -36,7 +37,10 @@ public GameEffect(Effect effect) { public static GameEffect parse(String input) { Effect effect = ENUM_UTILS.parse(input.toLowerCase(Locale.ENGLISH)); - if (effect == null || effect.getData() != null) { + if (effect == null) + return null; + if (effect.getData() != null) { + Skript.error("The effect " + Classes.toString(effect) + " requires data and cannot be parsed directly. Use the Game Effect expression instead."); return null; } return new GameEffect(effect); diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 2f993e704b2..1650e1f4547 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2476,18 +2476,56 @@ game effect: end_portal_created_in_overworld: complete end portal sound effect, create end portal sound effect composter_composts: composter compost effect - # these effect are deprecated in 1.19, they do nothing. + # these effects are deprecated in 1.19, they do nothing. # their respective Sounds should be used instead. - door_toggle: toggle door sound - iron_door_toggle: toggle iron door sound - trapdoor_toggle: toggle trapdoor sound - iron_trapdoor_toggle: toggle iron trapdoor sound - fence_gate_toggle: toggle fence gate sound - door_close: close door sound - iron_door_close: close iron door sound - trapdoor_close: close trapdoor sound - iron_trapdoor_close: close iron trapdoor sound - fence_gate_close: close fence gate sound + door_toggle: toggle door sound effect + iron_door_toggle: toggle iron door sound effect + trapdoor_toggle: toggle trapdoor sound effect + iron_trapdoor_toggle: toggle iron trapdoor sound effect + fence_gate_toggle: toggle fence gate sound effect + door_close: close door sound effect + iron_door_close: close iron door sound effect + trapdoor_close: close trapdoor sound effect + iron_trapdoor_close: close iron trapdoor sound effect + fence_gate_close: close fence gate sound effect + + # to categorize + pointed_dripstone_land: pointed dripstone land effect + sound_stop_jukebox_song: stop jukebox song sound effect + crafter_craft: crafter craft sound effect + crafter_fail: crafter fail sound effect + particles_egg_crack: egg crack effect + gust_dust: gust dust effect + trial_spawner_eject_item: trial spawner eject item effect + vault_eject_item: vault eject item effect + spawn_cobweb: spawn cobweb effect + sound_with_charge_shot: charge shot sound effect + + # these effects require data and can't be parsed directly, but exist here in case skript needs a proper name. + record_play: play record effect + smoke: dispenser black smoke effect + step_sound: footstep sound effect + potion_break: splash potion break effect + instant_potion_break: instant splash potion break effect + villager_plant_grow: villager grow plant effect + composter_fill_attempt: composting fill effect + bone_meal_use: bone meal use effect + electric_spark: electric spark effect + shoot_white_smoke: dispenser white smoke effect + bee_growth: bee grow plant effect + turtle_egg_placement: place turtle egg effect + smash_attack: mace smash attack effect + particles_sculk_charge: sculk charge effect + particles_sculk_shriek: sculk shriek effect + particles_and_sound_brush_block_complete: brush block effect + trial_spawner_spawn: trial spawner spawn effect + trial_spawner_spawn_mob_at: trial spawner spawn mob effect + trial_spawner_detect_player: trial spawner detect player effect + vault_activate: vault activate effect + vault_deactivate: vault deactivate effect + trial_spawner_detect_player_ominous: ominous trial spawner detect player effect + trial_spawner_become_ominous: trial spawner become ominous effect + trial_spawner_spawn_item: trial spawner spawn item effect # -- Teleport Flags -- teleport flags: From 6cbe2916aa7c7f5336141c7795741a08dd0ef4f0 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 20 Apr 2025 20:26:35 -0400 Subject: [PATCH 06/16] forgot types.gameeffect --- src/main/resources/lang/default.lang | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 1650e1f4547..6183361126d 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2475,6 +2475,16 @@ game effect: ender_dragon_death: ender dragon death sound effect, dragon death sound effect end_portal_created_in_overworld: complete end portal sound effect, create end portal sound effect composter_composts: composter compost effect + pointed_dripstone_land: pointed dripstone land effect + sound_stop_jukebox_song: stop jukebox song sound effect + crafter_craft: crafter craft sound effect + crafter_fail: crafter fail sound effect + particles_egg_crack: egg crack effect + gust_dust: gust dust effect + trial_spawner_eject_item: trial spawner eject item effect + vault_eject_item: vault eject item effect + spawn_cobweb: spawn cobweb effect + sound_with_charge_shot: charge shot sound effect # these effects are deprecated in 1.19, they do nothing. # their respective Sounds should be used instead. @@ -2489,18 +2499,6 @@ game effect: iron_trapdoor_close: close iron trapdoor sound effect fence_gate_close: close fence gate sound effect - # to categorize - pointed_dripstone_land: pointed dripstone land effect - sound_stop_jukebox_song: stop jukebox song sound effect - crafter_craft: crafter craft sound effect - crafter_fail: crafter fail sound effect - particles_egg_crack: egg crack effect - gust_dust: gust dust effect - trial_spawner_eject_item: trial spawner eject item effect - vault_eject_item: vault eject item effect - spawn_cobweb: spawn cobweb effect - sound_with_charge_shot: charge shot sound effect - # these effects require data and can't be parsed directly, but exist here in case skript needs a proper name. record_play: play record effect smoke: dispenser black smoke effect @@ -2805,6 +2803,7 @@ types: vehicle: vehicle¦s @a fishingstate: fishing state¦s @a equipmentslot: equipment slot¦s @an + gameeffect: game effect¦s @a # Skript From 104992397082116ac330915f727ad64d30868dc7 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 20 Apr 2025 21:03:01 -0400 Subject: [PATCH 07/16] Initial EntityEffect Efforts now i have to go through every one, check what it really does, and update its name accordingly fun --- .../skript/bukkit/particles/GameEffect.java | 2 + .../bukkit/particles/ParticleModule.java | 20 ++++- .../elements/effects/EffPlayEffect.java | 30 +++++-- src/main/resources/lang/default.lang | 81 ++++++++++++++++++- 4 files changed, 122 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index 7aec4912ac2..c5d5843b5d7 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -69,6 +69,8 @@ public boolean setData(Object data) { * @param radius the radius to play the effect in, or null to use the default radius */ public void draw(@NotNull Location location, @Nullable Number radius) { + if (effect.getData() != null && data == null) + return; World world = location.getWorld(); if (world == null) return; diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index 54af1d93227..6331f17d4c2 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -2,6 +2,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.classes.ClassInfo; +import ch.njol.skript.classes.EnumClassInfo; import ch.njol.skript.classes.Parser; import ch.njol.skript.classes.Serializer; import ch.njol.skript.expressions.base.EventValueExpression; @@ -9,10 +10,11 @@ import ch.njol.skript.registrations.Classes; import ch.njol.yggdrasil.Fields; import org.bukkit.Effect; +import org.bukkit.EntityEffect; import java.io.IOException; -import java.io.NotSerializableException; import java.io.StreamCorruptedException; +import java.util.Arrays; public class ParticleModule { @@ -23,6 +25,12 @@ public static void load () throws IOException { .description("Various game effects that can be played for players, like record disc songs, splash potions breaking, or fake bone meal effects.") .name("Game Effect") .usage(GameEffect.getAllNamesWithoutData()) + .supplier(() -> { + Effect[] effects = Effect.values(); + return Arrays.stream(effects).map(GameEffect::new) + .filter(effect -> effect.getData() == null) + .iterator(); + }) .serializer(new Serializer<>() { @Override public Fields serialize(GameEffect effect) { @@ -33,12 +41,12 @@ public Fields serialize(GameEffect effect) { } @Override - public void deserialize(GameEffect effect, Fields fields) throws StreamCorruptedException, NotSerializableException { + public void deserialize(GameEffect effect, Fields fields) { assert false; } @Override - protected GameEffect deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + protected GameEffect deserialize(Fields fields) throws StreamCorruptedException { String name = fields.getAndRemovePrimitive("name", String.class); GameEffect effect; try { @@ -78,6 +86,12 @@ public String toVariableNameString(GameEffect o) { } })); + Classes.registerClass(new EnumClassInfo<>(EntityEffect.class, "entityeffect", "entity effect") + .user("entity ?effects?") + .name("Entity Effect") + .description("Various entity effects that can be played for entities, like wolf howling, or villager happy.") + .since("INSERT VERSION")); + Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index 74810b0e3b3..4e9987b1d79 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -1,9 +1,11 @@ package org.skriptlang.skript.bukkit.particles.elements.effects; import ch.njol.skript.Skript; +import ch.njol.skript.config.Node; import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Direction; import ch.njol.util.Kleenean; import org.bukkit.EntityEffect; @@ -14,15 +16,16 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; import org.skriptlang.skript.bukkit.particles.ParticleEffect; +import org.skriptlang.skript.log.runtime.SyntaxRuntimeErrorProducer; // TODO: better terminology than "effects", as it's getting confusing. -public class EffPlayEffect extends Effect { +public class EffPlayEffect extends Effect implements SyntaxRuntimeErrorProducer { static { Skript.registerEffect(EffPlayEffect.class, "[:force] (play|show|draw) %gameeffects% %directions% %locations%", "[:force] (play|show|draw) %gameeffects% %directions% %locations% (for|to) %-players%", - "(play|show|draw) %gameeffects% %directions% %locations% (in|with) [a] [view] (radius|range) of %-number%)"); - // "(play|show|draw) %entityeffects% on %entities%)" + "(play|show|draw) %gameeffects% %directions% %locations% (in|with) [a] [view] (radius|range) of %-number%)", + "(play|show|draw) %entityeffects% on %entities%"); } private Expression toDraw; @@ -34,7 +37,7 @@ public class EffPlayEffect extends Effect { // for entity effects private @Nullable Expression entities; - + private Node node; @Override @SuppressWarnings("unchecked") @@ -53,6 +56,7 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is } case 3 -> this.entities = (Expression) expressions[1]; } + this.node = getParser().getNode(); return true; } @@ -62,9 +66,16 @@ protected void execute(Event event) { if (this.entities != null) { Entity[] entities = this.entities.getArray(event); EntityEffect[] effects = (EntityEffect[]) toDraw.getArray(event); - for (Entity entity : entities) { - for (EntityEffect effect : effects) { - entity.playEffect(effect); + for (EntityEffect effect : effects) { + boolean played = false; + for (Entity entity : entities) { + if (effect.isApplicableTo(entity)) { + entity.playEffect(effect); + played = true; + } + } + if (entities.length > 0 && !played) { + warning("Effect " + Classes.toString(effect) + " is not applicable to any of the given entities: (" + Classes.toString(entities, this.entities.getAnd()) + ")"); } } return; @@ -112,4 +123,9 @@ protected void execute(Event event) { public String toString(@Nullable Event event, boolean debug) { return ""; } + + @Override + public Node getNode() { + return node; + } } diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 6183361126d..1eb1e160d58 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2525,6 +2525,85 @@ game effect: trial_spawner_become_ominous: trial spawner become ominous effect trial_spawner_spawn_item: trial spawner spawn item effect +# -- Entity Effects -- +# all patterns should include the word 'effect' or 'animation' +entity effect: + arrow_particles: tipped arrow particles effect + rabbit_jump: rabbit jump effect + reset_spawner_minecart_delay: reset spawner minecart delay effect + hurt: hurt animation + death: death animation + egg_break: egg break animation + snowball_break: snowball break animation + projectile_crack: projectile crack animation + entity_death: entity death animation + fang_attack: fang attack animation + hoglin_attack: hoglin attack animation + iron_golen_attack: iron golem attack animation + ravager_attack: ravager attack animation + warden_attack: warden attack animation + zoglin_attack: zoglin attack animation + entity_attack: entity attack animation + wolf_smoke: wolf smoke effect + wolf_hearts: wolf hearts effect + taming_failed: taming failed effect + taming_succeeded: taming succeeded effect + wolf_shake: wolf shake animation + sheep_eat: sheep eat animation + sheep_eat_grass: sheep eat grass animation + tnt_minecart_ignite: ignite tnt minecart animation + iron_golem_rose: iron golem rose animation + villager_heart: villager heart effect + villager_angry: villager angry effect + villager_happy: villager happy effect + witch_magic: witch magic effect + zombie_transform: zombie transform animation + firework_explode: firework explode animation + love_hearts: love hearts effect + squid_rotate: squid rotate animation + entity_poof: entity poof effect + guardian_target: guardian target effect + shield_block: shield block animation + shield_break: shield break animation + armor_stand_hit: armor stand hit animation + thorns_hurt: thorns hurt animation + iron_golem_sheath: iron golem sheath animation + totem_resurrect: totem resurrect animation + protected_from_death: protected from death animation + hurt_drown: drown damage effect + hurt_explosion: explosion damage effect + dolphin_fed: fed dolphin animation + ravager_stunned: stunned ravager animation + cat_tame_fail: cat tame fail animation + cat_tame_success: cat tame success animation + trusting_failed: trusting failed + trusting_succeeded: trusting succeeded + villager_splash: villager splash effect + player_bad_omen_raid: player bad omen raid effect + hurt_berry_bush: hurt berry bush effect + fox_chew: fox chew animation + teleport_ender: ender teleport animation + break_equipment_main_hand: break equipment main hand effect + break_equipment_off_hand: break equipment off hand effect + break_equipment_helmet: break equipment helmet effect + break_equipment_chestplate: break equipment chestplate effect + break_equipment_leggings: break equipment leggings effect + break_equipment_boots: break equipment boots effect + honey_block_slide_particles: honey block slide particles + honey_block_fall_particles: honey block fall particles + swap_hand_items: swap hand items effect + wolf_shake_stop: wolf shake stop animation + goat_lower_head: goat lower head animation + goat_raise_head: goat raise head animation + spawn_death_smoke: spawn death smoke effect + warden_tendril_shake: warden tendril shake animation + warden_sonic_attack: warden sonic attack animation + sniffer_dig: sniffer dig animation + armadillo_peek: armadillo peek animation + body_break: break body animation + break_equipment_body: break equipment body effect + shake: shake animation + # -- Teleport Flags -- teleport flags: retain_open_inventory: opened inventory, open inventory, inventory @@ -2804,7 +2883,7 @@ types: fishingstate: fishing state¦s @a equipmentslot: equipment slot¦s @an gameeffect: game effect¦s @a - + entityeffect: entity effect¦s @a # Skript weathertype: weather type¦s @a From 74834455d83870b9c96c23c3993e3f358a66f80f Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Mon, 9 Jun 2025 00:23:53 -0700 Subject: [PATCH 08/16] verify + name entityeffects --- .../njol/skript/entity/SimpleEntityData.java | 1 + .../elements/effects/EffPlayEffect.java | 42 +++++-- src/main/resources/lang/default.lang | 114 ++++++++++-------- 3 files changed, 95 insertions(+), 62 deletions(-) diff --git a/src/main/java/ch/njol/skript/entity/SimpleEntityData.java b/src/main/java/ch/njol/skript/entity/SimpleEntityData.java index cb9fe2348e8..6bd92f03f46 100644 --- a/src/main/java/ch/njol/skript/entity/SimpleEntityData.java +++ b/src/main/java/ch/njol/skript/entity/SimpleEntityData.java @@ -235,6 +235,7 @@ private static void addSuperEntity(String codeName, Class enti addSuperEntity("mob", Mob.class); addSuperEntity("creature", Creature.class); addSuperEntity("animal", Animals.class); + addSuperEntity("tameable", Tameable.class); addSuperEntity("fish", Fish.class); addSuperEntity("golem", Golem.class); addSuperEntity("projectile", Projectile.class); diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index 4e9987b1d79..c89ef81956b 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -2,6 +2,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.config.Node; +import ch.njol.skript.entity.EntityData; import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -13,6 +14,7 @@ import org.bukkit.entity.Entity; import org.bukkit.entity.Player; import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; import org.skriptlang.skript.bukkit.particles.ParticleEffect; @@ -66,18 +68,7 @@ protected void execute(Event event) { if (this.entities != null) { Entity[] entities = this.entities.getArray(event); EntityEffect[] effects = (EntityEffect[]) toDraw.getArray(event); - for (EntityEffect effect : effects) { - boolean played = false; - for (Entity entity : entities) { - if (effect.isApplicableTo(entity)) { - entity.playEffect(effect); - played = true; - } - } - if (entities.length > 0 && !played) { - warning("Effect " + Classes.toString(effect) + " is not applicable to any of the given entities: (" + Classes.toString(entities, this.entities.getAnd()) + ")"); - } - } + drawEntityEffects(effects, entities); return; } @@ -119,6 +110,33 @@ protected void execute(Event event) { } } + /** + * Helper method to draw entity effects on entities. Provides a runtime warning if no provided entities are applicable + * @param effects the effects to draw + * @param entities the entities to draw the effects on + */ + private void drawEntityEffects(EntityEffect @NotNull [] effects, Entity @NotNull [] entities) { + for (EntityEffect effect : effects) { + boolean played = false; + for (Entity entity : entities) { + if (effect.isApplicableTo(entity)) { + entity.playEffect(effect); + played = true; + } + } + if (entities.length > 0 && !played) { + // todo: cache? + String[] applicableClasses = effect.getApplicableClasses().stream() + .map(EntityData::toString) + .distinct().toArray(String[]::new); + assert this.entities != null; + warning("The '" + Classes.toString(effect) + "' is not applicable to any of the given entities " + + "(" + Classes.toString(entities, this.entities.getAnd()) + "), " + + "only to " + Classes.toString(applicableClasses, false) + "."); + } + } + } + @Override public String toString(@Nullable Event event, boolean debug) { return ""; diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 1eb1e160d58..3c6355dd47c 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2527,82 +2527,96 @@ game effect: # -- Entity Effects -- # all patterns should include the word 'effect' or 'animation' +# 'effect' should be used for particles, 'sound effect' for sounds, 'animation' for model animations entity effect: arrow_particles: tipped arrow particles effect - rabbit_jump: rabbit jump effect + rabbit_jump: rabbit jump animation reset_spawner_minecart_delay: reset spawner minecart delay effect hurt: hurt animation + + # death was superseded by entity_death 1.12.2 + entity_death: death animation death: death animation - egg_break: egg break animation - snowball_break: snowball break animation - projectile_crack: projectile crack animation - entity_death: entity death animation - fang_attack: fang attack animation + + egg_break: egg break effect + snowball_break: snowball break effect + projectile_crack: projectile crack effect + fang_attack: evoker fang attack animation hoglin_attack: hoglin attack animation iron_golen_attack: iron golem attack animation ravager_attack: ravager attack animation warden_attack: warden attack animation zoglin_attack: zoglin attack animation entity_attack: entity attack animation - wolf_smoke: wolf smoke effect - wolf_hearts: wolf hearts effect + + # wolf effects superseded by taming effects 1.21 + wolf_smoke: taming failed effect + wolf_hearts: taming succeeded effect taming_failed: taming failed effect taming_succeeded: taming succeeded effect + + trusting_failed: ocelot distrust effect + trusting_succeeded: ocelot trust effect wolf_shake: wolf shake animation - sheep_eat: sheep eat animation - sheep_eat_grass: sheep eat grass animation + wolf_shake_stop: stop wolf shake animation tnt_minecart_ignite: ignite tnt minecart animation iron_golem_rose: iron golem rose animation + iron_golem_sheath: iron golem sheath animation villager_heart: villager heart effect villager_angry: villager angry effect villager_happy: villager happy effect witch_magic: witch magic effect - zombie_transform: zombie transform animation + zombie_transform: zombie transform sound effect firework_explode: firework explode animation love_hearts: love hearts effect - squid_rotate: squid rotate animation + squid_rotate: squid rotation reset entity_poof: entity poof effect - guardian_target: guardian target effect - shield_block: shield block animation - shield_break: shield break animation - armor_stand_hit: armor stand hit animation - thorns_hurt: thorns hurt animation - iron_golem_sheath: iron golem sheath animation - totem_resurrect: totem resurrect animation - protected_from_death: protected from death animation - hurt_drown: drown damage effect - hurt_explosion: explosion damage effect - dolphin_fed: fed dolphin animation - ravager_stunned: stunned ravager animation - cat_tame_fail: cat tame fail animation - cat_tame_success: cat tame success animation - trusting_failed: trusting failed - trusting_succeeded: trusting succeeded - villager_splash: villager splash effect - player_bad_omen_raid: player bad omen raid effect - hurt_berry_bush: hurt berry bush effect - fox_chew: fox chew animation - teleport_ender: ender teleport animation - break_equipment_main_hand: break equipment main hand effect - break_equipment_off_hand: break equipment off hand effect - break_equipment_helmet: break equipment helmet effect - break_equipment_chestplate: break equipment chestplate effect - break_equipment_leggings: break equipment leggings effect - break_equipment_boots: break equipment boots effect - honey_block_slide_particles: honey block slide particles - honey_block_fall_particles: honey block fall particles - swap_hand_items: swap hand items effect - wolf_shake_stop: wolf shake stop animation - goat_lower_head: goat lower head animation - goat_raise_head: goat raise head animation - spawn_death_smoke: spawn death smoke effect + shield_block: shield block sound effect + shield_break: shield break sound effect + armor_stand_hit: armor stand hit effect + dolphin_fed: fed dolphin effect + ravager_stunned: stunned ravager effect + villager_splash: villager sweat effect + player_bad_omen_raid: player bad omen raid effect # pre 1.20.5 + fox_chew: fox chew effect + teleport_ender: ender teleport effect + break_equipment_main_hand: break main hand effect + break_equipment_off_hand: break off hand effect + break_equipment_helmet: break helmet effect + break_equipment_chestplate: break chestplate effect + break_equipment_leggings: break leggings effect + break_equipment_boots: break boots effect + body_break: break body armor effect, break body armour effect + honey_block_slide_particles: honey block slide effect + honey_block_fall_particles: honey block fall effect + swap_hand_items: swap hands effect # this actually makes players swap hands + goat_lower_head: goat lowering head animation + goat_raise_head: goat raising head animation + spawn_death_smoke: spawn in smoke effect warden_tendril_shake: warden tendril shake animation warden_sonic_attack: warden sonic attack animation - sniffer_dig: sniffer dig animation + sniffer_dig: sniffer dig sound effect # only if the sniffer is already in search/dig state armadillo_peek: armadillo peek animation - body_break: break body animation - break_equipment_body: break equipment body effect - shake: shake animation + shake: creaking shake animation + + # sheep eat superseded by sheep eat grass 1.12.2 + sheep_eat: sheep eating grass animation + sheep_eat_grass: sheep eating grass animation + + # identical + totem_resurrect: protected from death effect + protected_from_death: protected from death effect + + # non-functional + guardian_target: guardian target sound effect + thorns_hurt: thorns hurt effect + hurt_drown: drown damage effect + hurt_explosion: explosion damage effect + hurt_berry_bush: hurt berry bush effect + cat_tame_fail: cat tame fail effect + cat_tame_success: cat tame success effect + + # -- Teleport Flags -- teleport flags: From 293acfd032655c1a700876126f2003a987280ffd Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:37:39 -0800 Subject: [PATCH 09/16] implement particle effects first pass --- .../ch/njol/skript/classes/EnumParser.java | 2 +- .../skript/classes/data/SkriptClasses.java | 58 ++-- .../njol/skript/effects/EffVisualEffect.java | 6 +- .../bukkit/particles/ParticleEffect.java | 135 +++++++- .../bukkit/particles/ParticleModule.java | 110 +++++++ .../elements/effects/EffPlayEffect.java | 4 +- .../expressions/ExprParticleWithData.java | 293 ++++++++++++++++++ src/main/resources/lang/default.lang | 130 +++++++- 8 files changed, 695 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java diff --git a/src/main/java/ch/njol/skript/classes/EnumParser.java b/src/main/java/ch/njol/skript/classes/EnumParser.java index 69d503a130f..e286afe1cc0 100644 --- a/src/main/java/ch/njol/skript/classes/EnumParser.java +++ b/src/main/java/ch/njol/skript/classes/EnumParser.java @@ -20,7 +20,7 @@ public class EnumParser> extends PatternedParser implements private final Class enumClass; private final String languageNode; private String[] names; - private final Map parseMap = new HashMap<>(); + protected final Map parseMap = new HashMap<>(); private String[] patterns; /** diff --git a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java index 87df08749ad..578d0a374a4 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -18,8 +18,6 @@ import ch.njol.skript.localization.RegexMessage; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.*; -import ch.njol.skript.util.visual.VisualEffect; -import ch.njol.skript.util.visual.VisualEffects; import ch.njol.yggdrasil.Fields; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -475,34 +473,34 @@ public String toVariableNameString(final Experience xp) { }) .serializer(new YggdrasilSerializer<>())); - Classes.registerClass(new ClassInfo<>(VisualEffect.class, "visualeffect") - .name("Visual Effect") - .description("A visible effect, e.g. particles.") - .examples("show wolf hearts on the clicked wolf", - "play mob spawner flames at the targeted block to the player") - .usage(VisualEffects.getAllNames()) - .since("2.1") - .user("(visual|particle) effects?") - .after("itemtype") - .parser(new Parser() { - @Override - @Nullable - public VisualEffect parse(String s, ParseContext context) { - return VisualEffects.parse(s); - } - - @Override - public String toString(VisualEffect e, int flags) { - return e.toString(flags); - } - - @Override - public String toVariableNameString(VisualEffect e) { - return e.toString(); - } - - }) - .serializer(new YggdrasilSerializer<>())); +// Classes.registerClass(new ClassInfo<>(VisualEffect.class, "visualeffect") +// .name("Visual Effect") +// .description("A visible effect, e.g. particles.") +// .examples("show wolf hearts on the clicked wolf", +// "play mob spawner flames at the targeted block to the player") +// .usage(VisualEffects.getAllNames()) +// .since("2.1") +// .user("(visual|particle) effects?") +// .after("itemtype") +// .parser(new Parser() { +// @Override +// @Nullable +// public VisualEffect parse(String s, ParseContext context) { +// return VisualEffects.parse(s); +// } +// +// @Override +// public String toString(VisualEffect e, int flags) { +// return e.toString(flags); +// } +// +// @Override +// public String toVariableNameString(VisualEffect e) { +// return e.toString(); +// } +// +// }) +// .serializer(new YggdrasilSerializer<>())); Classes.registerClass(new ClassInfo<>(GameruleValue.class, "gamerulevalue") .user("gamerule values?") diff --git a/src/main/java/ch/njol/skript/effects/EffVisualEffect.java b/src/main/java/ch/njol/skript/effects/EffVisualEffect.java index bd6acbfdef7..f62b0fb3365 100644 --- a/src/main/java/ch/njol/skript/effects/EffVisualEffect.java +++ b/src/main/java/ch/njol/skript/effects/EffVisualEffect.java @@ -28,9 +28,9 @@ public class EffVisualEffect extends Effect { static { - Skript.registerEffect(EffVisualEffect.class, - "(play|show) %visualeffects% (on|%directions%) %entities/locations% [(to %-players%|in (radius|range) of %-number%)]", - "(play|show) %number% %visualeffects% (on|%directions%) %entities/locations% [(to %-players%|in (radius|range) of %-number%)]"); +// Skript.registerEffect(EffVisualEffect.class, +// "(play|show) %visualeffects% (on|%directions%) %entities/locations% [(to %-players%|in (radius|range) of %-number%)]", +// "(play|show) %number% %visualeffects% (on|%directions%) %entities/locations% [(to %-players%|in (radius|range) of %-number%)]"); } @SuppressWarnings("NotNullFieldNotInitialized") diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java index a92dfbd96df..62e704944d7 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java @@ -1,17 +1,49 @@ package org.skriptlang.skript.bukkit.particles; +import ch.njol.skript.Skript; +import ch.njol.skript.classes.EnumParser; +import ch.njol.skript.lang.Debuggable; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.StringMode; import org.bukkit.Location; import org.bukkit.Particle; import org.bukkit.World; import org.bukkit.entity.Player; +import org.bukkit.event.Event; import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Locale; +import java.util.Map; + /** * A class to hold particle metadata prior to spawning */ -public class ParticleEffect { +public class ParticleEffect implements Debuggable { + + private static final ParticleParser ENUM_PARSER = new ParticleParser(); + + public static ParticleEffect parse(String input, ParseContext context) { + Particle particle = ENUM_PARSER.parse(input.toLowerCase(Locale.ENGLISH), context); + if (particle == null) + return null; + if (particle.getDataType() != Void.class) { + Skript.error("The " + Classes.toString(particle) + " requires data and cannot be parsed directly. Use the Particle With Data expression instead."); + return null; + } + return new ParticleEffect(particle); + } + + public static String toString(Particle particle, int flags) { + return ENUM_PARSER.toString(particle, flags); + } + + public static String[] getAllNamesWithoutData() { + return ENUM_PARSER.getPatternsWithoutData(); + } /** * The base {@link Particle} to use. This determines the properties and what data this {@link ParticleEffect} can accept. @@ -40,26 +72,34 @@ public class ParticleEffect { * This, by default, determines the speed at which a particle moves. It must be positive. *
* When {@link ParticleEffect#count} is 0, this instead acts as a multiplier to the velocity provided by {@link ParticleEffect#offset}, - * or if {@link ParticleEffect#particle} is {@link Particle#SPELL_MOB_AMBIENT} or {@link Particle#SPELL_MOB}, then + * or if {@link ParticleEffect#particle} is {@link Particle#ENTITY_EFFECT}, then * this acts as an exponent to the RGB value provided by {@link ParticleEffect#offset}. */ private float extra; /** - * This field contains extra data that some particles require. For example, {@link Particle#REDSTONE} requires {@link org.bukkit.Particle.DustOptions} + * This determines whether the particle should be visible to players at long range. + */ + private boolean force; + + /** + * This field contains extra data that some particles require. For example, {@link Particle#DUST} requires {@link org.bukkit.Particle.DustOptions} * to determine its size and colour. */ @Nullable private Object data; - ParticleEffect(Particle particle) { + public ParticleEffect(Particle particle) { this.particle = particle; this.count = 1; this.extra = 0; this.offset = new Vector(0,0,0); + this.force = false; } public void draw(Location location, boolean force) { + if (this.particle.getDataType() != Void.class && !this.particle.getDataType().isInstance(data)) + return; // data is not compatible with the particle type World world = location.getWorld(); if (world == null) return; @@ -67,9 +107,92 @@ public void draw(Location location, boolean force) { } public void drawForPlayer(Location location, Player player, boolean force) { + if (this.particle.getDataType() != Void.class && !this.particle.getDataType().isInstance(data)) + return; // data is not compatible with the particle type player.spawnParticle(particle, location, count, offset.getX(), offset.getY(), offset.getZ(), extra, data, force); } - // TODO: Add parent interface for ParticleEffect, PlayerEffect, EntityEffect? Would make spawning easier, maybe. - // TODO: add getters, setters, maybe builder class? Add spawn method. + public Particle getParticle() { + return particle; + } + + public ParticleEffect setParticle(Particle particle) { + this.particle = particle; + return this; + } + + public int getCount() { + return count; + } + + public ParticleEffect setCount(int count) { + this.count = count; + return this; + } + + public Vector getOffset() { + return offset; + } + + public ParticleEffect setOffset(Vector offset) { + this.offset = offset; + return this; + } + + public float getExtra() { + return extra; + } + + public ParticleEffect setExtra(float extra) { + this.extra = extra; + return this; + } + + public @Nullable Object getData() { + return data; + } + + public ParticleEffect setData(@Nullable Object data) { + if (data != null && !this.particle.getDataType().isInstance(data)) + throw new IllegalArgumentException("Data type " + data.getClass().getName() + " is not compatible with particle type " + this.particle.name() + " (expected " + this.particle.getDataType().getName() + ")"); + this.data = data; + return this; + } + + public boolean isForce() { + return force; + } + + public ParticleEffect setForce(boolean force) { + this.force = force; + return this; + } + + @Override + public String toString() { + return toString(null, false); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return ENUM_PARSER.toString(particle, 0) + (data != null ? " with data" + Classes.toString(data, debug ? StringMode.DEBUG : StringMode.MESSAGE) : ""); + } + + private static class ParticleParser extends EnumParser { + + public ParticleParser() { + super(Particle.class, "particle"); + } + + public String @NotNull [] getPatternsWithoutData() { + return parseMap.entrySet().stream() + .filter(entry -> { + Particle particle = entry.getValue(); + return particle.getDataType() == Void.class; + }) + .map(Map.Entry::getKey) + .toArray(String[]::new); + } + + } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index 6331f17d4c2..fa14d96c2c1 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -11,6 +11,8 @@ import ch.njol.yggdrasil.Fields; import org.bukkit.Effect; import org.bukkit.EntityEffect; +import org.bukkit.Particle; +import org.bukkit.util.Vector; import java.io.IOException; import java.io.StreamCorruptedException; @@ -19,6 +21,8 @@ public class ParticleModule { public static void load () throws IOException { + + // gane effects Classes.registerClass(new ClassInfo<>(GameEffect.class, "gameeffect") .user("game ?effects?") .since("INSERT VERSION") @@ -86,12 +90,118 @@ public String toVariableNameString(GameEffect o) { } })); + + // entity effects Classes.registerClass(new EnumClassInfo<>(EntityEffect.class, "entityeffect", "entity effect") .user("entity ?effects?") .name("Entity Effect") .description("Various entity effects that can be played for entities, like wolf howling, or villager happy.") .since("INSERT VERSION")); + + + // particles + Classes.registerClass(new ClassInfo<>(ParticleEffect.class, "particle") + .user("particles?") + .since("INSERT VERSION") + .description("Various particles.") + .name("Particle") + .usage(ParticleEffect.getAllNamesWithoutData()) + .supplier(() -> { + Particle[] particles = Particle.values(); + return Arrays.stream(particles).map(ParticleEffect::new).iterator(); + }) + .serializer(new Serializer<>() { + @Override + public Fields serialize(ParticleEffect effect) { + Fields fields = new Fields(); + fields.putPrimitive("name", effect.getParticle().name()); + fields.putPrimitive("count", effect.getCount()); + fields.putObject("offset", effect.getOffset()); + fields.putPrimitive("extra", effect.getExtra()); + fields.putObject("data", effect.getData()); + fields.putPrimitive("force", effect.isForce()); + return fields; + } + + @Override + public void deserialize(ParticleEffect effect, Fields fields) { + assert false; + } + + @Override + protected ParticleEffect deserialize(Fields fields) throws StreamCorruptedException { + String name = fields.getAndRemovePrimitive("name", String.class); + ParticleEffect effect; + try { + effect = new ParticleEffect(Particle.valueOf(name)); + } catch (IllegalArgumentException e) { + return null; + } + effect.setCount(fields.getAndRemovePrimitive("count", Integer.class)); + effect.setOffset(fields.getAndRemoveObject("offset", Vector.class)); + effect.setExtra(fields.getAndRemovePrimitive("extra", Float.class)); + effect.setData(fields.getAndRemoveObject("data", effect.getParticle().getDataType())); + effect.setForce(fields.getAndRemovePrimitive("force", Boolean.class)); + return effect; + } + + @Override + public boolean mustSyncDeserialization() { + return false; + } + + @Override + protected boolean canBeInstantiated() { + return false; + } + }) + .defaultExpression(new EventValueExpression<>(ParticleEffect.class)) + .parser(new Parser<>() { + @Override + public ParticleEffect parse(String input, ParseContext context) { + return ParticleEffect.parse(input, context); + } + + @Override + public String toString(ParticleEffect effect, int flags) { + return effect.toString(); + } + + @Override + public String toVariableNameString(ParticleEffect effect) { + return effect.getParticle().name(); + } + })); + + // bukkit particles, so Classes.toString(Particle) works + // Should not be used directly + Classes.registerClass(new ClassInfo<>(Particle.class, "bukkitparticle") + .user("bukkit ?particles?") + .since("INSERT VERSION") + .parser(new Parser<>() { + @Override + public Particle parse(String input, ParseContext context) { + throw new IllegalStateException(); + } + + @Override + public boolean canParse(ParseContext context) { + return false; + } + + @Override + public String toString(Particle particle, int flags) { + return ParticleEffect.toString(particle, flags); + } + + @Override + public String toVariableNameString(Particle particle) { + return toString(particle, 0); + } + })); + + // load elements! Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index c89ef81956b..ba35003fb5e 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -24,8 +24,8 @@ public class EffPlayEffect extends Effect implements SyntaxRuntimeErrorProducer { static { Skript.registerEffect(EffPlayEffect.class, - "[:force] (play|show|draw) %gameeffects% %directions% %locations%", - "[:force] (play|show|draw) %gameeffects% %directions% %locations% (for|to) %-players%", + "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations%", + "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations% (for|to) %-players%", "(play|show|draw) %gameeffects% %directions% %locations% (in|with) [a] [view] (radius|range) of %-number%)", "(play|show|draw) %entityeffects% on %entities%"); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java new file mode 100644 index 00000000000..87670464310 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java @@ -0,0 +1,293 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.util.SimpleExpression; +import ch.njol.skript.util.Color; +import ch.njol.skript.util.Patterns; +import ch.njol.skript.util.Timespan; +import ch.njol.util.Kleenean; +import org.bukkit.*; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.ParticleEffect; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class ExprParticleWithData extends SimpleExpression { + + private static final Patterns PATTERNS; + + private static final List REGISTERED_PATTERNS = new ArrayList<>(); + + private static void registerParticle(Particle particle, String pattern, T defaultData) { + registerParticle(particle, pattern, (event, expressions, parseResult) -> { + if (expressions[0] == null) + return defaultData; // default data if none is provided + //noinspection unchecked + T data = (T) expressions[0].getSingle(event); + if (data == null) + return defaultData; // default data if none is provided + return data; + }); + } + + private static void registerParticle(Particle particle, String pattern, GetData getData) { + REGISTERED_PATTERNS.add(new ParticlePattern(particle, pattern, getData)); + } + + static { + registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", + // + (event, expressions, parseResult) -> { + Color color = (Color) expressions[0].getSingle(event); + if (color == null) + return org.bukkit.Color.WHITE; // default color if none is provided + Number power = (Number) expressions[1].getSingle(event); + if (power == null) + power = 1.0; // default power if none is provided + return new Particle.Spell(color.asBukkitColor(), power.floatValue()); + } + // + ); + + registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE); + registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE); + registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE); + + registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + Color color = (Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Number size = (Number) expressions[1].getSingle(event); + if (size == null || size.doubleValue() <= 0) { + size = 1.0; // default size if none is provided or invalid + } + + return new Particle.DustOptions(bukkitColor, size.floatValue()); + } // + ); + + // dust color transition particle + registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + Color color = (Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Number size = (Number) expressions[1].getSingle(event); + if (size == null || size.doubleValue() <= 0) { + size = 1.0; // default size if none is provided or invalid + } + + Color toColor = (Color) expressions[2].getSingle(event); + org.bukkit.Color bukkitToColor; + if (toColor == null) { + bukkitToColor = org.bukkit.Color.WHITE; // default transition color if none is provided + } else { + bukkitToColor = toColor.asBukkitColor(); + } + + return new Particle.DustTransition(bukkitColor, bukkitToColor, size.floatValue()); + } // + ); + + registerParticle(Particle.ITEM, "[an] %itemtype% item particle[s]", + // + (event, expressions, parseResult) -> { + ItemType itemType = (ItemType) expressions[0].getSingle(event); + if (itemType == null) + return new ItemStack(Material.AIR); // default item if none is provided + return itemType.getRandom(); + } // + ); + + GetData blockdataData = (event, expressions, parseResult) -> { + // + Object object = expressions[0].getSingle(event); + if (object instanceof ItemType itemType) { + ItemStack random = itemType.getRandom(); + return Bukkit.createBlockData(random != null ? random.getType() : itemType.getMaterial()); + } else if (object instanceof BlockData blockData) { + return blockData; + } + return Bukkit.createBlockData(Material.AIR); // default block if none is provided + // + }; + registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", blockdataData); + registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", blockdataData); + registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", blockdataData); + registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", blockdataData); + registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", blockdataData); + + registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", 0.5f); + + registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with angle %-number%]", + // + (event, expressions, parseResult) -> { + if (expressions[0] == null) + return 0; // default angle if none is provided + Number angle = (Number) expressions[0].getSingle(event); + if (angle == null) + return 0; // default angle if none is provided + return (float) Math.toRadians(angle.floatValue()); + } // + ); + + registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle leading to %location% [(for|with [a] duration of) %-timespan%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + Color color = (Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Location targetLocation = (Location) expressions[1].getSingle(event); + if (targetLocation == null) + return null; + + Number durationTicks; + if (expressions[2] == null) { + durationTicks = 20; // default duration of 1 second if none is provided + } else { + Number duration = (Number) expressions[2].getSingle(event); + // default duration of 1 second if none is provided + durationTicks = Objects.requireNonNullElse(duration, 20); + } + + return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); + } // + ); + + registerParticle(Particle.VIBRATION, "[a] vibration particle moving to %entity/location% over %timespan%", + // + (event, expressions, parseResult) -> { + Object target = expressions[0].getSingle(event); + Vibration.Destination destination; + if (target instanceof Location location) { + destination = new Vibration.Destination.BlockDestination(location); + } else if (target instanceof Entity entity) { + destination = new Vibration.Destination.EntityDestination(entity); + } else { + return null; + } + + int duration; + Timespan timespan = (Timespan) expressions[1].getSingle(event); + if (timespan == null) { + duration = 20; // default duration of 1 second if none is provided + } else { + duration = (int) timespan.getAs(Timespan.TimePeriod.TICK); + } + return new Vibration(destination, duration); + } // + ); + + // create Patterns object + Object[][] patterns = new Object[REGISTERED_PATTERNS.size()][2]; + int i = 0; + for (ParticlePattern particlePattern : REGISTERED_PATTERNS) { + patterns[i][0] = particlePattern.pattern; + patterns[i][1] = particlePattern; + i++; + } + PATTERNS = new Patterns<>(patterns); + + Skript.registerExpression(ExprParticleWithData.class, ParticleEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); + } + + private ParseResult parseResult; + private Expression[] expressions; + private Particle particle; + private GetData getData; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.parseResult = parseResult; + this.expressions = expressions; + ParticlePattern particlePattern = PATTERNS.getInfo(matchedPattern); + if (particlePattern == null) + return false; + this.particle = particlePattern.particle; + this.getData = particlePattern.getData; + return true; + } + + @Override + protected ParticleEffect @Nullable [] get(Event event) { + Object data = getData.getData(event, expressions, parseResult); + if (data == null) { + error("Could not obtain required data for particle " + particle.name()); + return null; + } + ParticleEffect effect = new ParticleEffect(particle); + effect.setData(data); + return new ParticleEffect[] {effect}; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return ParticleEffect.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return "particle with data"; + } + + /** + * A helper class to store a particle and its pattern. + * + * @param particle The particle + * @param pattern The pattern + * @param getData The function to get the data from the event + */ + private record ParticlePattern(Particle particle, String pattern, GetData getData) {} + + /** + * A functional interface to get the data from the event. + * + * @param The type of the data + */ + @FunctionalInterface + interface GetData { + /** + * Get the data from the event. + * + * @param event The event to evaluate with + * @param expressions Any expressions that are used in the pattern + * @param parseResult The parse result from parsing + * @return The data to use for the effect, or null if the required data could not be obtained + */ + @Nullable T getData(Event event, Expression[] expressions, ParseResult parseResult); + } + +} diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 7265ed48735..60c8710d7af 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -1616,6 +1616,9 @@ entities: animal: name: animal¦s pattern: animal[plural:s] + tameable: + name: tameable creature¦s + pattern: tameable creature[1¦s] fish: name: fish¦es pattern: fish[plural:es] @@ -2890,6 +2893,7 @@ entity effect: hoglin_attack: hoglin attack animation iron_golen_attack: iron golem attack animation ravager_attack: ravager attack animation + ravager_roared: ravager roared animation warden_attack: warden attack animation zoglin_attack: zoglin attack animation entity_attack: entity attack animation @@ -2931,7 +2935,9 @@ entity effect: break_equipment_chestplate: break chestplate effect break_equipment_leggings: break leggings effect break_equipment_boots: break boots effect + break_equipment_saddle: break saddle effect body_break: break body armor effect, break body armour effect + break_equipment_body: break body armor effect, break body armour effect honey_block_slide_particles: honey block slide effect honey_block_fall_particles: honey block fall effect swap_hand_items: swap hands effect # this actually makes players swap hands @@ -2943,6 +2949,10 @@ entity effect: sniffer_dig: sniffer dig sound effect # only if the sniffer is already in search/dig state armadillo_peek: armadillo peek animation shake: creaking shake animation + + + hurt_drown: drown damage effect + drown_particles: drowning damage effect # sheep eat superseded by sheep eat grass 1.12.2 sheep_eat: sheep eating grass animation @@ -2955,12 +2965,129 @@ entity effect: # non-functional guardian_target: guardian target sound effect thorns_hurt: thorns hurt effect - hurt_drown: drown damage effect hurt_explosion: explosion damage effect hurt_berry_bush: hurt berry bush effect cat_tame_fail: cat tame fail effect cat_tame_success: cat tame success effect +# -- Particle Effects -- +# all patterns should include the word 'particle' +particle: + angry_villager: angry villager particle + ash: ash particle + block: block particle + block_crumble: block crumble particle + block_marker: block marker particle + bubble: bubble particle + bubble_column_up: bubble column particle + bubble_pop: bubble pop particle + campfire_cosy_smoke: campfire cosy smoke particle + campfire_signal_smoke: campfire signal smoke particle + cherry_leaves: cherry leaves particle + cloud: cloud particle + composter: composter particle + copper_fire_flame: copper flame particle + crimson_spore: crimson spore particle + crit: crit particle + current_down: downward current particle + damage_indicator: damage indicator particle + dolphin: dolphin particle + dragon_breath: dragon breath particle + dripping_dripstone_lava: dripstone dripping lava particle + dripping_dripstone_water: dripstone dripping water particle + dripping_honey: dripping honey particle + dripping_lava: dripping lava particle + dripping_obsidian_tear: dripping obsidian tear particle + dripping_water: dripping water particle + dust: dust particle + dust_color_transition: dust color transition particle + dust_pillar: dust pillar particle + dust_plume: dust plume particle + effect: potion effect particle + egg_crack: egg crack particle + elder_guardian: elder guardian particle + electric_spark: electric spark particle + enchant: enchanting particle + enchanted_hit: enchanted hit particle + end_rod: end rod particle + entity_effect: entity effect particle + explosion: explosion particle + explosion_emitter: explosion emitter particle + falling_dripstone_lava: falling dripstone lava particle + falling_dripstone_water: falling dripstone water particle + falling_dust: falling dust particle + falling_honey: falling honey particle + falling_lava: falling lava particle + falling_nectar: falling nectar particle + falling_obsidian_tear: falling obsidian tear particle + falling_spore_blossom: falling spore blossom particle + falling_water: falling water particle + firefly: firefly particle + firework: firework particle + fishing: fishing particle + flame: flame particle + flash: flash particle + glow: glow particle + glow_squid_ink: glow squid ink particle + gust: gust particle + gust_emitter_large: large gust emitter particle + gust_emitter_small: small gust emitter particle + happy_villager: happy villager particle + heart: heart particle + infested: infested particle + instant_effect: instant effect particle + item: item particle + item_cobweb: cobweb item particle + item_slime: slime item particle + item_snowball: snowball item particle + landing_honey: landing honey particle + landing_lava: landing lava particle + landing_obsidian_tear: landing obsidian tear particle + large_smoke: large smoke particle + lava: lava particle + mycelium: mycelium particle + nautilus: nautilus particle + note: note particle + ominous_spawning: ominous spawning particle + pale_oak_leaves: pale oak leaves particle + poof: poof particle + portal: portal particle + raid_omen: raid omen particle + rain: rain particle + reverse_portal: reverse portal particle + scrape: scrape particle + sculk_charge: sculk charge particle + sculk_charge_pop: sculk charge pop particle + sculk_soul: sculk soul particle + shriek: shriek particle + small_flame: small flame particle + small_gust: small gust particle + smoke: smoke particle + sneeze: sneeze particle + snowflake: snowflake particle + sonic_boom: sonic boom particle + soul: soul particle + soul_fire_flame: soul fire flame particle + spit: spit particle + splash: splash particle + spore_blossom_air: spore blossom air particle + squid_ink: squid ink particle + sweep_attack: sweep attack particle + tinted_leaves: tinted leaves particle + totem_of_undying: totem of undying particle + trail: trail particle + trial_omen: trial omen particle + trial_spawner_detection: trial spawner detection particle + trial_spawner_detection_ominous: ominous trial spawner detection particle + underwater: underwater particle + vault_connection: vault connection particle + vibration: vibration particle + warped_spore: warped spore particle + wax_off: wax off particle + wax_on: wax on particle + white_ash: white ash particle + white_smoke: white smoke particle + witch: witch particle # -- Teleport Flags -- @@ -3342,6 +3469,7 @@ types: equippablecomponent: equippable component¦s @an gameeffect: game effect¦s @a entityeffect: entity effect¦s @a + particle: particle effect¦s @a # Skript weathertype: weather type¦s @a From 251523509ed22861487f94f430410d2fa53a62d9 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sat, 29 Nov 2025 21:15:15 -0800 Subject: [PATCH 10/16] rework particles use particle builder as a base class separate child impls for properties like directional add expressions for offset/speed/scale/distribution/velocitty/count velocity/scale and distribution automatically change count as supported add serialization for particle datas (required a helper class in yggsdrasil) better particle info registration for particles with data --- .../njol/skript/expressions/ExprVelocity.java | 112 ++-- .../ch/njol/skript/expressions/ExprXOf.java | 29 +- .../njol/yggdrasil/SimpleClassSerializer.java | 71 +++ .../bukkit/particles/ParticleEffect.java | 198 ------- .../bukkit/particles/ParticleModule.java | 518 +++++++++++++++--- .../bukkit/particles/ParticleUtils.java | 156 ++++++ .../elements/effects/EffPlayEffect.java | 34 +- .../expressions/ExprParticleCount.java | 68 +++ .../expressions/ExprParticleDistribution.java | 64 +++ .../expressions/ExprParticleEffect.java | 50 -- .../expressions/ExprParticleOffset.java | 70 +++ .../expressions/ExprParticleScale.java | 63 +++ .../expressions/ExprParticleSpeed.java | 64 +++ .../expressions/ExprParticleWithData.java | 253 +-------- .../particleeffects/ConvergingEffect.java | 17 + .../particleeffects/DirectionalEffect.java | 33 ++ .../particleeffects/ParticleEffect.java | 298 ++++++++++ .../particleeffects/ParticleInfo.java | 49 ++ .../particleeffects/ScalableEffect.java | 67 +++ .../regressions/3284-itemcrack particle.sk | 3 - .../regressions/4769-fire-visualeffect.sk | 18 - .../tests/syntaxes/effects/EffPlayEffect.sk | 52 ++ .../tests/syntaxes/effects/EffVisualEffect.sk | 108 ---- 23 files changed, 1642 insertions(+), 753 deletions(-) create mode 100644 src/main/java/ch/njol/yggdrasil/SimpleClassSerializer.java delete mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java delete mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleOffset.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ConvergingEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/DirectionalEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java delete mode 100644 src/test/skript/tests/regressions/3284-itemcrack particle.sk delete mode 100644 src/test/skript/tests/regressions/4769-fire-visualeffect.sk create mode 100644 src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk delete mode 100644 src/test/skript/tests/syntaxes/effects/EffVisualEffect.sk diff --git a/src/main/java/ch/njol/skript/expressions/ExprVelocity.java b/src/main/java/ch/njol/skript/expressions/ExprVelocity.java index 3cea900ab22..4dc6c70ad71 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprVelocity.java +++ b/src/main/java/ch/njol/skript/expressions/ExprVelocity.java @@ -1,68 +1,100 @@ package ch.njol.skript.expressions; -import org.bukkit.entity.Entity; -import org.bukkit.event.Event; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.Nullable; - import ch.njol.skript.classes.Changer.ChangeMode; import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Example; import ch.njol.skript.doc.Name; import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import ch.njol.util.coll.CollectionUtils; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; +import org.skriptlang.skript.bukkit.particles.particleeffects.DirectionalEffect; -/** - * @author Sashie - */ -@Name("Vectors - Velocity") -@Description("Gets or changes velocity of an entity.") -@Examples({"set player's velocity to {_v}"}) +// TODO: replace with type property expression +@Name("Velocity") +@Description({ + "Gets or changes velocity of an entity or particle.", + "Setting the velocity of a particle will remove its random dispersion and force it to be a single particle." +}) +@Example("set player's velocity to {_v}") +@Example("set the velocity of {_particle} to vector(0, 1, 0)") +@Example(""" + if the vector length of the player's velocity is greater than 5: + send "You're moving fast!" to the player + """) @Since("2.2-dev31") -public class ExprVelocity extends SimplePropertyExpression { +public class ExprVelocity extends SimplePropertyExpression { static { - register(ExprVelocity.class, Vector.class, "velocit(y|ies)", "entities"); + register(ExprVelocity.class, Vector.class, "velocit(y|ies)", "entities/directionalparticles"); } @Override - @Nullable - public Vector convert(Entity e) { - return e.getVelocity(); + public @Nullable Vector convert(Object object) { + if (object instanceof Entity entity) + return entity.getVelocity(); + if (object instanceof DirectionalEffect particleEffect && particleEffect.hasVelocity()) + return Vector.fromJOML(particleEffect.velocity()); + return null; } @Override - @Nullable - @SuppressWarnings("null") - public Class[] acceptChange(ChangeMode mode) { + public Class @Nullable [] acceptChange(ChangeMode mode) { if ((mode == ChangeMode.ADD || mode == ChangeMode.REMOVE || mode == ChangeMode.SET || mode == ChangeMode.DELETE || mode == ChangeMode.RESET)) return CollectionUtils.array(Vector.class); return null; } @Override - @SuppressWarnings("null") - public void change(Event e, @Nullable Object[] delta, ChangeMode mode) { + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { assert mode == ChangeMode.DELETE || mode == ChangeMode.RESET || delta != null; - for (final Entity entity : getExpr().getArray(e)) { - if (entity == null) - return; - switch (mode) { - case ADD: - entity.setVelocity(entity.getVelocity().add((Vector) delta[0])); - break; - case REMOVE: - entity.setVelocity(entity.getVelocity().subtract((Vector) delta[0])); - break; - case REMOVE_ALL: - break; - case DELETE: - case RESET: - entity.setVelocity(new Vector()); - break; - case SET: - entity.setVelocity((Vector) delta[0]); + for (Object object : getExpr().getArray(event)) { + // entities + if (object instanceof Entity entity) { + switch (mode) { + case ADD: + entity.setVelocity(entity.getVelocity().add((Vector) delta[0])); + break; + case REMOVE: + entity.setVelocity(entity.getVelocity().subtract((Vector) delta[0])); + break; + case REMOVE_ALL: + break; + case DELETE: + case RESET: + entity.setVelocity(new Vector()); + break; + case SET: + entity.setVelocity((Vector) delta[0]); + } + // particles (don't allow add/remove if no velocity is set) + } else if (object instanceof DirectionalEffect particleEffect) { + switch (mode) { + case ADD: + if (!particleEffect.hasVelocity()) + continue; + particleEffect.velocity(particleEffect.velocity().add(((Vector) delta[0]).toVector3d())); + break; + case REMOVE: + if (!particleEffect.hasVelocity()) + continue; + particleEffect.velocity(particleEffect.velocity().sub(((Vector) delta[0]).toVector3d())); + break; + case REMOVE_ALL: + break; + case DELETE: + case RESET: + if (!particleEffect.hasVelocity()) + continue; + particleEffect.velocity(new Vector3d()); + break; + case SET: + particleEffect.velocity(((Vector) delta[0]).toVector3d()); + } } } } diff --git a/src/main/java/ch/njol/skript/expressions/ExprXOf.java b/src/main/java/ch/njol/skript/expressions/ExprXOf.java index d8df50519c9..8e1e8942fc6 100644 --- a/src/main/java/ch/njol/skript/expressions/ExprXOf.java +++ b/src/main/java/ch/njol/skript/expressions/ExprXOf.java @@ -2,11 +2,7 @@ import ch.njol.skript.Skript; import ch.njol.skript.aliases.ItemType; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Keywords; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; +import ch.njol.skript.doc.*; import ch.njol.skript.entity.EntityType; import ch.njol.skript.expressions.base.PropertyExpression; import ch.njol.skript.lang.Expression; @@ -18,6 +14,7 @@ import org.bukkit.event.Event; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; import java.lang.reflect.Array; import java.util.ArrayList; @@ -33,7 +30,7 @@ public class ExprXOf extends PropertyExpression { static { Skript.registerExpression(ExprXOf.class, Object.class, ExpressionType.PATTERN_MATCHES_EVERYTHING, - "%number% of %itemstacks/itemtypes/entitytype%"); + "%number% of %itemstacks/itemtypes/entitytypes/particles%"); } private Class[] possibleReturnTypes; @@ -63,6 +60,9 @@ public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelaye if (type.canReturn(EntityType.class)) { possibleReturnTypes.add(EntityType.class); } + if (type.canReturn(ParticleEffect.class)) { + possibleReturnTypes.add(ParticleEffect.class); + } this.possibleReturnTypes = possibleReturnTypes.toArray(new Class[0]); return true; @@ -74,20 +74,27 @@ protected Object[] get(Event event, Object[] source) { if (amount == null) return (Object[]) Array.newInstance(getReturnType(), 0); + long absAmount = Math.abs(amount.longValue()); + return get(source, object -> { if (object instanceof ItemStack itemStack) { itemStack = itemStack.clone(); - itemStack.setAmount(amount.intValue()); + itemStack.setAmount((int) absAmount); return itemStack; } else if (object instanceof ItemType itemType) { ItemType type = itemType.clone(); - type.setAmount(amount.intValue()); + type.setAmount(absAmount); return type; - } else { - EntityType entityType = ((EntityType) object).clone(); - entityType.amount = amount.intValue(); + } else if (object instanceof EntityType ogType) { + EntityType entityType = ogType.clone(); + entityType.amount = (int) absAmount; return entityType; + } else if (object instanceof ParticleEffect particleEffect) { + ParticleEffect effect = particleEffect.copy(); + effect.count((int) absAmount); + return effect; } + return null; }); } diff --git a/src/main/java/ch/njol/yggdrasil/SimpleClassSerializer.java b/src/main/java/ch/njol/yggdrasil/SimpleClassSerializer.java new file mode 100644 index 00000000000..4057683dd92 --- /dev/null +++ b/src/main/java/ch/njol/yggdrasil/SimpleClassSerializer.java @@ -0,0 +1,71 @@ +package ch.njol.yggdrasil; + +import org.jetbrains.annotations.Nullable; + +import java.io.NotSerializableException; +import java.io.StreamCorruptedException; + +/** + * A simple serializer for a single class. Useful for registering serializers for external classes that should not + * have their own classinfos, and that are not handled by {@link ch.njol.skript.classes.ConfigurationSerializer} + * @param the type of the class to serialize + */ +public abstract class SimpleClassSerializer extends YggdrasilSerializer { + + protected final Class type; + protected final String id; + + public SimpleClassSerializer(Class type, String id) { + this.type = type; + this.id = id; + } + + @Override + public @Nullable Class getClass(String id) { + return this.id.equals(id) ? this.type : null; + } + + @Override + public @Nullable String getID(Class clazz) { + return this.type.equals(clazz) ? this.id : null; + } + + public static abstract class NonInstantiableClassSerializer extends SimpleClassSerializer { + + public NonInstantiableClassSerializer(Class type, String id) { + super(type, id); + } + + @Override + public final boolean canBeInstantiated(Class type) { + return false; + } + + @Override + public @Nullable E newInstance(Class c) { + return null; + } + + @Override + public void deserialize(T object, Fields fields) throws StreamCorruptedException, NotSerializableException { + throw new UnsupportedOperationException("This class cannot be instantiated"); + } + + @Override + @SuppressWarnings("unchecked") + public E deserialize(Class type, Fields fields) throws StreamCorruptedException, NotSerializableException { + assert this.type.equals(type); + return (E) deserialize(fields); + } + + /** + * Used to deserialize objects that cannot be instantiated. + * + * @param fields The Fields object that holds the information about the serialised object + * @return The deserialized object. Must not be null (throw an exception instead). + */ + abstract protected T deserialize(final Fields fields) throws StreamCorruptedException, NotSerializableException; + + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java deleted file mode 100644 index 62e704944d7..00000000000 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleEffect.java +++ /dev/null @@ -1,198 +0,0 @@ -package org.skriptlang.skript.bukkit.particles; - - -import ch.njol.skript.Skript; -import ch.njol.skript.classes.EnumParser; -import ch.njol.skript.lang.Debuggable; -import ch.njol.skript.lang.ParseContext; -import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.StringMode; -import org.bukkit.Location; -import org.bukkit.Particle; -import org.bukkit.World; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.bukkit.util.Vector; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Locale; -import java.util.Map; - -/** - * A class to hold particle metadata prior to spawning - */ -public class ParticleEffect implements Debuggable { - - private static final ParticleParser ENUM_PARSER = new ParticleParser(); - - public static ParticleEffect parse(String input, ParseContext context) { - Particle particle = ENUM_PARSER.parse(input.toLowerCase(Locale.ENGLISH), context); - if (particle == null) - return null; - if (particle.getDataType() != Void.class) { - Skript.error("The " + Classes.toString(particle) + " requires data and cannot be parsed directly. Use the Particle With Data expression instead."); - return null; - } - return new ParticleEffect(particle); - } - - public static String toString(Particle particle, int flags) { - return ENUM_PARSER.toString(particle, flags); - } - - public static String[] getAllNamesWithoutData() { - return ENUM_PARSER.getPatternsWithoutData(); - } - - /** - * The base {@link Particle} to use. This determines the properties and what data this {@link ParticleEffect} can accept. - */ - private Particle particle; - - /** - * This determines how many particles to spawn with the given properties. If set to 0, {@link ParticleEffect#offset} may - * be used to determine things like colour or velocity, rather than the area where the particles spawn. - */ - private int count; - - /** - * This, by default, determines a bounding box around the spawn location in which particles are randomly offset. - * Dimensions are multiplied by roughly 8, meaning an offset of (1, 1, 1) results in particles spawning in an - * 8x8x8 cuboid centered on the spawn location. - * Particles are distributed following a Gaussian distribution, clustering towards the center. - *
- * When {@link ParticleEffect#count} is 0, however, this may instead act as a velocity vector, an RGB colour, - * or determine the colour of a note particle. - * See the wiki on the particle command for more information. - */ - private Vector offset; - - /** - * This, by default, determines the speed at which a particle moves. It must be positive. - *
- * When {@link ParticleEffect#count} is 0, this instead acts as a multiplier to the velocity provided by {@link ParticleEffect#offset}, - * or if {@link ParticleEffect#particle} is {@link Particle#ENTITY_EFFECT}, then - * this acts as an exponent to the RGB value provided by {@link ParticleEffect#offset}. - */ - private float extra; - - /** - * This determines whether the particle should be visible to players at long range. - */ - private boolean force; - - /** - * This field contains extra data that some particles require. For example, {@link Particle#DUST} requires {@link org.bukkit.Particle.DustOptions} - * to determine its size and colour. - */ - @Nullable - private Object data; - - public ParticleEffect(Particle particle) { - this.particle = particle; - this.count = 1; - this.extra = 0; - this.offset = new Vector(0,0,0); - this.force = false; - } - - public void draw(Location location, boolean force) { - if (this.particle.getDataType() != Void.class && !this.particle.getDataType().isInstance(data)) - return; // data is not compatible with the particle type - World world = location.getWorld(); - if (world == null) - return; - world.spawnParticle(particle, location, count, offset.getX(), offset.getY(), offset.getZ(), extra, data, force); - } - - public void drawForPlayer(Location location, Player player, boolean force) { - if (this.particle.getDataType() != Void.class && !this.particle.getDataType().isInstance(data)) - return; // data is not compatible with the particle type - player.spawnParticle(particle, location, count, offset.getX(), offset.getY(), offset.getZ(), extra, data, force); - } - - public Particle getParticle() { - return particle; - } - - public ParticleEffect setParticle(Particle particle) { - this.particle = particle; - return this; - } - - public int getCount() { - return count; - } - - public ParticleEffect setCount(int count) { - this.count = count; - return this; - } - - public Vector getOffset() { - return offset; - } - - public ParticleEffect setOffset(Vector offset) { - this.offset = offset; - return this; - } - - public float getExtra() { - return extra; - } - - public ParticleEffect setExtra(float extra) { - this.extra = extra; - return this; - } - - public @Nullable Object getData() { - return data; - } - - public ParticleEffect setData(@Nullable Object data) { - if (data != null && !this.particle.getDataType().isInstance(data)) - throw new IllegalArgumentException("Data type " + data.getClass().getName() + " is not compatible with particle type " + this.particle.name() + " (expected " + this.particle.getDataType().getName() + ")"); - this.data = data; - return this; - } - - public boolean isForce() { - return force; - } - - public ParticleEffect setForce(boolean force) { - this.force = force; - return this; - } - - @Override - public String toString() { - return toString(null, false); - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return ENUM_PARSER.toString(particle, 0) + (data != null ? " with data" + Classes.toString(data, debug ? StringMode.DEBUG : StringMode.MESSAGE) : ""); - } - - private static class ParticleParser extends EnumParser { - - public ParticleParser() { - super(Particle.class, "particle"); - } - - public String @NotNull [] getPatternsWithoutData() { - return parseMap.entrySet().stream() - .filter(entry -> { - Particle particle = entry.getValue(); - return particle.getDataType() == Void.class; - }) - .map(Map.Entry::getKey) - .toArray(String[]::new); - } - - } -} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index fa14d96c2c1..2269a667808 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -1,6 +1,7 @@ package org.skriptlang.skript.bukkit.particles; import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.EnumClassInfo; import ch.njol.skript.classes.Parser; @@ -8,20 +9,41 @@ import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.ColorRGB; +import ch.njol.skript.util.Timespan; +import ch.njol.skript.variables.Variables; import ch.njol.yggdrasil.Fields; -import org.bukkit.Effect; -import org.bukkit.EntityEffect; -import org.bukkit.Particle; -import org.bukkit.util.Vector; +import ch.njol.yggdrasil.SimpleClassSerializer; +import org.bukkit.*; +import org.bukkit.block.data.BlockData; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.skriptlang.skript.bukkit.particles.particleeffects.*; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleInfo.DataSupplier; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleInfo.ToString; import java.io.IOException; +import java.io.NotSerializableException; import java.io.StreamCorruptedException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.function.Function; public class ParticleModule { + public static final List> DATA_PARTICLE_INFOS = new ArrayList<>(); + public static void load () throws IOException { + registerClasses(); + registerDataSerializers(); + registerDataParticles(); + + // load elements! + Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); + } + private static void registerClasses() { // gane effects Classes.registerClass(new ClassInfo<>(GameEffect.class, "gameeffect") .user("game ?effects?") @@ -90,7 +112,6 @@ public String toVariableNameString(GameEffect o) { } })); - // entity effects Classes.registerClass(new EnumClassInfo<>(EntityEffect.class, "entityeffect", "entity effect") .user("entity ?effects?") @@ -98,64 +119,45 @@ public String toVariableNameString(GameEffect o) { .description("Various entity effects that can be played for entities, like wolf howling, or villager happy.") .since("INSERT VERSION")); - - // particles - Classes.registerClass(new ClassInfo<>(ParticleEffect.class, "particle") - .user("particles?") + + // Bukkit Particle enum. Used for Classes.toString, but should not be used directly. + Classes.registerClass(new ClassInfo<>(Particle.class, "bukkitparticle") + .name(ClassInfo.NO_DOC) .since("INSERT VERSION") - .description("Various particles.") - .name("Particle") - .usage(ParticleEffect.getAllNamesWithoutData()) - .supplier(() -> { - Particle[] particles = Particle.values(); - return Arrays.stream(particles).map(ParticleEffect::new).iterator(); - }) - .serializer(new Serializer<>() { + .parser(new Parser<>() { @Override - public Fields serialize(ParticleEffect effect) { - Fields fields = new Fields(); - fields.putPrimitive("name", effect.getParticle().name()); - fields.putPrimitive("count", effect.getCount()); - fields.putObject("offset", effect.getOffset()); - fields.putPrimitive("extra", effect.getExtra()); - fields.putObject("data", effect.getData()); - fields.putPrimitive("force", effect.isForce()); - return fields; + public Particle parse(String input, ParseContext context) { + throw new IllegalStateException(); } @Override - public void deserialize(ParticleEffect effect, Fields fields) { - assert false; + public boolean canParse(ParseContext context) { + return false; } @Override - protected ParticleEffect deserialize(Fields fields) throws StreamCorruptedException { - String name = fields.getAndRemovePrimitive("name", String.class); - ParticleEffect effect; - try { - effect = new ParticleEffect(Particle.valueOf(name)); - } catch (IllegalArgumentException e) { - return null; - } - effect.setCount(fields.getAndRemovePrimitive("count", Integer.class)); - effect.setOffset(fields.getAndRemoveObject("offset", Vector.class)); - effect.setExtra(fields.getAndRemovePrimitive("extra", Float.class)); - effect.setData(fields.getAndRemoveObject("data", effect.getParticle().getDataType())); - effect.setForce(fields.getAndRemovePrimitive("force", Boolean.class)); - return effect; + public String toString(Particle particle, int flags) { + return ParticleEffect.toString(particle, flags); } @Override - public boolean mustSyncDeserialization() { - return false; + public String toVariableNameString(Particle particle) { + return toString(particle, 0); } + })); - @Override - protected boolean canBeInstantiated() { - return false; - } + Classes.registerClass(new ClassInfo<>(ParticleEffect.class, "particle") + .user("particle( ?effect)?s?") + .since("INSERT VERSION") + .description("Various particles.") + .name("Particle") + .usage(ParticleEffect.getAllNamesWithoutData()) + .supplier(() -> { + Particle[] particles = Particle.values(); + return Arrays.stream(particles).map(ParticleEffect::of).iterator(); }) + .serializer(new ParticleSerializer()) .defaultExpression(new EventValueExpression<>(ParticleEffect.class)) .parser(new Parser<>() { @Override @@ -170,40 +172,410 @@ public String toString(ParticleEffect effect, int flags) { @Override public String toVariableNameString(ParticleEffect effect) { - return effect.getParticle().name(); + return effect.particle().name(); } })); - // bukkit particles, so Classes.toString(Particle) works - // Should not be used directly - Classes.registerClass(new ClassInfo<>(Particle.class, "bukkitparticle") - .user("bukkit ?particles?") + Classes.registerClass(new ClassInfo<>(ConvergingEffect.class, "convergingparticle") + .user("converging ?particle( ?effect)?s?") .since("INSERT VERSION") - .parser(new Parser<>() { - @Override - public Particle parse(String input, ParseContext context) { - throw new IllegalStateException(); - } + .description("A particle effect where particles converge towards a point.") + .name("Converging Particle Effect") + .supplier(() -> ParticleUtils.getConvergingParticles().stream() + .map(ConvergingEffect::new) + .iterator()) + .serializer(new ParticleSerializer()) + .defaultExpression(new EventValueExpression<>(ConvergingEffect.class))); - @Override - public boolean canParse(ParseContext context) { - return false; - } + Classes.registerClass(new ClassInfo<>(DirectionalEffect.class, "directionalparticle") + .user("directional ?particle( ?effect)?s?") + .since("INSERT VERSION") + .description("A particle effect which can be given a directional velocity.") + .name("Directional Particle Effect") + .supplier(() -> ParticleUtils.getDirectionalParticles().stream() + .map(DirectionalEffect::new) + .iterator()) + .serializer(new ParticleSerializer()) + .defaultExpression(new EventValueExpression<>(DirectionalEffect.class))); - @Override - public String toString(Particle particle, int flags) { - return ParticleEffect.toString(particle, flags); - } + Classes.registerClass(new ClassInfo<>(ScalableEffect.class, "scalableparticle") + .user("scalable ?particle( ?effect)?s?") + .since("INSERT VERSION") + .description("A particle effect which can be scaled up or down.") + .name("Scalable Particle Effect") + .supplier(() -> ParticleUtils.getScalableParticles().stream() + .map(ScalableEffect::new) + .iterator()) + .serializer(new ParticleSerializer()) + .defaultExpression(new EventValueExpression<>(ScalableEffect.class))); + } - @Override - public String toVariableNameString(Particle particle) { - return toString(particle, 0); - } - })); + private static void registerParticle(Particle particle, String pattern, D defaultData, Function dataFunction, ToString toStringFunction) { + registerParticle(particle, pattern, (event, expressions, parseResult) -> { + if (expressions[0] == null) + return defaultData; // default data if none is provided + //noinspection unchecked + D data = dataFunction.apply((F) expressions[0].getSingle(event)); + if (data == null) + return defaultData; // default data if none is provided + return data; + }, toStringFunction); + } - // load elements! - Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); + private static void registerParticle(Particle particle, String pattern, DataSupplier dataSupplier, ToString toStringFunction) { + DATA_PARTICLE_INFOS.add(new ParticleInfo<>(particle, pattern, dataSupplier, toStringFunction)); + } + + private static void registerDataParticles() { + + // colors + + registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", + // + (event, expressions, parseResult) -> { + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) + color = ColorRGB.fromBukkitColor(org.bukkit.Color.WHITE); // default color if none is provided + Number power = (Number) expressions[1].getSingle(event); + if (power == null) + power = 1.0; // default power if none is provided + return new Particle.Spell(color.asBukkitColor(), power.floatValue()); + }, + // + spell -> Classes.toString(ColorRGB.fromBukkitColor(spell.getColor())) + " effect particle of power " + spell.getPower()); + + registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " potion effect particle"); + + registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " flash particle"); + + registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " tinted leaves particle"); + + registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Number size = (Number) expressions[1].getSingle(event); + if (size == null || size.doubleValue() <= 0) { + size = 1.0; // default size if none is provided or invalid + } + + return new Particle.DustOptions(bukkitColor, size.floatValue()); + }, // + dustOptions -> Classes.toString(ColorRGB.fromBukkitColor(dustOptions.getColor())) + " dust particle of size " + dustOptions.getSize()); + + // dust color transition particle + registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Number size = (Number) expressions[1].getSingle(event); + if (size == null || size.doubleValue() <= 0) { + size = 1.0; // default size if none is provided or invalid + } + + ch.njol.skript.util.Color toColor = (ch.njol.skript.util.Color) expressions[2].getSingle(event); + org.bukkit.Color bukkitToColor; + if (toColor == null) { + bukkitToColor = org.bukkit.Color.WHITE; // default transition color if none is provided + } else { + bukkitToColor = toColor.asBukkitColor(); + } + + return new Particle.DustTransition(bukkitColor, bukkitToColor, size.floatValue()); + }, // + dustTransition -> Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getColor())) + + " dust particle of size " + dustTransition.getSize() + + " that transitions to " + Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getToColor()))); + + // blockdata + + DataSupplier blockdataData = (event, expressions, parseResult) -> { + // + Object object = expressions[0].getSingle(event); + if (object instanceof ItemType itemType) { + ItemStack random = itemType.getRandom(); + return Bukkit.createBlockData(random != null ? random.getType() : itemType.getMaterial()); + } else if (object instanceof BlockData blockData) { + return blockData; + } + return Bukkit.createBlockData(Material.AIR); // default block if none is provided + // + }; + + registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", blockdataData, + blockData -> Classes.toString(blockData) + " block particle"); + + registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", blockdataData, + blockData -> Classes.toString(blockData) + " block crumble particle"); + + registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", blockdataData, + blockData -> Classes.toString(blockData) + " block marker particle"); + + registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", blockdataData, + blockData -> Classes.toString(blockData) + " dust pillar particle"); + + registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", blockdataData, + blockData -> "falling " + Classes.toString(blockData) + " dust particle"); + + registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", 0.5f, input -> input, + power -> "dragon breath particle of power " + power); + + // misc + + registerParticle(Particle.ITEM, "[an] %itemtype% item particle[s]", + // + (event, expressions, parseResult) -> { + ItemType itemType = (ItemType) expressions[0].getSingle(event); + if (itemType == null) + return new ItemStack(Material.AIR); // default item if none is provided + return itemType.getRandom(); + }, // + itemStack -> Classes.toString(itemStack) + " item particle"); + + registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with [a] roll angle [of] %-number%]", + // + (event, expressions, parseResult) -> { + if (expressions[0] == null) + return 0.0f; // default angle if none is provided + Number angle = (Number) expressions[0].getSingle(event); + if (angle == null) + return 0.0f; // default angle if none is provided + return (float) Math.toRadians(angle.floatValue()); + }, // + angle -> "sculk charge particle with roll angle " + Math.toDegrees(angle) + " degrees"); + + registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Location targetLocation = (Location) expressions[1].getSingle(event); + if (targetLocation == null) + return null; + + Number durationTicks= 20; + if (expressions[2] != null) { + Timespan duration = (Timespan) expressions[2].getSingle(event); + if (duration != null) + durationTicks = duration.getAs(Timespan.TimePeriod.TICK); + } + + return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); + }, // + trail -> Classes.toString(ColorRGB.fromBukkitColor(trail.getColor())) + + " trail particle leading to " + Classes.toString(trail.getTarget()) + + " over " + trail.getDuration() + " ticks"); + + registerParticle(Particle.VIBRATION, "[a] vibration particle moving to[wards] %entity/location% over [a duration of] %timespan%", + // + (event, expressions, parseResult) -> { + Object target = expressions[0].getSingle(event); + Vibration.Destination destination; + if (target instanceof Location location) { + destination = new Vibration.Destination.BlockDestination(location); + } else if (target instanceof Entity entity) { + destination = new Vibration.Destination.EntityDestination(entity); + } else { + return null; + } + + int duration; + Timespan timespan = (Timespan) expressions[1].getSingle(event); + if (timespan == null) { + duration = 20; // default duration of 1 second if none is provided + } else { + duration = (int) timespan.getAs(Timespan.TimePeriod.TICK); + } + return new Vibration(destination, duration); + }, // + vibration -> "vibration particle moving to " + + (vibration.getDestination() instanceof Vibration.Destination.BlockDestination blockDestination ? + Classes.toString(blockDestination.getLocation()) : + Classes.toString(((Vibration.Destination.EntityDestination) vibration.getDestination()).getEntity()) + ) + + " over " + vibration.getArrivalTime() + " ticks"); + } + + private static void registerDataSerializers() { + // allow serializing particle data classes + Variables.yggdrasil.registerSingleClass(Color.class, "particle.color"); + Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.DustOptions.class, "particle.dustoptions") { + @Override + public Fields serialize(Particle.DustOptions object) { + Fields fields = new Fields(); + fields.putObject("color", object.getColor()); + fields.putPrimitive("size", object.getSize()); + return fields; + } + + @Override + protected Particle.DustOptions deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Color color = fields.getAndRemoveObject("color", Color.class); + float size = fields.getAndRemovePrimitive("size", Float.class); + if (color == null) + throw new NotSerializableException("Color cannot be null for DustOptions"); + return new Particle.DustOptions(color, size); + } + }); + + Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.DustTransition.class, "particle.dusttransition") { + @Override + public Fields serialize(Particle.DustTransition object) { + Fields fields = new Fields(); + fields.putObject("fromColor", object.getColor()); + fields.putObject("toColor", object.getToColor()); + fields.putPrimitive("size", object.getSize()); + return fields; + } + + @Override + protected Particle.DustTransition deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Color fromColor = fields.getAndRemoveObject("fromColor", Color.class); + Color toColor = fields.getAndRemoveObject("toColor", Color.class); + float size = fields.getAndRemovePrimitive("size", Float.class); + if (fromColor == null || toColor == null) + throw new NotSerializableException("Colors cannot be null for DustTransition"); + return new Particle.DustTransition(fromColor, toColor, size); + } + }); + + Variables.yggdrasil.registerClassResolver( new SimpleClassSerializer.NonInstantiableClassSerializer<>(Vibration.class, "particle.vibration") { + @Override + public Fields serialize(Vibration object) { + Fields fields = new Fields(); + fields.putObject("destination", object.getDestination()); + fields.putPrimitive("arrivalTime", object.getArrivalTime()); + return fields; + } + + @Override + protected Vibration deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Vibration.Destination destination = fields.getAndRemoveObject("destination", Vibration.Destination.class); + int arrivalTime = fields.getAndRemovePrimitive("arrivalTime", Integer.class); + if (destination == null) + throw new NotSerializableException("Destination cannot be null for Vibration"); + return new Vibration(destination, arrivalTime); + } + }); + + Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Spell.class, "particle.spell") { + @Override + public Fields serialize(Particle.Spell object) { + Fields fields = new Fields(); + fields.putObject("color", object.getColor()); + fields.putPrimitive("power", object.getPower()); + return fields; + } + + @Override + protected Particle.Spell deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Color color = fields.getAndRemoveObject("color", Color.class); + float power = fields.getAndRemovePrimitive("power", Float.class); + if (color == null) + throw new NotSerializableException("Color cannot be null for Spell"); + return new Particle.Spell(color, power); + } + }); + + Variables.yggdrasil.registerClassResolver( new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Trail.class, "particle.trail") { + @Override + public Fields serialize(Particle.Trail object) { + Fields fields = new Fields(); + fields.putObject("target", object.getTarget()); + fields.putObject("color", object.getColor()); + fields.putPrimitive("duration", object.getDuration()); + return fields; + } + + @Override + protected Particle.Trail deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Location target = fields.getAndRemoveObject("target", Location.class); + Color color = fields.getAndRemoveObject("color", Color.class); + int duration = fields.getAndRemovePrimitive("duration", Integer.class); + if (target == null) + throw new NotSerializableException("Target cannot be null for Trail"); + if (color == null) + throw new NotSerializableException("Color cannot be null for Trail"); + return new Particle.Trail(target, color, duration); + } + }); + } + + + + static class ParticleSerializer extends Serializer { + @Override + public Fields serialize(ParticleEffect effect) { + Fields fields = new Fields(); + fields.putObject("name", effect.particle().name()); + fields.putPrimitive("count", effect.count()); + fields.putPrimitive("offsetX", effect.offsetX()); + fields.putPrimitive("offsetY", effect.offsetY()); + fields.putPrimitive("offsetZ", effect.offsetZ()); + fields.putPrimitive("extra", effect.extra()); + fields.putObject("data", effect.data()); + fields.putPrimitive("force", effect.force()); + return fields; + } + + @Override + public void deserialize(ParticleEffect effect, Fields fields) { + assert false; + } + + @Override + protected ParticleEffect deserialize(Fields fields) throws StreamCorruptedException { + String name = fields.getAndRemoveObject("name", String.class); + ParticleEffect effect; + try { + effect = ParticleEffect.of(Particle.valueOf(name)); + } catch (IllegalArgumentException e) { + return null; + } + return effect.count(fields.getAndRemovePrimitive("count", Integer.class)) + .offset(fields.getAndRemovePrimitive("offsetX", Double.class), + fields.getAndRemovePrimitive("offsetY", Double.class), + fields.getAndRemovePrimitive("offsetZ", Double.class)) + .extra(fields.getAndRemovePrimitive("extra", Double.class)) + .force(fields.getAndRemovePrimitive("force", Boolean.class)) + .data(fields.getAndRemoveObject("data", effect.particle().getDataType())); + } + + @Override + public boolean mustSyncDeserialization() { + return false; + } + @Override + protected boolean canBeInstantiated() { + return false; + } } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java new file mode 100644 index 00000000000..ec52254f238 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java @@ -0,0 +1,156 @@ +package org.skriptlang.skript.bukkit.particles; + +import io.papermc.paper.registry.RegistryKey; +import io.papermc.paper.registry.set.RegistryKeySet; +import io.papermc.paper.registry.set.RegistrySet; +import org.bukkit.Particle; +import org.bukkit.Registry; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.Collection; +import java.util.List; + +@SuppressWarnings("UnstableApiUsage") +public class ParticleUtils { + + private static final RegistryKey PARTICLE_REGISTRY_KEY = RegistryKey.PARTICLE_TYPE; + private static final RegistryKeySet<@NotNull Particle> DIRECTIONAL_PARTICLES = RegistrySet.keySetFromValues(PARTICLE_REGISTRY_KEY, + List.of( // sourced from https://docs.papermc.io/paper/dev/particles/#list-of-directional-particles + // + Particle.BLOCK, + Particle.BUBBLE, + Particle.BUBBLE_COLUMN_UP, + Particle.BUBBLE_POP, + Particle.CAMPFIRE_COSY_SMOKE, + Particle.CAMPFIRE_SIGNAL_SMOKE, + Particle.CLOUD, + Particle.CRIT, + Particle.DAMAGE_INDICATOR, + Particle.DRAGON_BREATH, + Particle.DUST, + Particle.DUST_COLOR_TRANSITION, + Particle.DUST_PLUME, + Particle.ELECTRIC_SPARK, + Particle.ENCHANTED_HIT, + Particle.END_ROD, + Particle.FIREWORK, + Particle.FISHING, + Particle.FLAME, + Particle.FLASH, + Particle.GLOW_SQUID_INK, + Particle.ITEM, + Particle.LARGE_SMOKE, + Particle.POOF, + Particle.REVERSE_PORTAL, + Particle.SCRAPE, + Particle.SCULK_CHARGE, + Particle.SCULK_CHARGE_POP, + Particle.SCULK_SOUL, + Particle.SMALL_FLAME, + Particle.SMOKE, + Particle.SNEEZE, + Particle.SNOWFLAKE, + Particle.SOUL, + Particle.SOUL_FIRE_FLAME, + Particle.SPIT, + Particle.SQUID_INK, + Particle.TOTEM_OF_UNDYING, + Particle.TRIAL_SPAWNER_DETECTION, + Particle.TRIAL_SPAWNER_DETECTION_OMINOUS, + Particle.WAX_OFF, + Particle.WAX_ON, + Particle.WHITE_SMOKE + // + )); + private static final RegistryKeySet<@NotNull Particle> CONVERGING_PARTICLES = RegistrySet.keySetFromValues(PARTICLE_REGISTRY_KEY, + List.of( // sourced from https://docs.papermc.io/paper/dev/particles/#list-of-converging-particles + // + Particle.ENCHANT, + Particle.NAUTILUS, + Particle.OMINOUS_SPAWNING, + Particle.PORTAL, + Particle.VAULT_CONNECTION + // + )); + private static final RegistryKeySet<@NotNull Particle> RISING_PARTICLES = RegistrySet.keySetFromValues(PARTICLE_REGISTRY_KEY, + List.of( // sourced from https://docs.papermc.io/paper/dev/particles/#list-of-rising-particles + // + Particle.EFFECT, + Particle.ENTITY_EFFECT, + Particle.GLOW, + Particle.INFESTED, + Particle.INSTANT_EFFECT, + Particle.RAID_OMEN, + Particle.TRIAL_OMEN, + Particle.WITCH + // + )); + private static final RegistryKeySet<@NotNull Particle> SCALABLE_PARTICLES = RegistrySet.keySetFromValues(PARTICLE_REGISTRY_KEY, + List.of( + // + Particle.SWEEP_ATTACK, + Particle.EXPLOSION + // + )); + + + private static Collection directionalParticlesCache = null; + private static Collection convergingParticlesCache = null; + private static Collection risingParticlesCache = null; + private static Collection scalableParticlesCache = null; + + + public static boolean isDirectional(@NotNull Particle particle) { + if (directionalParticlesCache == null) + directionalParticlesCache = DIRECTIONAL_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return directionalParticlesCache.contains(particle); + } + + public static boolean isConverging(@NotNull Particle particle) { + if (convergingParticlesCache == null) + convergingParticlesCache = CONVERGING_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return convergingParticlesCache.contains(particle); + } + + public static boolean isRising(@NotNull Particle particle) { + if (risingParticlesCache == null) + risingParticlesCache = RISING_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return risingParticlesCache.contains(particle); + } + + public static boolean isScalable(@NotNull Particle particle) { + if (scalableParticlesCache == null) + scalableParticlesCache = SCALABLE_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return scalableParticlesCache.contains(particle); + } + + public static boolean usesVelocity(@NotNull Particle particle) { + return isDirectional(particle) || isRising(particle); + } + + public static @Unmodifiable @NotNull Collection getDirectionalParticles() { + if (directionalParticlesCache == null) + directionalParticlesCache = DIRECTIONAL_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return directionalParticlesCache; + } + + public static @Unmodifiable @NotNull Collection getConvergingParticles() { + if (convergingParticlesCache == null) + convergingParticlesCache = CONVERGING_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return convergingParticlesCache; + } + + public static @Unmodifiable @NotNull Collection getRisingParticles() { + if (risingParticlesCache == null) + risingParticlesCache = RISING_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return risingParticlesCache; + } + + public static @Unmodifiable @NotNull Collection getScalableParticles() { + if (scalableParticlesCache == null) + scalableParticlesCache = SCALABLE_PARTICLES.resolve(Registry.PARTICLE_TYPE); + return scalableParticlesCache; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index ba35003fb5e..0eea7c48afe 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -6,6 +6,7 @@ import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.Direction; import ch.njol.util.Kleenean; @@ -17,15 +18,15 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; -import org.skriptlang.skript.bukkit.particles.ParticleEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; import org.skriptlang.skript.log.runtime.SyntaxRuntimeErrorProducer; // TODO: better terminology than "effects", as it's getting confusing. public class EffPlayEffect extends Effect implements SyntaxRuntimeErrorProducer { static { Skript.registerEffect(EffPlayEffect.class, - "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations%", - "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations% (for|to) %-players%", + "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations% [as %-player%]", + "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations% (for|to) %-players% [as %-player%]", "(play|show|draw) %gameeffects% %directions% %locations% (in|with) [a] [view] (radius|range) of %-number%)", "(play|show|draw) %entityeffects% on %entities%"); } @@ -33,6 +34,7 @@ public class EffPlayEffect extends Effect implements SyntaxRuntimeErrorProducer private Expression toDraw; private @Nullable Expression locations; private @Nullable Expression toPlayers; + private @Nullable Expression asPlayer; private @Nullable Expression radius; private boolean force; @@ -47,10 +49,14 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is this.force = parseResult.hasTag("force"); this.toDraw = expressions[0]; switch (matchedPattern) { - case 0 -> this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); + case 0 -> { + this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); + this.asPlayer = (Expression) expressions[3]; + } case 1 -> { this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); this.toPlayers = (Expression) expressions[3]; + this.asPlayer = (Expression) expressions[4]; } case 2 -> { this.locations = Direction.combine((Expression) expressions[1], (Expression) expressions[2]); @@ -95,17 +101,8 @@ protected void execute(Event event) { } // Particles } else if (draw instanceof ParticleEffect particleEffect) { - // to everyone - if (players == null) { - for (Location location : locations) - particleEffect.draw(location, force); - // for players - } else { - for (Player player : players) { - for (Location location : locations) - particleEffect.drawForPlayer(location, player, force); - } - } + for (Location location : locations) + particleEffect.spawn(location, force, players); } } } @@ -139,7 +136,12 @@ private void drawEntityEffects(EntityEffect @NotNull [] effects, Entity @NotNull @Override public String toString(@Nullable Event event, boolean debug) { - return ""; + //noinspection DataFlowIssue + return new SyntaxStringBuilder(event, debug) + .append("play", toDraw) + .appendIf(locations != null, locations) + .appendIf(toPlayers != null, "for", toPlayers) + .toString(); } @Override diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java new file mode 100644 index 00000000000..cace90cbaaa --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java @@ -0,0 +1,68 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +public class ExprParticleCount extends SimplePropertyExpression { + + static { + register(ExprParticleCount.class, Number.class, "particle count", "particles"); + } + + @Override + public Number convert(ParticleEffect from) { + return from.count(); + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case SET, ADD, REMOVE, RESET -> new Class[]{Number.class}; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + ParticleEffect[] particleEffect = getExpr().getArray(event); + if (particleEffect == null) return; + int countDelta = 0; + if (mode != ChangeMode.RESET) { + assert delta != null; + if (delta[0] == null) return; + countDelta = ((Number) delta[0]).intValue(); + } + + switch (mode) { + case REMOVE: + countDelta = -countDelta; + // fallthrough + case ADD: + for (ParticleEffect effect : particleEffect) + effect.count(Math.clamp(effect.count() + countDelta, 0, 1000)); + break; + case SET: + for (ParticleEffect effect : particleEffect) + effect.count(Math.clamp(countDelta, 0, 1000)); // Limit count to 1000 to prevent unintended crashing + break; + case RESET: + for (ParticleEffect effect : particleEffect) + effect.count(0); + break; + } + } + + @Override + public Class getReturnType() { + return Number.class; + } + + @Override + protected String getPropertyName() { + return "particle count"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java new file mode 100644 index 00000000000..d425ec0f583 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java @@ -0,0 +1,64 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.bukkit.event.Event; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +public class ExprParticleDistribution extends SimplePropertyExpression { + + static { + register(ExprParticleDistribution.class, Vector.class, "particle distribution", "particles"); + } + + @Override + public @Nullable Vector convert(ParticleEffect from) { + return from.isUsingNormalDistribution() ? Vector.fromJOML(from.getDistribution()) : null; + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case SET, RESET -> new Class[]{Vector.class}; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + ParticleEffect[] particleEffect = getExpr().getArray(event); + if (particleEffect == null) return; + + Vector3d newVector = null; + if (mode != ChangeMode.RESET) { + assert delta != null; + if (delta[0] == null) return; + newVector = ((Vector) delta[0]).toVector3d(); + } + + switch (mode) { + case SET: + for (ParticleEffect effect : particleEffect) + effect.setDistribution(newVector); + break; + case RESET: + for (ParticleEffect effect : particleEffect) + effect.setDistribution(new Vector3d(0,0,0)); + break; + } + } + + @Override + public Class getReturnType() { + return Vector.class; + } + + @Override + protected String getPropertyName() { + return "particle distribution"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java deleted file mode 100644 index 67c8780084a..00000000000 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleEffect.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.skriptlang.skript.bukkit.particles.elements.expressions; - -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.util.Kleenean; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.particles.ParticleEffect; - -public class ExprParticleEffect extends SimpleExpression { - @Override - protected ParticleEffect @Nullable [] get(Event event) { - return new ParticleEffect[0]; - } - - @Override - public boolean isSingle() { - return false; - } - - @Override - public Class getReturnType() { - return null; - } - - @Override - public String toString(@Nullable Event event, boolean debug) { - return ""; - } - - @Override - public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { - return false; - } - // TODO: - // Syntax: - // count + (name + "particle" + data) + offset + extra - // # count: - // %number% [of] - // # offset: - // [with [an]] offset (of|by) ((%number%, %number%(,|[,] and) %number%)|%vector%) - // # extra: - // [(at|with) [a]] (speed|extra [value]) [of] %number% - // This expression should handle the common elements between all particles - // Specific data should be handled by something more dynamic, since data can vary wildly. - // Consider VisualEffect approach, or SkBee approach of various functions. - // Prefer something like VisualEffect for better readability + grammar, but needs to be - // better documented this time. -} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleOffset.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleOffset.java new file mode 100644 index 00000000000..974f116af7c --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleOffset.java @@ -0,0 +1,70 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.bukkit.event.Event; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +public class ExprParticleOffset extends SimplePropertyExpression { + + static { + register(ExprParticleOffset.class, Vector.class, "particle offset", "particles"); + } + + @Override + public @Nullable Vector convert(ParticleEffect from) { + return Vector.fromJOML(from.offset()); + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case SET, ADD, REMOVE, RESET -> new Class[]{Vector.class}; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + ParticleEffect[] particleEffect = getExpr().getArray(event); + if (particleEffect == null) return; + Vector3d vectorDelta = null; + if (mode != ChangeMode.RESET) { + assert delta != null; + if (delta[0] == null) return; + vectorDelta = ((Vector) delta[0]).toVector3d(); + } + + switch (mode) { + case REMOVE: + vectorDelta.mul(-1); + // fallthrough + case ADD: + for (ParticleEffect effect : particleEffect) + effect.offset(vectorDelta.add(effect.offset())); + break; + case SET: + for (ParticleEffect effect : particleEffect) + effect.offset(vectorDelta); + break; + case RESET: + for (ParticleEffect effect : particleEffect) + effect.offset(0, 0, 0); + break; + } + } + + @Override + public Class getReturnType() { + return Vector.class; + } + + @Override + protected String getPropertyName() { + return "particle offset"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java new file mode 100644 index 00000000000..3fa63545b4d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java @@ -0,0 +1,63 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.classes.Changer; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ScalableEffect; + +public class ExprParticleScale extends SimplePropertyExpression { + + static { + register(ExprParticleScale.class, Number.class, "scale [value]", "scalableparticles"); + } + + @Override + public @Nullable Number convert(ScalableEffect from) { + return from.scale(); + } + + @Override + public Class @Nullable [] acceptChange(Changer.ChangeMode mode) { + return switch (mode) { + case SET, ADD, REMOVE, RESET -> new Class[]{Number.class}; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, Changer.ChangeMode mode) { + ScalableEffect[] scalableEffect = getExpr().getArray(event); + if (scalableEffect == null) return; + double scaleDelta = 1; + if (mode != Changer.ChangeMode.RESET) { + assert delta != null; + scaleDelta = ((Number) delta[0]).doubleValue(); + } + + switch (mode) { + case REMOVE: + scaleDelta = -scaleDelta; + // fallthrough + case ADD: + for (ScalableEffect effect : scalableEffect) + effect.scale(effect.scale() + scaleDelta); + break; + case SET, RESET: + for (ScalableEffect effect : scalableEffect) + effect.scale(scaleDelta); + break; + } + } + + @Override + public Class getReturnType() { + return Number.class; + } + + @Override + protected String getPropertyName() { + return "scale"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java new file mode 100644 index 00000000000..7ba463372b9 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java @@ -0,0 +1,64 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +public class ExprParticleSpeed extends SimplePropertyExpression { + + static { + register(ExprParticleSpeed.class, Number.class, "speed [value]", "particles"); + } + + @Override + public @Nullable Number convert(ParticleEffect from) { + return from.extra(); + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case SET, ADD, REMOVE, RESET -> new Class[]{Number.class}; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + ParticleEffect[] particleEffect = getExpr().getArray(event); + if (particleEffect == null) return; + double extraDelta = 0; + if (mode != ChangeMode.RESET) { + assert delta != null; + if (delta[0] == null) return; + extraDelta = ((Number) delta[0]).doubleValue(); + } + + switch (mode) { + case REMOVE: + extraDelta = -extraDelta; + // fallthrough + case ADD: + for (ParticleEffect effect : particleEffect) + effect.extra(effect.extra() + extraDelta); + break; + case SET, RESET: + for (ParticleEffect effect : particleEffect) + effect.extra(extraDelta); + break; + } + } + + @Override + public Class getReturnType() { + return Number.class; + } + + @Override + protected String getPropertyName() { + return "speed"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java index 87670464310..f0f6d98845d 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java @@ -1,217 +1,29 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.Skript; -import ch.njol.skript.aliases.ItemType; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.util.Color; import ch.njol.skript.util.Patterns; -import ch.njol.skript.util.Timespan; import ch.njol.util.Kleenean; -import org.bukkit.*; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; import org.bukkit.event.Event; -import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.particles.ParticleEffect; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; +import org.skriptlang.skript.bukkit.particles.ParticleModule; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleInfo; public class ExprParticleWithData extends SimpleExpression { - private static final Patterns PATTERNS; - - private static final List REGISTERED_PATTERNS = new ArrayList<>(); - - private static void registerParticle(Particle particle, String pattern, T defaultData) { - registerParticle(particle, pattern, (event, expressions, parseResult) -> { - if (expressions[0] == null) - return defaultData; // default data if none is provided - //noinspection unchecked - T data = (T) expressions[0].getSingle(event); - if (data == null) - return defaultData; // default data if none is provided - return data; - }); - } - - private static void registerParticle(Particle particle, String pattern, GetData getData) { - REGISTERED_PATTERNS.add(new ParticlePattern(particle, pattern, getData)); - } + private static final Patterns> PATTERNS; static { - registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", - // - (event, expressions, parseResult) -> { - Color color = (Color) expressions[0].getSingle(event); - if (color == null) - return org.bukkit.Color.WHITE; // default color if none is provided - Number power = (Number) expressions[1].getSingle(event); - if (power == null) - power = 1.0; // default power if none is provided - return new Particle.Spell(color.asBukkitColor(), power.floatValue()); - } - // - ); - - registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE); - registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE); - registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE); - - registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - Color color = (Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Number size = (Number) expressions[1].getSingle(event); - if (size == null || size.doubleValue() <= 0) { - size = 1.0; // default size if none is provided or invalid - } - - return new Particle.DustOptions(bukkitColor, size.floatValue()); - } // - ); - - // dust color transition particle - registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - Color color = (Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Number size = (Number) expressions[1].getSingle(event); - if (size == null || size.doubleValue() <= 0) { - size = 1.0; // default size if none is provided or invalid - } - - Color toColor = (Color) expressions[2].getSingle(event); - org.bukkit.Color bukkitToColor; - if (toColor == null) { - bukkitToColor = org.bukkit.Color.WHITE; // default transition color if none is provided - } else { - bukkitToColor = toColor.asBukkitColor(); - } - - return new Particle.DustTransition(bukkitColor, bukkitToColor, size.floatValue()); - } // - ); - - registerParticle(Particle.ITEM, "[an] %itemtype% item particle[s]", - // - (event, expressions, parseResult) -> { - ItemType itemType = (ItemType) expressions[0].getSingle(event); - if (itemType == null) - return new ItemStack(Material.AIR); // default item if none is provided - return itemType.getRandom(); - } // - ); - - GetData blockdataData = (event, expressions, parseResult) -> { - // - Object object = expressions[0].getSingle(event); - if (object instanceof ItemType itemType) { - ItemStack random = itemType.getRandom(); - return Bukkit.createBlockData(random != null ? random.getType() : itemType.getMaterial()); - } else if (object instanceof BlockData blockData) { - return blockData; - } - return Bukkit.createBlockData(Material.AIR); // default block if none is provided - // - }; - registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", blockdataData); - registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", blockdataData); - registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", blockdataData); - registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", blockdataData); - registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", blockdataData); - - registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", 0.5f); - - registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with angle %-number%]", - // - (event, expressions, parseResult) -> { - if (expressions[0] == null) - return 0; // default angle if none is provided - Number angle = (Number) expressions[0].getSingle(event); - if (angle == null) - return 0; // default angle if none is provided - return (float) Math.toRadians(angle.floatValue()); - } // - ); - - registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle leading to %location% [(for|with [a] duration of) %-timespan%]", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - Color color = (Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Location targetLocation = (Location) expressions[1].getSingle(event); - if (targetLocation == null) - return null; - - Number durationTicks; - if (expressions[2] == null) { - durationTicks = 20; // default duration of 1 second if none is provided - } else { - Number duration = (Number) expressions[2].getSingle(event); - // default duration of 1 second if none is provided - durationTicks = Objects.requireNonNullElse(duration, 20); - } - - return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); - } // - ); - - registerParticle(Particle.VIBRATION, "[a] vibration particle moving to %entity/location% over %timespan%", - // - (event, expressions, parseResult) -> { - Object target = expressions[0].getSingle(event); - Vibration.Destination destination; - if (target instanceof Location location) { - destination = new Vibration.Destination.BlockDestination(location); - } else if (target instanceof Entity entity) { - destination = new Vibration.Destination.EntityDestination(entity); - } else { - return null; - } - - int duration; - Timespan timespan = (Timespan) expressions[1].getSingle(event); - if (timespan == null) { - duration = 20; // default duration of 1 second if none is provided - } else { - duration = (int) timespan.getAs(Timespan.TimePeriod.TICK); - } - return new Vibration(destination, duration); - } // - ); - // create Patterns object - Object[][] patterns = new Object[REGISTERED_PATTERNS.size()][2]; + Object[][] patterns = new Object[ParticleModule.DATA_PARTICLE_INFOS.size()][2]; int i = 0; - for (ParticlePattern particlePattern : REGISTERED_PATTERNS) { - patterns[i][0] = particlePattern.pattern; - patterns[i][1] = particlePattern; + for (var particleInfo : ParticleModule.DATA_PARTICLE_INFOS) { + patterns[i][0] = particleInfo.pattern(); + patterns[i][1] = particleInfo; i++; } PATTERNS = new Patterns<>(patterns); @@ -221,30 +33,25 @@ private static void registerParticle(Particle particle, String pattern, GetData< private ParseResult parseResult; private Expression[] expressions; - private Particle particle; - private GetData getData; + private ParticleInfo particleInfo; @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { this.parseResult = parseResult; this.expressions = expressions; - ParticlePattern particlePattern = PATTERNS.getInfo(matchedPattern); - if (particlePattern == null) - return false; - this.particle = particlePattern.particle; - this.getData = particlePattern.getData; - return true; + particleInfo = PATTERNS.getInfo(matchedPattern); + return particleInfo != null; } @Override protected ParticleEffect @Nullable [] get(Event event) { - Object data = getData.getData(event, expressions, parseResult); + Object data = particleInfo.dataSupplier().getData(event, expressions, parseResult); if (data == null) { - error("Could not obtain required data for particle " + particle.name()); + error("Could not obtain required data for particle " + particleInfo.particle().name()); return null; } - ParticleEffect effect = new ParticleEffect(particle); - effect.setData(data); + ParticleEffect effect = ParticleEffect.of(particleInfo.particle()); + effect.data(data); return new ParticleEffect[] {effect}; } @@ -260,34 +67,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - return "particle with data"; - } - - /** - * A helper class to store a particle and its pattern. - * - * @param particle The particle - * @param pattern The pattern - * @param getData The function to get the data from the event - */ - private record ParticlePattern(Particle particle, String pattern, GetData getData) {} - - /** - * A functional interface to get the data from the event. - * - * @param The type of the data - */ - @FunctionalInterface - interface GetData { - /** - * Get the data from the event. - * - * @param event The event to evaluate with - * @param expressions Any expressions that are used in the pattern - * @param parseResult The parse result from parsing - * @return The data to use for the effect, or null if the required data could not be obtained - */ - @Nullable T getData(Event event, Expression[] expressions, ParseResult parseResult); + Object data = particleInfo.dataSupplier().getData(event, expressions, parseResult); + return particleInfo.toStringFunction().toString(data); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ConvergingEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ConvergingEffect.java new file mode 100644 index 00000000000..83c338aec92 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ConvergingEffect.java @@ -0,0 +1,17 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + +import org.bukkit.Particle; +import org.jetbrains.annotations.ApiStatus; + +public class ConvergingEffect extends ParticleEffect { + @ApiStatus.Internal + public ConvergingEffect(Particle particle) { + super(particle); + } + + @Override + public ConvergingEffect clone() { + return (ConvergingEffect) super.clone(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/DirectionalEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/DirectionalEffect.java new file mode 100644 index 00000000000..b72e4e54bc0 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/DirectionalEffect.java @@ -0,0 +1,33 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + + +import org.bukkit.Particle; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Vector3d; + +public class DirectionalEffect extends ParticleEffect { + @ApiStatus.Internal + public DirectionalEffect(Particle particle) { + super(particle); + } + + public boolean hasVelocity() { + return count() == 0; + } + + public Vector3d velocity() { + return offset(); + } + + public DirectionalEffect velocity(Vector3d velocity) { + count(0); + offset(velocity); + return this; + } + + @Override + public DirectionalEffect clone() { + return (DirectionalEffect) super.clone(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java new file mode 100644 index 00000000000..e8b6a22f178 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java @@ -0,0 +1,298 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + +import ch.njol.skript.Skript; +import ch.njol.skript.classes.EnumParser; +import ch.njol.skript.lang.Debuggable; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +import com.destroystokyo.paper.ParticleBuilder; +import org.bukkit.Location; +import org.bukkit.Particle; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; +import org.joml.Vector3i; +import org.skriptlang.skript.bukkit.particles.ParticleModule; +import org.skriptlang.skript.bukkit.particles.ParticleUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * A wrapper around Paper's ParticleBuilder to provide additional functionality + * and a more fluent API for spawning particle effects. Categories of particles + * with special behaviors may extend this class. + *
+ * Particle behavior depends a lot on whether the count is zero or not. If count is + * zero, the offset and extra parameters are used to define a normal distribution + * for randomly offsetting particle positions. If count is greater than zero, the offset + * may be used for a number of special behaviors depending on the particle type. + * For example, {@link DirectionalEffect}s will use the offset as a velocity vector, multiplied + * by the extra parameter. {@link ScalableEffect}s will use the offset to determine scale. + */ +public class ParticleEffect extends ParticleBuilder implements Debuggable { + + @Contract("_ -> new") + public static @NotNull ParticleEffect of(Particle particle) { + if (ParticleUtils.isConverging(particle)) { + return new ConvergingEffect(particle); + } else if (ParticleUtils.usesVelocity(particle)) { + return new DirectionalEffect(particle); + } else if (ParticleUtils.isScalable(particle)) { + return new ScalableEffect(particle); + } + return new ParticleEffect(particle); + } + + // Skript parsing dependencies + + private static final ParticleParser ENUM_PARSER = new ParticleParser(); + + public static ParticleEffect parse(String input, ParseContext context) { + Particle particle = ENUM_PARSER.parse(input.toLowerCase(Locale.ENGLISH), context); + if (particle == null) + return null; + if (particle.getDataType() != Void.class) { + Skript.error("The " + Classes.toString(particle) + " requires data and cannot be parsed directly. Use the Particle With Data expression instead."); + return null; + } + return ParticleEffect.of(particle); + } + + public static String toString(Particle particle, int flags) { + return ENUM_PARSER.toString(particle, flags); + } + + public static String[] getAllNamesWithoutData() { + return ENUM_PARSER.getPatternsWithoutData(); + } + + // Instance code + + protected ParticleEffect(Particle particle) { + super(particle); + } + + @Override + public ParticleEffect spawn() { + if (dataType() != Void.class && !dataType().isInstance(data())) + return this; // data is not compatible with the particle type + return (ParticleEffect) super.spawn(); + } + + public ParticleEffect spawn(Location location, Player @Nullable... receivers) { + this.clone() + .location(location) + .receivers(receivers) + .spawn(); + return this; + } + + public ParticleEffect spawn(Location location, @Nullable List receivers) { + this.clone() + .location(location) + .receivers(receivers) + .spawn(); + return this; + } + + public ParticleEffect spawn(Location location, boolean force, Player @Nullable... receivers) { + this.clone() + .location(location) + .force(force) + .receivers(receivers) + .spawn(); + return this; + } + + public ParticleEffect spawn(Location location, boolean force, @Nullable List receivers) { + this.clone() + .location(location) + .force(force) + .receivers(receivers) + .spawn(); + return this; + } + + public Vector3d offset() { + return new Vector3d(offsetX(), offsetY(), offsetZ()); + } + + public ParticleEffect offset(Vector3d offset) { + return (ParticleEffect) super.offset(offset.x(), offset.y(), offset.z()); + } + + public ParticleEffect receivers(Vector3i radii) { + return (ParticleEffect) super.receivers(radii.x(), radii.y(), radii.z()); + } + + public ParticleEffect receivers(Vector3d radii) { + return (ParticleEffect) super.receivers((int) radii.x(), (int) radii.y(), (int) radii.z()); + } + + public boolean isUsingNormalDistribution() { + return count() != 0; + } + + public Vector3d getDistribution() { + return isUsingNormalDistribution() ? offset() : null; + } + + public void setDistribution(Vector3d distribution) { + if (!isUsingNormalDistribution()) { + count(1); + } + offset(distribution); + } + + @Override + public ParticleEffect data(@Nullable T data) { + if (data != null && !dataType().isInstance(data)) { + return this; // do not allow incompatible data types + } + return (ParticleEffect) super.data(data); + } + + public boolean acceptsData(@Nullable Object data) { + if (data == null) return true; + return dataType().isInstance(data); + } + + public Class dataType() { + return particle().getDataType(); + } + + public ParticleEffect copy() { + return (ParticleEffect) this.clone(); + } + + @Override + public String toString() { + return toString(null, false); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + if (dataType() == Void.class) + return ENUM_PARSER.toString(particle(), 0); + for (var particleInfo : ParticleModule.DATA_PARTICLE_INFOS) { + if (particleInfo.particle() == particle()) { + return particleInfo.toStringFunction().toString(data()); + } + } + // Fallback + return ENUM_PARSER.toString(particle(), 0) + " with data " + Classes.toString(data()); + } + + private static class ParticleParser extends EnumParser { + + public ParticleParser() { + super(Particle.class, "particle"); + } + + public String @NotNull [] getPatternsWithoutData() { + return parseMap.entrySet().stream() + .filter(entry -> { + Particle particle = entry.getValue(); + return particle.getDataType() == Void.class; + }) + .map(Map.Entry::getKey) + .toArray(String[]::new); + } + + } + + // + + @Override + public ParticleEffect particle(Particle particle) { + return (ParticleEffect) super.particle(particle); + } + + @Override + public ParticleEffect allPlayers() { + return (ParticleEffect) super.allPlayers(); + } + + @Override + public ParticleEffect receivers(@Nullable List receivers) { + return (ParticleEffect) super.receivers(receivers); + } + + @Override + public ParticleEffect receivers(@Nullable Collection receivers) { + return (ParticleEffect) super.receivers(receivers); + } + + @Override + public ParticleEffect receivers(Player @Nullable ... receivers) { + return (ParticleEffect) super.receivers(receivers); + } + + @Override + public ParticleEffect receivers(int radius) { + return (ParticleEffect) super.receivers(radius); + } + + @Override + public ParticleEffect receivers(int radius, boolean byDistance) { + return (ParticleEffect) super.receivers(radius, byDistance); + } + + @Override + public ParticleEffect receivers(int xzRadius, int yRadius) { + return (ParticleEffect) super.receivers(xzRadius, yRadius); + } + + @Override + public ParticleEffect receivers(int xzRadius, int yRadius, boolean byDistance) { + return (ParticleEffect) super.receivers(xzRadius, yRadius, byDistance); + } + + @Override + public ParticleEffect receivers(int xRadius, int yRadius, int zRadius) { + return (ParticleEffect) super.receivers(xRadius, yRadius, zRadius); + } + + @Override + public ParticleEffect source(@Nullable Player source) { + return (ParticleEffect) super.source(source); + } + + @Override + public ParticleEffect location(Location location) { + return (ParticleEffect) super.location(location); + } + + @Override + public ParticleEffect location(World world, double x, double y, double z) { + return (ParticleEffect) super.location(world, x, y, z); + } + + @Override + public ParticleEffect count(int count) { + return (ParticleEffect) super.count(count); + } + + @Override + public ParticleEffect offset(double offsetX, double offsetY, double offsetZ) { + return (ParticleEffect) super.offset(offsetX, offsetY, offsetZ); + } + + @Override + public ParticleEffect extra(double extra) { + return (ParticleEffect) super.extra(extra); + } + + @Override + public ParticleEffect force(boolean force) { + return (ParticleEffect) super.force(force); + } + // +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java new file mode 100644 index 00000000000..0ad98d28a56 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java @@ -0,0 +1,49 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import org.bukkit.Particle; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.elements.expressions.ExprParticleWithData; + +/** + * Information about a particle type that requires additional data. + * + * @param particle The particle type + * @param pattern The pattern that can be used to parse this particle via {@link ExprParticleWithData} + * @param dataSupplier Function to supply data from parsed expressions + * @param toStringFunction Function to convert the particle and data to a string representation + * @param The type of data required by the particle + */ +public record ParticleInfo( + Particle particle, + String pattern, + DataSupplier dataSupplier, + ToString toStringFunction +) { + + @FunctionalInterface + public interface DataSupplier { + /** + * Supplies data from the parsed expressions from a pattern. + * + * @param event The event to evaluate with + * @param expressions Any expressions that are used in the pattern + * @param parseResult The parse result from parsing + * @return The data to use for the effect, or null if the required data could not be obtained + */ + @Nullable D getData(@Nullable Event event, Expression[] expressions, ParseResult parseResult); + } + + @FunctionalInterface + public interface ToString { + /** + * Converts the particle and provided data to a string representation. + * + * @param data The particle data + * @return The string representation + */ + String toString(D data); + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java new file mode 100644 index 00000000000..cec4abb409f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java @@ -0,0 +1,67 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + +import com.destroystokyo.paper.ParticleBuilder; +import com.google.common.base.Function; +import org.bukkit.Particle; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +public class ScalableEffect extends ParticleEffect { + + private double scale; + private final ScalingFunction scalingFunction; + + // Sweep: scale = 1.0 - offsetX * 0.5 -> offsetX = (1.0 - scale) * 2.0 + // Explosion: scale = 2.0 * (1.0 - offsetX * 0.5) -> offsetX = (2.0 - scale) + private enum ScalingFunction { + SWEEP(scale -> 2 - (2 * scale)), + EXPLOSION(scale -> 2 - scale); + + private final Function scaleToOffsetX; + + ScalingFunction(Function scalingFunction) { + this.scaleToOffsetX = scalingFunction; + } + + public double apply(double scale) { + return scaleToOffsetX.apply(scale); + } + } + + private ScalingFunction getScalingFunction(@NotNull Particle particle) { + return switch (particle) { + case SWEEP_ATTACK -> ScalingFunction.SWEEP; + case EXPLOSION -> ScalingFunction.EXPLOSION; + default -> throw new IllegalArgumentException("Particle " + particle.name() + " is not a scalable effect."); + }; + } + + @ApiStatus.Internal + public ScalableEffect(Particle particle) { + super(particle); + this.scale = 1.0f; + this.scalingFunction = getScalingFunction(particle); + } + + public boolean hasScale() { + return this.count() == 0; + } + + public ParticleEffect scale(double scale) { + this.scale = scale; + count(0); + offset(scalingFunction.apply(scale), 0, 0); + return this; + } + + public double scale() { + return scale; + } + + @Override + public ParticleBuilder clone() { + ScalableEffect clone = (ScalableEffect) super.clone(); + clone.scale = this.scale; + return clone; + } +} diff --git a/src/test/skript/tests/regressions/3284-itemcrack particle.sk b/src/test/skript/tests/regressions/3284-itemcrack particle.sk deleted file mode 100644 index fc3a1b26634..00000000000 --- a/src/test/skript/tests/regressions/3284-itemcrack particle.sk +++ /dev/null @@ -1,3 +0,0 @@ -test "item crack particles": - set {_loc} to test-location - play diamond sword item crack at {_loc} diff --git a/src/test/skript/tests/regressions/4769-fire-visualeffect.sk b/src/test/skript/tests/regressions/4769-fire-visualeffect.sk deleted file mode 100644 index cfe83f94713..00000000000 --- a/src/test/skript/tests/regressions/4769-fire-visualeffect.sk +++ /dev/null @@ -1,18 +0,0 @@ -test "fire visual effect comparison": - assert a block is a block with "failed to compare block classinfo against block classinfo" - assert an itemtype is an itemtype with "failed to compare itemtype classinfo against itemtype classinfo" - assert a diamond is an itemtype with "failed to compare itemtype 'diamond' against itemtype classinfo" - set {_below} to type of block below block at spawn of world "world" - set {_block} to type of block at spawn of world "world" - set block below block at spawn of world "world" to oak planks - set block at spawn of world "world" to fire[] - assert block at spawn of world "world" is fire with "failed to compare fire (itemtype) with a block" - set block below block at spawn of world "world" to {_below} - set block at spawn of world "world" to {_block} - play 5 fire at spawn of world "world" - assert "fire" parsed as visual effect is fire with "failed to compare visual effects" - assert "fire" parsed as visual effect is "fire" parsed as itemtype to fail with "failed to compare visual effects" - spawn a chicken at spawn of world "world": - assert event-entity is a chicken with "failed to compare a chicken" - assert event-entity is a "chicken" parsed as itemtype to fail with "failed to compare a chicken" - clear event-entity diff --git a/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk b/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk new file mode 100644 index 00000000000..ea10cb8b3bd --- /dev/null +++ b/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk @@ -0,0 +1,52 @@ +test "all particle effects": + set {_effects::*} to all particle effects + set particle offset of {_effects::*} to vector(1, 1, 1) + assert particle offset of {_effects::*} is vector(1, 1, 1) with "particle offset was not set correctly for all effects" + + set speed of {_effects::*} to 2 + assert speed of {_effects::*} is 2 with "speed was not set correctly for all effects" + + set particle count of {_effects::*} to 5 + assert particle count of {_effects::*} is 5 with "count was not set correctly for all effects" + assert particle offset of {_effects::*} is vector(1, 1, 1) with "setting count from 1 to 5 changed particle offset" + assert speed of {_effects::*} is 2 with "setting count from 1 to 5 changed speed" + + set particle count of {_effects::*} to 0 + assert particle count of {_effects::*} is 0 with "count was not set correctly to 0" + assert particle offset of {_effects::*} is vector(1, 1, 1) with "setting count to 0 changed particle offset" + assert speed of {_effects::*} is 2 with "setting count to 0 changed speed" + + set the particle distribution of {_effects::*} to vector(0.5, 0.5, 0.5) + assert particle distribution of {_effects::*} is vector(0.5, 0.5, 0.5) with "particle distribution was not set correctly" + assert particle offset of {_effects::*} is vector(0.5, 0.5, 0.5) with "setting particle distribution did not set particle offset" + assert particle count of {_effects::*} is 1 with "setting particle distribution did not set count to 1" + + set particle count of {_effects::*} to 10 + assert particle count of {_effects::*} is 10 with "particle count was not set correctly" + set the particle distribution of {_effects::*} to vector(1, 1, 1) + assert particle count of {_effects::*} is 10 with "setting particle distribution did not keep particle count" + + draw {_effects::*} at location(0, 0, 0) + +test "directional particle effects": + set {_directional_effects::*} to all directional particle effects + assert particle count of {_directional_effects::*} is 1 with "directional effects particle count did not default to 1" + assert velocity of {_directional_effects::*} is not set with "directional effects had velocity at count of 1" + + set velocity of {_directional_effects::*} to vector(0, 1, 0) + assert velocity of {_directional_effects::*} is vector(0, 1, 0) with "velocity was not set correctly for directional effects" + assert particle count of {_directional_effects::*} is 0 with "setting velocity did not set particle count to 0" + assert particle distribution of {_directional_effects::*} is not set with "distribution was set when velocity was set" + + set particle count of {_directional_effects::*} to 5 + assert velocity of {_directional_effects::*} is not set with "setting particle count exited velocity mode" + assert particle offset of {_directional_effects::*} is vector(0, 1, 0) with "particle offset did not remain after exiting velocity mode" + + set particle count of {_directional_effects::*} to 0 + set particle offset of {_directional_effects::*} to vector(1, 1, 1) + assert particle offset of {_directional_effects::*} is vector(1, 1, 1) with "particle offset was not set correctly in velocity mode" + assert velocity of {_directional_effects::*} is vector(1, 1, 1) with "setting particle offset did not change velocity" + + draw {_directional_effects::*} at location(0, 0, 0) + + diff --git a/src/test/skript/tests/syntaxes/effects/EffVisualEffect.sk b/src/test/skript/tests/syntaxes/effects/EffVisualEffect.sk deleted file mode 100644 index 0c1925a6541..00000000000 --- a/src/test/skript/tests/syntaxes/effects/EffVisualEffect.sk +++ /dev/null @@ -1,108 +0,0 @@ -# we want to ensure particles parse and play on all versions -test "visual effects": - set {_} to test-location - play angry villager at {_} - play air breaking at {_} - play sprinting dust of air at {_} - play barrier at {_} - play bubble at {_} - play bubble column up at {_} - play bubble pop at {_} - play cloud at {_} - play crit at {_} - play current down at {_} - play dolphin at {_} - play dripping lava at {_} - play dripping water at {_} - play white dust with size 2 at {_} - play effect at {_} - play elder guardian particle at {_} - play enchant at {_} - play enchanted hit at {_} - play end rod at {_} - play lingering potion at {_} - play white potion swirl at {_} - play white transparent potion swirl at {_} - play large explosion at {_} - play explosion emitter at {_} - play falling dust of air at {_} - play firework spark at {_} - play fishing at {_} - play flame at {_} - play happy villager at {_} - play instant effect at {_} - play iron sword item break at {_} - play slime at {_} - play snowball at {_} - play snowball break at {_} - play large smoke at {_} - play lava pop at {_} - play mycelium at {_} - play nautilus at {_} - play note at {_} - play note of 5 at {_} - play poof at {_} - play portal at {_} - play rain at {_} - play smoke at {_} - play spit at {_} - play splash at {_} - play squid ink at {_} - play totem of undying at {_} - play suspended at {_} - play void fog at {_} - play witch particle at {_} - play campfire cosy smoke at {_} - play campfire signal smoke at {_} - play composter at {_} - play damage indicator at {_} - play dragon's breath at {_} - play falling lava at {_} - play falling water at {_} - play flash at {_} - play landing lava at {_} - play sneeze at {_} - play sweep attack at {_} - play dripping honey at {_} - play falling honey at {_} - play falling nectar at {_} - play landing honey at {_} - play ash at {_} - play crimson spore at {_} - play dripping obsidian tear at {_} - play falling obsidian tear at {_} - play landing obsidian tear at {_} - play reverse portal at {_} - play soul at {_} - play soul fire flame at {_} - play warped spore at {_} - play white ash at {_} - play light at {_} - play dripping dripstone lava at {_} - play dripping dripstone water at {_} - play dust color transition from white to white with size 1 at {_} - play falling dripstone lava at {_} - play falling dripstone water at {_} - play falling spore blossom at {_} - play glow at {_} - play glow squid ink at {_} - play scrape at {_} - play small flame at {_} - play snowflake at {_} - play spore blossom air at {_} - play vibration over 1 second at {_} - play wax off at {_} - play wax on at {_} - play air block marker at {_} - # TODO - Remove this when 1.19.4 support is dropped - parse if running minecraft "1.19.4": - play sculk charge with a roll of 1 at {_} - play sculk charge pop at {_} - play sculk soul at {_} - play shriek with a delay of 0 seconds at {_} - play sonic boom at {_} - parse if running minecraft "1.20.6": - play cherry leaves at {_} - play dust plume at {_} - play egg crack at {_} - play white smoke at {_} From dc19415b9e12015bf0cd78b286af32017b77cbde Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 30 Nov 2025 14:00:44 -0800 Subject: [PATCH 11/16] Unify effect infos between game effects and particles --- .../bukkit/particles/ParticleModule.java | 234 +----------------- .../elements/expressions/ExprGameEffect.java | 176 +------------ .../expressions/ExprParticleWithData.java | 27 +- .../particleeffects/ParticleEffect.java | 6 +- .../particleeffects/ParticleInfo.java | 49 ---- .../registration/DataGameEffects.java | 123 +++++++++ .../particles/registration/DataParticles.java | 233 +++++++++++++++++ .../particles/registration/DataSupplier.java | 79 ++++++ .../particles/registration/EffectInfo.java | 18 ++ .../particles/registration/ToString.java | 12 + 10 files changed, 503 insertions(+), 454 deletions(-) delete mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index 2269a667808..153347a0bc4 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -1,7 +1,6 @@ package org.skriptlang.skript.bukkit.particles; import ch.njol.skript.Skript; -import ch.njol.skript.aliases.ItemType; import ch.njol.skript.classes.ClassInfo; import ch.njol.skript.classes.EnumClassInfo; import ch.njol.skript.classes.Parser; @@ -9,35 +8,30 @@ import ch.njol.skript.expressions.base.EventValueExpression; import ch.njol.skript.lang.ParseContext; import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.ColorRGB; -import ch.njol.skript.util.Timespan; import ch.njol.skript.variables.Variables; import ch.njol.yggdrasil.Fields; import ch.njol.yggdrasil.SimpleClassSerializer; import org.bukkit.*; -import org.bukkit.block.data.BlockData; -import org.bukkit.entity.Entity; -import org.bukkit.inventory.ItemStack; -import org.skriptlang.skript.bukkit.particles.particleeffects.*; -import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleInfo.DataSupplier; -import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleInfo.ToString; +import org.skriptlang.skript.bukkit.particles.particleeffects.ConvergingEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.DirectionalEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.ScalableEffect; +import org.skriptlang.skript.bukkit.particles.registration.DataGameEffects; +import org.skriptlang.skript.bukkit.particles.registration.DataParticles; import java.io.IOException; import java.io.NotSerializableException; import java.io.StreamCorruptedException; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.function.Function; public class ParticleModule { - public static final List> DATA_PARTICLE_INFOS = new ArrayList<>(); public static void load () throws IOException { registerClasses(); registerDataSerializers(); - registerDataParticles(); + DataGameEffects.getGameEffectInfos(); + DataParticles.getParticleInfos(); // load elements! Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); @@ -210,218 +204,6 @@ public String toVariableNameString(ParticleEffect effect) { .defaultExpression(new EventValueExpression<>(ScalableEffect.class))); } - private static void registerParticle(Particle particle, String pattern, D defaultData, Function dataFunction, ToString toStringFunction) { - registerParticle(particle, pattern, (event, expressions, parseResult) -> { - if (expressions[0] == null) - return defaultData; // default data if none is provided - //noinspection unchecked - D data = dataFunction.apply((F) expressions[0].getSingle(event)); - if (data == null) - return defaultData; // default data if none is provided - return data; - }, toStringFunction); - } - - private static void registerParticle(Particle particle, String pattern, DataSupplier dataSupplier, ToString toStringFunction) { - DATA_PARTICLE_INFOS.add(new ParticleInfo<>(particle, pattern, dataSupplier, toStringFunction)); - } - - private static void registerDataParticles() { - - // colors - - registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", - // - (event, expressions, parseResult) -> { - ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); - if (color == null) - color = ColorRGB.fromBukkitColor(org.bukkit.Color.WHITE); // default color if none is provided - Number power = (Number) expressions[1].getSingle(event); - if (power == null) - power = 1.0; // default power if none is provided - return new Particle.Spell(color.asBukkitColor(), power.floatValue()); - }, - // - spell -> Classes.toString(ColorRGB.fromBukkitColor(spell.getColor())) + " effect particle of power " + spell.getPower()); - - registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, - color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), - color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " potion effect particle"); - - registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE, - color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), - color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " flash particle"); - - registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE, - color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), - color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " tinted leaves particle"); - - registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Number size = (Number) expressions[1].getSingle(event); - if (size == null || size.doubleValue() <= 0) { - size = 1.0; // default size if none is provided or invalid - } - - return new Particle.DustOptions(bukkitColor, size.floatValue()); - }, // - dustOptions -> Classes.toString(ColorRGB.fromBukkitColor(dustOptions.getColor())) + " dust particle of size " + dustOptions.getSize()); - - // dust color transition particle - registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Number size = (Number) expressions[1].getSingle(event); - if (size == null || size.doubleValue() <= 0) { - size = 1.0; // default size if none is provided or invalid - } - - ch.njol.skript.util.Color toColor = (ch.njol.skript.util.Color) expressions[2].getSingle(event); - org.bukkit.Color bukkitToColor; - if (toColor == null) { - bukkitToColor = org.bukkit.Color.WHITE; // default transition color if none is provided - } else { - bukkitToColor = toColor.asBukkitColor(); - } - - return new Particle.DustTransition(bukkitColor, bukkitToColor, size.floatValue()); - }, // - dustTransition -> Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getColor())) + - " dust particle of size " + dustTransition.getSize() + - " that transitions to " + Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getToColor()))); - - // blockdata - - DataSupplier blockdataData = (event, expressions, parseResult) -> { - // - Object object = expressions[0].getSingle(event); - if (object instanceof ItemType itemType) { - ItemStack random = itemType.getRandom(); - return Bukkit.createBlockData(random != null ? random.getType() : itemType.getMaterial()); - } else if (object instanceof BlockData blockData) { - return blockData; - } - return Bukkit.createBlockData(Material.AIR); // default block if none is provided - // - }; - - registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", blockdataData, - blockData -> Classes.toString(blockData) + " block particle"); - - registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", blockdataData, - blockData -> Classes.toString(blockData) + " block crumble particle"); - - registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", blockdataData, - blockData -> Classes.toString(blockData) + " block marker particle"); - - registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", blockdataData, - blockData -> Classes.toString(blockData) + " dust pillar particle"); - - registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", blockdataData, - blockData -> "falling " + Classes.toString(blockData) + " dust particle"); - - registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", 0.5f, input -> input, - power -> "dragon breath particle of power " + power); - - // misc - - registerParticle(Particle.ITEM, "[an] %itemtype% item particle[s]", - // - (event, expressions, parseResult) -> { - ItemType itemType = (ItemType) expressions[0].getSingle(event); - if (itemType == null) - return new ItemStack(Material.AIR); // default item if none is provided - return itemType.getRandom(); - }, // - itemStack -> Classes.toString(itemStack) + " item particle"); - - registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with [a] roll angle [of] %-number%]", - // - (event, expressions, parseResult) -> { - if (expressions[0] == null) - return 0.0f; // default angle if none is provided - Number angle = (Number) expressions[0].getSingle(event); - if (angle == null) - return 0.0f; // default angle if none is provided - return (float) Math.toRadians(angle.floatValue()); - }, // - angle -> "sculk charge particle with roll angle " + Math.toDegrees(angle) + " degrees"); - - registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Location targetLocation = (Location) expressions[1].getSingle(event); - if (targetLocation == null) - return null; - - Number durationTicks= 20; - if (expressions[2] != null) { - Timespan duration = (Timespan) expressions[2].getSingle(event); - if (duration != null) - durationTicks = duration.getAs(Timespan.TimePeriod.TICK); - } - - return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); - }, // - trail -> Classes.toString(ColorRGB.fromBukkitColor(trail.getColor())) + - " trail particle leading to " + Classes.toString(trail.getTarget()) + - " over " + trail.getDuration() + " ticks"); - - registerParticle(Particle.VIBRATION, "[a] vibration particle moving to[wards] %entity/location% over [a duration of] %timespan%", - // - (event, expressions, parseResult) -> { - Object target = expressions[0].getSingle(event); - Vibration.Destination destination; - if (target instanceof Location location) { - destination = new Vibration.Destination.BlockDestination(location); - } else if (target instanceof Entity entity) { - destination = new Vibration.Destination.EntityDestination(entity); - } else { - return null; - } - - int duration; - Timespan timespan = (Timespan) expressions[1].getSingle(event); - if (timespan == null) { - duration = 20; // default duration of 1 second if none is provided - } else { - duration = (int) timespan.getAs(Timespan.TimePeriod.TICK); - } - return new Vibration(destination, duration); - }, // - vibration -> "vibration particle moving to " + - (vibration.getDestination() instanceof Vibration.Destination.BlockDestination blockDestination ? - Classes.toString(blockDestination.getLocation()) : - Classes.toString(((Vibration.Destination.EntityDestination) vibration.getDestination()).getEntity()) - ) + - " over " + vibration.getArrivalTime() + " ticks"); - } - private static void registerDataSerializers() { // allow serializing particle data classes Variables.yggdrasil.registerSingleClass(Color.class, "particle.color"); diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java index 0c617886044..b03d9af1f1e 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java @@ -1,106 +1,30 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.Skript; -import ch.njol.skript.aliases.ItemType; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.util.SimpleExpression; -import ch.njol.skript.util.Direction; import ch.njol.skript.util.Patterns; import ch.njol.util.Kleenean; -import org.bukkit.Axis; -import org.bukkit.Color; import org.bukkit.Effect; -import org.bukkit.Material; -import org.bukkit.block.BlockFace; import org.bukkit.event.Event; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; - -import java.util.ArrayList; -import java.util.List; +import org.skriptlang.skript.bukkit.particles.registration.DataGameEffects; +import org.skriptlang.skript.bukkit.particles.registration.EffectInfo; public class ExprGameEffect extends SimpleExpression { - private static final Patterns PATTERNS; - - private static final List REGISTERED_PATTERNS = new ArrayList<>(); - - private static void registerEffect(Effect effect, String pattern) { - registerEffect(effect, pattern, (event, expressions, parseResult) -> expressions[0].getSingle(event)); - } - - private static void registerEffect(Effect effect, String pattern, GetData getData) { - REGISTERED_PATTERNS.add(new EffectPattern(effect, pattern, getData)); - } + private static final Patterns> PATTERNS; static { - registerEffect(Effect.RECORD_PLAY, "[record] song (of|using) %itemtype%", GetData::getMaterialData); - registerEffect(Effect.SMOKE, "[dispenser] black smoke effect [(in|with|using) direction] %direction%", - GetData::getBlockFaceData); - registerEffect(Effect.SHOOT_WHITE_SMOKE, "[dispenser] white smoke effect [(in|with|using) direction] %direction%", - GetData::getBlockFaceData); - registerEffect(Effect.STEP_SOUND, "[foot]step sound [effect] (on|of|using) %itemtype/blockdata%"); // handle version changes - registerEffect(Effect.POTION_BREAK, "[splash] potion break effect (with|of|using) [colour] %color%", - GetData::getColorData); // paper changes this type from potion data to color - registerEffect(Effect.INSTANT_POTION_BREAK, "instant [splash] potion break effect (with|of|using) [colour] %color%", - GetData::getColorData); - registerEffect(Effect.COMPOSTER_FILL_ATTEMPT, "[composter] fill[ing] (succe[ss|ed]|1:fail[ure]) sound [effect]", - (event, expressions, parseResult) -> parseResult.mark == 0); - - if (!Skript.isRunningMinecraft(1, 20, 5)) { - //noinspection removal - registerEffect(Effect.VILLAGER_PLANT_GROW, "villager plant grow[th] effect [(with|using) %-number% particles]", - GetData::defaultTo10Particles); - } - - registerEffect(Effect.BONE_MEAL_USE, "[fake] bone meal effect [(with|using) %-number% particles]", - GetData::defaultTo10Particles); - registerEffect(Effect.ELECTRIC_SPARK, "(electric|lightning[ rod]|copper) spark effect [(in|using|along) the (1:x|2:y|3:z) axis]", - (event, expressions, parseResult) -> (parseResult.mark == 0 ? null : Axis.values()[parseResult.mark - 1])); - - // All modern ones are Paper only - if (Skript.fieldExists(Effect.class, "PARTICLES_SCULK_CHARGE")) { - if (Skript.isRunningMinecraft(1, 20, 1)) { - registerEffect(Effect.PARTICLES_SCULK_CHARGE, "sculk (charge|spread) effect [(with|using) data %number%]"); // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 - registerEffect(Effect.PARTICLES_AND_SOUND_BRUSH_BLOCK_COMPLETE, "[finish] brush[ing] effect (with|using) %itemtype/blockdata%"); - } - if (Skript.isRunningMinecraft(1, 20, 4)) { - registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER, "trial spawner detect[ing|s] [%-number%] player[s] effect", - GetData::defaultTo1Player); - registerEffect(Effect.TRIAL_SPAWNER_SPAWN, "[:ominous] trial spawner spawn[ing] effect", - GetData::isOminous); - registerEffect(Effect.TRIAL_SPAWNER_SPAWN_MOB_AT, "[:ominous] trial spawner spawn[ing] mob effect with sound", - GetData::isOminous); - } - if (Skript.isRunningMinecraft(1, 20, 5)) { - registerEffect(Effect.BEE_GROWTH, "bee growth effect [(with|using) %-number% particles]", - GetData::defaultTo10Particles); - registerEffect(Effect.VAULT_ACTIVATE, "[:ominous] [trial] vault activate effect", - GetData::isOminous); - registerEffect(Effect.VAULT_DEACTIVATE, "[:ominous] [trial] vault deactivate effect", - GetData::isOminous); - registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS, "ominous trial spawner detect[ing|s] [%-number%] player[s] effect", - GetData::defaultTo1Player); - registerEffect(Effect.TRIAL_SPAWNER_BECOME_OMINOUS, "trial spawner become[ing] [:not] ominous effect", - (event, expressions, parseResult) -> !parseResult.hasTag("not")); - registerEffect(Effect.TRIAL_SPAWNER_SPAWN_ITEM, "[:ominous] trial spawner spawn[ing] item effect", - GetData::isOminous); - registerEffect(Effect.TURTLE_EGG_PLACEMENT, "place turtle egg effect [(with|using) %-number% particles]", - GetData::defaultTo10Particles); - registerEffect(Effect.SMASH_ATTACK, "[mace] smash attack effect [(with|using) %-number% particles]", - GetData::defaultTo10Particles); - } - } - // create Patterns object - Object[][] patterns = new Object[REGISTERED_PATTERNS.size()][2]; + Object[][] patterns = new Object[DataGameEffects.getGameEffectInfos().size()][2]; int i = 0; - for (EffectPattern effectPattern : REGISTERED_PATTERNS) { - patterns[i][0] = effectPattern.pattern; - patterns[i][1] = effectPattern; + for (var gameEffectInfo : DataGameEffects.getGameEffectInfos()) { + patterns[i][0] = gameEffectInfo.pattern(); + patterns[i][1] = gameEffectInfo; i++; } PATTERNS = new Patterns<>(patterns); @@ -108,16 +32,14 @@ private static void registerEffect(Effect effect, String pattern, GetData get Skript.registerExpression(ExprGameEffect.class, GameEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); } - private GameEffect gameEffect; - private GetData getData; + private EffectInfo gameEffectInfo; private Expression[] expressions; private ParseResult parseResult; @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - gameEffect = new GameEffect(PATTERNS.getInfo(matchedPattern).effect()); - getData = PATTERNS.getInfo(matchedPattern).getData(); + gameEffectInfo = PATTERNS.getInfo(matchedPattern); this.expressions = expressions; this.parseResult = parseResult; return true; @@ -125,10 +47,9 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is @Override protected GameEffect @Nullable [] get(Event event) { - return setData(gameEffect, getData.getData(event, expressions, parseResult)); - } + GameEffect gameEffect = new GameEffect(gameEffectInfo.effect()); + Object data = gameEffectInfo.dataSupplier().getData(event, expressions, parseResult); - private GameEffect @Nullable [] setData(GameEffect gameEffect, Object data){ if (data == null) return new GameEffect[0]; // invalid data, must return nothing. boolean success = gameEffect.setData(data); @@ -149,79 +70,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - // TODO: handle properly - return "game effect " + gameEffect.getEffect().name(); - } - - /** - * A helper class to store the effect and its pattern. - * - * @param effect The effect - * @param pattern The pattern - * @param getData The function to get the data from the event - */ - private record EffectPattern(Effect effect, String pattern, GetData getData) {} - - /** - * A functional interface to get the data from the event. - * - * @param The type of the data - */ - @FunctionalInterface - interface GetData { - /** - * Get the data from the event. - * - * @param event The event to evaluate with - * @param expressions Any expressions that are used in the pattern - * @param parseResult The parse result from parsing - * @return The data to use for the effect - */ - T getData(Event event, Expression[] expressions, ParseResult parseResult); - - // - // Helper functions for common data types - // - - private static @Nullable Material getMaterialData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { - Object input = expressions[0].getSingle(event); - if (!(input instanceof ItemType itemType)) - return null; - return itemType.getMaterial(); - } - - private static @Nullable BlockFace getBlockFaceData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { - Object input = expressions[0].getSingle(event); - if (!(input instanceof Direction direction)) - return null; - return Direction.toNearestBlockFace(direction.getDirection()); - } - - private static @Nullable Color getColorData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { - Object input = expressions[0].getSingle(event); - if (!(input instanceof ch.njol.skript.util.Color color)) - return null; - return color.asBukkitColor(); - } - - private static boolean isOminous(Event event, Expression[] expressions, @NotNull ParseResult parseResult) { - return parseResult.hasTag("ominous"); - } - - private static int defaultTo10Particles(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { - Object input = expressions[0].getSingle(event); - if (!(input instanceof Number number)) - return 10; - return number.intValue(); - } - - private static int defaultTo1Player(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { - Object input = expressions[0].getSingle(event); - if (!(input instanceof Number number)) - return 1; - return number.intValue(); - } - + Object data = gameEffectInfo.dataSupplier().getData(event, expressions, parseResult); + return gameEffectInfo.toStringFunction().toString(data); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java index f0f6d98845d..645394ff6bd 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java @@ -7,21 +7,22 @@ import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.util.Patterns; import ch.njol.util.Kleenean; +import org.bukkit.Particle; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.particles.ParticleModule; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; -import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleInfo; +import org.skriptlang.skript.bukkit.particles.registration.DataParticles; +import org.skriptlang.skript.bukkit.particles.registration.EffectInfo; public class ExprParticleWithData extends SimpleExpression { - private static final Patterns> PATTERNS; + private static final Patterns> PATTERNS; static { // create Patterns object - Object[][] patterns = new Object[ParticleModule.DATA_PARTICLE_INFOS.size()][2]; + Object[][] patterns = new Object[DataParticles.getParticleInfos().size()][2]; int i = 0; - for (var particleInfo : ParticleModule.DATA_PARTICLE_INFOS) { + for (var particleInfo : DataParticles.getParticleInfos()) { patterns[i][0] = particleInfo.pattern(); patterns[i][1] = particleInfo; i++; @@ -33,24 +34,24 @@ public class ExprParticleWithData extends SimpleExpression { private ParseResult parseResult; private Expression[] expressions; - private ParticleInfo particleInfo; + private EffectInfo effectInfo; @Override public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { this.parseResult = parseResult; this.expressions = expressions; - particleInfo = PATTERNS.getInfo(matchedPattern); - return particleInfo != null; + effectInfo = PATTERNS.getInfo(matchedPattern); + return effectInfo != null; } @Override protected ParticleEffect @Nullable [] get(Event event) { - Object data = particleInfo.dataSupplier().getData(event, expressions, parseResult); + Object data = effectInfo.dataSupplier().getData(event, expressions, parseResult); if (data == null) { - error("Could not obtain required data for particle " + particleInfo.particle().name()); + error("Could not obtain required data for particle " + effectInfo.effect().name()); return null; } - ParticleEffect effect = ParticleEffect.of(particleInfo.particle()); + ParticleEffect effect = ParticleEffect.of(effectInfo.effect()); effect.data(data); return new ParticleEffect[] {effect}; } @@ -67,8 +68,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - Object data = particleInfo.dataSupplier().getData(event, expressions, parseResult); - return particleInfo.toStringFunction().toString(data); + Object data = effectInfo.dataSupplier().getData(event, expressions, parseResult); + return effectInfo.toStringFunction().toString(data); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java index e8b6a22f178..1e8e3aa20a5 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java @@ -16,8 +16,8 @@ import org.jetbrains.annotations.Nullable; import org.joml.Vector3d; import org.joml.Vector3i; -import org.skriptlang.skript.bukkit.particles.ParticleModule; import org.skriptlang.skript.bukkit.particles.ParticleUtils; +import org.skriptlang.skript.bukkit.particles.registration.DataParticles; import java.util.Collection; import java.util.List; @@ -181,8 +181,8 @@ public String toString() { public String toString(@Nullable Event event, boolean debug) { if (dataType() == Void.class) return ENUM_PARSER.toString(particle(), 0); - for (var particleInfo : ParticleModule.DATA_PARTICLE_INFOS) { - if (particleInfo.particle() == particle()) { + for (var particleInfo : DataParticles.getParticleInfos()) { + if (particleInfo.effect() == particle()) { return particleInfo.toStringFunction().toString(data()); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java deleted file mode 100644 index 0ad98d28a56..00000000000 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleInfo.java +++ /dev/null @@ -1,49 +0,0 @@ -package org.skriptlang.skript.bukkit.particles.particleeffects; - -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import org.bukkit.Particle; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; -import org.skriptlang.skript.bukkit.particles.elements.expressions.ExprParticleWithData; - -/** - * Information about a particle type that requires additional data. - * - * @param particle The particle type - * @param pattern The pattern that can be used to parse this particle via {@link ExprParticleWithData} - * @param dataSupplier Function to supply data from parsed expressions - * @param toStringFunction Function to convert the particle and data to a string representation - * @param The type of data required by the particle - */ -public record ParticleInfo( - Particle particle, - String pattern, - DataSupplier dataSupplier, - ToString toStringFunction -) { - - @FunctionalInterface - public interface DataSupplier { - /** - * Supplies data from the parsed expressions from a pattern. - * - * @param event The event to evaluate with - * @param expressions Any expressions that are used in the pattern - * @param parseResult The parse result from parsing - * @return The data to use for the effect, or null if the required data could not be obtained - */ - @Nullable D getData(@Nullable Event event, Expression[] expressions, ParseResult parseResult); - } - - @FunctionalInterface - public interface ToString { - /** - * Converts the particle and provided data to a string representation. - * - * @param data The particle data - * @return The string representation - */ - String toString(D data); - } -} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java new file mode 100644 index 00000000000..0f0fd85aab1 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java @@ -0,0 +1,123 @@ +package org.skriptlang.skript.bukkit.particles.registration; + +import ch.njol.skript.registrations.Classes; +import org.bukkit.Axis; +import org.bukkit.Effect; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class DataGameEffects { + private static final List> GAME_EFFECT_INFOS = new ArrayList<>(); + + @SuppressWarnings("unchecked") + private static void registerEffect(Effect effect, String pattern, ToString toString) { + DataGameEffects.registerEffect(effect, pattern, (event, expressions, parseResult) -> (D) expressions[0].getSingle(event), toString); + } + + private static void registerEffect(Effect effect, String pattern, DataSupplier dataSupplier, ToString toString) { + GAME_EFFECT_INFOS.add(new EffectInfo<>(effect, pattern, dataSupplier, toString)); + } + + public static @Unmodifiable List> getGameEffectInfos() { + if (GAME_EFFECT_INFOS.isEmpty()) { + registerAll(); + } + return Collections.unmodifiableList(GAME_EFFECT_INFOS); + } + + private static void registerAll() { + registerEffect(Effect.RECORD_PLAY, "[record] song (of|using) %itemtype%", + DataSupplier::getMaterialData, + data -> "record song of " + Classes.toString(data)); + + registerEffect(Effect.SMOKE, "[dispenser] black smoke effect [(in|with|using) direction] %direction%", + DataSupplier::getBlockFaceData, + data -> "black smoke effect in direction " + Classes.toString(data)); + + registerEffect(Effect.SHOOT_WHITE_SMOKE, "[dispenser] white smoke effect [(in|with|using) direction] %direction%", + DataSupplier::getBlockFaceData, + data -> "white smoke effect in direction " + Classes.toString(data)); + + registerEffect(Effect.STEP_SOUND, "[foot]step sound [effect] (on|of|using) %itemtype/blockdata%", + data -> "footstep sound of " + Classes.toString(data)); // handle version changes + + registerEffect(Effect.POTION_BREAK, "%color% [splash] potion break effect", + DataSupplier::getColorData, + data -> Classes.toString(data) + " splash potion break effect"); + + registerEffect(Effect.INSTANT_POTION_BREAK, "%color% instant [splash] potion break effect", + DataSupplier::getColorData, + data -> Classes.toString(data) + " instant splash potion break effect"); + + registerEffect(Effect.COMPOSTER_FILL_ATTEMPT, "[composter] fill[ing] (succe[ss|ed]|1:fail[ure]) sound [effect]", + (event, expressions, parseResult) -> parseResult.mark == 0, + data -> (data ? "composter filling success sound effect" : "composter filling failure sound effect")); + + //noinspection removal + registerEffect(Effect.VILLAGER_PLANT_GROW, "villager plant grow[th] effect [(with|using) %-number% particles]", + DataSupplier::defaultTo10Particles, + data -> "villager plant growth effect with " + Classes.toString(data) + " particles"); + + registerEffect(Effect.BONE_MEAL_USE, "[fake] bone meal effect [(with|using) %-number% particles]", + DataSupplier::defaultTo10Particles, + data -> "bone meal effect with " + Classes.toString(data) + " particles"); + + registerEffect(Effect.ELECTRIC_SPARK, "(electric|lightning[ rod]|copper) spark effect [(in|using|along) the (1:x|2:y|3:z) axis]", + (event, expressions, parseResult) -> (parseResult.mark == 0 ? null : Axis.values()[parseResult.mark - 1]), + data -> "electric spark effect along the " + (data == null ? "default" : Classes.toString(data)) + " axis"); + + registerEffect(Effect.PARTICLES_SCULK_CHARGE, "sculk (charge|spread) effect [(with|using) data %number%]", + data -> "sculk charge effect with data " + Classes.toString(data)); // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 + + registerEffect(Effect.PARTICLES_AND_SOUND_BRUSH_BLOCK_COMPLETE, "[finish] brush[ing] %itemtype/blockdata% effect", + DataSupplier::getBlockData, + data -> "brushing " + Classes.toString(data) + " effect"); + + registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER, "trial spawner detect[ing|s] [%-number%] player[s] effect", + DataSupplier::defaultTo1Player, + data -> "trial spawner detecting " + Classes.toString(data) + " players effect"); + + registerEffect(Effect.TRIAL_SPAWNER_SPAWN, "[:ominous] trial spawner spawn[ing] effect", + DataSupplier::isOminous, + data -> (data ? "ominous trial spawner spawning effect" : "trial spawner spawning effect")); + + registerEffect(Effect.TRIAL_SPAWNER_SPAWN_MOB_AT, "[:ominous] trial spawner spawn[ing] mob effect with sound", + DataSupplier::isOminous, + data -> (data ? "ominous trial spawner spawning mob effect with sound" : "trial spawner spawning mob effect with sound")); + + registerEffect(Effect.BEE_GROWTH, "bee growth effect [(with|using) %-number% particles]", + DataSupplier::defaultTo10Particles, + data -> "bee growth effect with " + Classes.toString(data) + " particles"); + + registerEffect(Effect.VAULT_ACTIVATE, "[:ominous] [trial] vault activate effect", + DataSupplier::isOminous, + data -> (data ? "ominous trial vault activate effect" : "trial vault activate effect")); + + registerEffect(Effect.VAULT_DEACTIVATE, "[:ominous] [trial] vault deactivate effect", + DataSupplier::isOminous, + data -> (data ? "ominous trial vault deactivate effect" : "trial vault deactivate effect")); + + registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS, "ominous trial spawner detect[ing|s] [%-number%] player[s] effect", + DataSupplier::defaultTo1Player, + data -> "ominous trial spawner detecting " + Classes.toString(data) + " players effect"); + + registerEffect(Effect.TRIAL_SPAWNER_BECOME_OMINOUS, "trial spawner become[ing] [:not] ominous effect", + (event, expressions, parseResult) -> !parseResult.hasTag("not"), + data -> (data ? "trial spawner becoming ominous effect" : "trial spawner becoming not ominous effect")); + + registerEffect(Effect.TRIAL_SPAWNER_SPAWN_ITEM, "[:ominous] trial spawner spawn[ing] item effect", + DataSupplier::isOminous, + data -> (data ? "ominous trial spawner spawning item effect" : "trial spawner spawning item effect")); + + registerEffect(Effect.TURTLE_EGG_PLACEMENT, "place turtle egg effect [(with|using) %-number% particles]", + DataSupplier::defaultTo10Particles, + data -> "place turtle egg effect with " + Classes.toString(data) + " particles"); + + registerEffect(Effect.SMASH_ATTACK, "[mace] smash attack effect [(with|using) %-number% particles]", + DataSupplier::defaultTo10Particles, + data -> "smash attack effect with " + Classes.toString(data) + " particles"); + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java new file mode 100644 index 00000000000..8a565dcedb7 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java @@ -0,0 +1,233 @@ +package org.skriptlang.skript.bukkit.particles.registration; + +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.registrations.Classes; +import ch.njol.skript.util.ColorRGB; +import ch.njol.skript.util.Timespan; +import org.bukkit.Location; +import org.bukkit.Material; +import org.bukkit.Particle; +import org.bukkit.Vibration; +import org.bukkit.entity.Entity; +import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +public class DataParticles { + private static final List> PARTICLE_INFOS = new ArrayList<>(); + + private static void registerParticle(Particle particle, String pattern, D defaultData, Function dataFunction, ToString toStringFunction) { + registerParticle(particle, pattern, (event, expressions, parseResult) -> { + if (expressions[0] == null) + return defaultData; // default data if none is provided + //noinspection unchecked + D data = dataFunction.apply((F) expressions[0].getSingle(event)); + if (data == null) + return defaultData; // default data if none is provided + return data; + }, toStringFunction); + } + + private static void registerParticle(Particle particle, String pattern, DataSupplier dataSupplier, ToString toStringFunction) { + PARTICLE_INFOS.add(new EffectInfo<>(particle, pattern, dataSupplier, toStringFunction)); + } + + public static @Unmodifiable List> getParticleInfos() { + if (PARTICLE_INFOS.isEmpty()) { + registerAll(); + } + return Collections.unmodifiableList(PARTICLE_INFOS); + } + + private static void registerAll() { + + // colors + + registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", + // + (event, expressions, parseResult) -> { + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) + color = ColorRGB.fromBukkitColor(org.bukkit.Color.WHITE); // default color if none is provided + Number power = (Number) expressions[1].getSingle(event); + if (power == null) + power = 1.0; // default power if none is provided + return new Particle.Spell(color.asBukkitColor(), power.floatValue()); + }, + // + spell -> Classes.toString(ColorRGB.fromBukkitColor(spell.getColor())) + " effect particle of power " + spell.getPower()); + + registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " potion effect particle"); + + registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " flash particle"); + + registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " tinted leaves particle"); + + registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Number size = (Number) expressions[1].getSingle(event); + if (size == null || size.doubleValue() <= 0) { + size = 1.0; // default size if none is provided or invalid + } + + return new Particle.DustOptions(bukkitColor, size.floatValue()); + }, // + dustOptions -> Classes.toString(ColorRGB.fromBukkitColor(dustOptions.getColor())) + " dust particle of size " + dustOptions.getSize()); + + // dust color transition particle + registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Number size = (Number) expressions[1].getSingle(event); + if (size == null || size.doubleValue() <= 0) { + size = 1.0; // default size if none is provided or invalid + } + + ch.njol.skript.util.Color toColor = (ch.njol.skript.util.Color) expressions[2].getSingle(event); + org.bukkit.Color bukkitToColor; + if (toColor == null) { + bukkitToColor = org.bukkit.Color.WHITE; // default transition color if none is provided + } else { + bukkitToColor = toColor.asBukkitColor(); + } + + return new Particle.DustTransition(bukkitColor, bukkitToColor, size.floatValue()); + }, // + dustTransition -> Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getColor())) + + " dust particle of size " + dustTransition.getSize() + + " that transitions to " + Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getToColor()))); + + // blockdata + registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", + DataSupplier::getBlockData, + blockData -> Classes.toString(blockData) + " block particle"); + + registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", + DataSupplier::getBlockData, + blockData -> Classes.toString(blockData) + " block crumble particle"); + + registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", + DataSupplier::getBlockData, + blockData -> Classes.toString(blockData) + " block marker particle"); + + registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", + DataSupplier::getBlockData, + blockData -> Classes.toString(blockData) + " dust pillar particle"); + + registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", + DataSupplier::getBlockData, + blockData -> "falling " + Classes.toString(blockData) + " dust particle"); + + registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", + 0.5f, input -> input, + power -> "dragon breath particle of power " + power); + + // misc + + registerParticle(Particle.ITEM, "[an] %itemtype% item particle[s]", + // + (event, expressions, parseResult) -> { + ItemType itemType = (ItemType) expressions[0].getSingle(event); + if (itemType == null) + return new ItemStack(Material.AIR); // default item if none is provided + return itemType.getRandom(); + }, // + itemStack -> Classes.toString(itemStack) + " item particle"); + + registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with [a] roll angle [of] %-number%]", + // + (event, expressions, parseResult) -> { + if (expressions[0] == null) + return 0.0f; // default angle if none is provided + Number angle = (Number) expressions[0].getSingle(event); + if (angle == null) + return 0.0f; // default angle if none is provided + return (float) Math.toRadians(angle.floatValue()); + }, // + angle -> "sculk charge particle with roll angle " + Math.toDegrees(angle) + " degrees"); + + registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Location targetLocation = (Location) expressions[1].getSingle(event); + if (targetLocation == null) + return null; + + Number durationTicks= 20; + if (expressions[2] != null) { + Timespan duration = (Timespan) expressions[2].getSingle(event); + if (duration != null) + durationTicks = duration.getAs(Timespan.TimePeriod.TICK); + } + + return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); + }, // + trail -> Classes.toString(ColorRGB.fromBukkitColor(trail.getColor())) + + " trail particle leading to " + Classes.toString(trail.getTarget()) + + " over " + trail.getDuration() + " ticks"); + + registerParticle(Particle.VIBRATION, "[a] vibration particle moving to[wards] %entity/location% over [a duration of] %timespan%", + // + (event, expressions, parseResult) -> { + Object target = expressions[0].getSingle(event); + Vibration.Destination destination; + if (target instanceof Location location) { + destination = new Vibration.Destination.BlockDestination(location); + } else if (target instanceof Entity entity) { + destination = new Vibration.Destination.EntityDestination(entity); + } else { + return null; + } + + int duration; + Timespan timespan = (Timespan) expressions[1].getSingle(event); + if (timespan == null) { + duration = 20; // default duration of 1 second if none is provided + } else { + duration = (int) timespan.getAs(Timespan.TimePeriod.TICK); + } + return new Vibration(destination, duration); + }, // + vibration -> "vibration particle moving to " + + (vibration.getDestination() instanceof Vibration.Destination.BlockDestination blockDestination ? + Classes.toString(blockDestination.getLocation()) : + Classes.toString(((Vibration.Destination.EntityDestination) vibration.getDestination()).getEntity()) + ) + + " over " + vibration.getArrivalTime() + " ticks"); + } +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java new file mode 100644 index 00000000000..13f2017f4e7 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java @@ -0,0 +1,79 @@ +package org.skriptlang.skript.bukkit.particles.registration; + +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.util.Direction; +import org.bukkit.Color; +import org.bukkit.Material; +import org.bukkit.block.BlockFace; +import org.bukkit.block.data.BlockData; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@FunctionalInterface +public interface DataSupplier { + /** + * Supplies data from the parsed expressions from a pattern. + * + * @param event The event to evaluate with + * @param expressions Any expressions that are used in the pattern + * @param parseResult The parse result from parsing + * @return The data to use for the effect, or null if the required data could not be obtained + */ + @Nullable D getData(@Nullable Event event, Expression[] expressions, ParseResult parseResult); + + // + // Helper functions for common data types + // + + static @Nullable Material getMaterialData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof ItemType itemType)) + return null; + return itemType.getMaterial(); + } + + static @Nullable BlockFace getBlockFaceData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Direction direction)) + return null; + return Direction.toNearestBlockFace(direction.getDirection()); + } + + static @Nullable BlockData getBlockData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (input instanceof ItemType itemType) + return itemType.getMaterial().createBlockData(); + if (input instanceof BlockData blockData) + return blockData; + return null; + } + + static @Nullable Color getColorData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof ch.njol.skript.util.Color color)) + return null; + return color.asBukkitColor(); + } + + static boolean isOminous(Event event, Expression[] expressions, @NotNull ParseResult parseResult) { + return parseResult.hasTag("ominous"); + } + + static int defaultTo10Particles(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Number number)) + return 10; + return number.intValue(); + } + + static int defaultTo1Player(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Number number)) + return 1; + return number.intValue(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java new file mode 100644 index 00000000000..7d0c68fcf35 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java @@ -0,0 +1,18 @@ +package org.skriptlang.skript.bukkit.particles.registration; + +/** + * Information about a effect type that requires additional data. + * + * @param effect The effect type + * @param pattern The pattern that can be used to parse this particle + * @param dataSupplier Function to supply data from parsed expressions + * @param toStringFunction Function to convert the particle and data to a string representation + * @param The type of effect + * @param The type of data required by the particle + */ +public record EffectInfo( + E effect, + String pattern, + DataSupplier dataSupplier, + ToString toStringFunction +) { } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java new file mode 100644 index 00000000000..c8142f599d2 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java @@ -0,0 +1,12 @@ +package org.skriptlang.skript.bukkit.particles.registration; + +@FunctionalInterface +public interface ToString { + /** + * Converts the particle and provided data to a string representation. + * + * @param data The particle data + * @return The string representation + */ + String toString(D data); +} From 12aa9b2f8e2614bb729d80f5dd27208f60a6408d Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 30 Nov 2025 18:41:23 -0800 Subject: [PATCH 12/16] re-work toString methodology, fix version compat, various bug fixing, add tests all 2.14 tests (1.21+) pass! --- .../njol/skript/lang/SyntaxStringBuilder.java | 8 +- .../java/ch/njol/skript/util/Direction.java | 24 +- .../skript/bukkit/particles/GameEffect.java | 24 +- .../bukkit/particles/ParticleModule.java | 122 +++++++--- .../elements/effects/EffPlayEffect.java | 18 +- ...ffect.java => ExprGameEffectWithData.java} | 21 +- .../expressions/ExprParticleWithData.java | 6 +- .../particleeffects/ParticleEffect.java | 57 ++--- .../registration/DataGameEffects.java | 113 ++++++---- .../particles/registration/DataParticles.java | 213 +++++++++++------- .../particles/registration/DataSupplier.java | 11 + .../particles/registration/EffectInfo.java | 2 +- .../particles/registration/ToString.java | 15 +- src/main/resources/lang/default.lang | 22 +- .../tests/syntaxes/effects/EffPlayEffect.sk | 51 +++++ 15 files changed, 467 insertions(+), 240 deletions(-) rename src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/{ExprGameEffect.java => ExprGameEffectWithData.java} (74%) diff --git a/src/main/java/ch/njol/skript/lang/SyntaxStringBuilder.java b/src/main/java/ch/njol/skript/lang/SyntaxStringBuilder.java index f28b6acf8e8..ad647712409 100644 --- a/src/main/java/ch/njol/skript/lang/SyntaxStringBuilder.java +++ b/src/main/java/ch/njol/skript/lang/SyntaxStringBuilder.java @@ -71,11 +71,11 @@ public SyntaxStringBuilder append(@NotNull Object... objects) { * {@link Debuggable#toString(Event, boolean)}. * * @param condition The condition. - * @param object The object to add. + * @param object The object to add. Ensure this is not null. * @return The builder. * @see #append(Object) */ - public SyntaxStringBuilder appendIf(boolean condition, @NotNull Object object) { + public SyntaxStringBuilder appendIf(boolean condition, Object object) { if (condition) { append(object); } @@ -87,11 +87,11 @@ public SyntaxStringBuilder appendIf(boolean condition, @NotNull Object object) { * Spaces are automatically added between the provided objects. * * @param condition The condition. - * @param objects The objects to add. + * @param objects The objects to add. Ensure this is not null. * @return The builder. * @see #append(Object...) */ - public SyntaxStringBuilder appendIf(boolean condition, @NotNull Object... objects) { + public SyntaxStringBuilder appendIf(boolean condition, Object... objects) { if (condition) { append(objects); } diff --git a/src/main/java/ch/njol/skript/util/Direction.java b/src/main/java/ch/njol/skript/util/Direction.java index 93f2b6edcca..be81e23e109 100644 --- a/src/main/java/ch/njol/skript/util/Direction.java +++ b/src/main/java/ch/njol/skript/util/Direction.java @@ -94,7 +94,7 @@ public Direction(final Vector v) { yawOrY = v.getY(); lengthOrZ = v.getZ(); } - + public Location getRelative(final Location l) { return l.clone().add(getDirection(l)); } @@ -284,6 +284,28 @@ public static BlockFace toNearestBlockFace(Vector vector) { } return nearest; } + + /** + * Calculates the nearest cartesian {@link BlockFace} to an arbitrary unit {@link Vector}. + * + * @param vector a normalized vector + * @return the block face most closely aligned to the input vector + */ + public static @Nullable BlockFace toNearestCartesianBlockFace(Vector vector) { + double maxDot = -1; + double dot; + BlockFace nearest = null; + for (BlockFace face : new BlockFace[] { + BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST, + BlockFace.UP, BlockFace.DOWN}) { + dot = face.getDirection().dot(vector); + if (dot > maxDot) { + maxDot = dot; + nearest = face; + } + } + return nearest; + } @Override public String toString() { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index c5d5843b5d7..050d66ca063 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -1,8 +1,9 @@ package org.skriptlang.skript.bukkit.particles; import ch.njol.skript.Skript; +import ch.njol.skript.classes.EnumParser; +import ch.njol.skript.lang.ParseContext; import ch.njol.skript.registrations.Classes; -import ch.njol.skript.util.EnumUtils; import org.bukkit.Effect; import org.bukkit.Location; import org.bukkit.World; @@ -18,7 +19,7 @@ */ public class GameEffect { - public static final EnumUtils ENUM_UTILS = new EnumUtils<>(Effect.class, "game effect"); // exclude effects that require data + public static final EnumParser ENUM_UTILS = new EnumParser<>(Effect.class, "game effect"); // exclude effects that require data /** * The {@link Effect} that this object represents @@ -36,7 +37,7 @@ public GameEffect(Effect effect) { } public static GameEffect parse(String input) { - Effect effect = ENUM_UTILS.parse(input.toLowerCase(Locale.ENGLISH)); + Effect effect = ENUM_UTILS.parse(input.toLowerCase(Locale.ENGLISH), ParseContext.DEFAULT); if (effect == null) return null; if (effect.getData() != null) { @@ -59,6 +60,10 @@ public boolean setData(Object data) { if (effect.getData() != null && effect.getData().isInstance(data)) { this.data = data; return true; + } else if (effect == Effect.ELECTRIC_SPARK && data == null) { + // ELECTRIC_SPARK effect can have null data + this.data = null; + return true; } return false; } @@ -91,20 +96,21 @@ public void drawForPlayer(Location location, @NotNull Player player) { } public String toString(int flags) { - if (effect.getData() != null) - return ENUM_UTILS.toString(getEffect(), flags); - return toString(); + return ENUM_UTILS.toString(getEffect(), flags); + } + + @Override + public String toString() { + return toString(0); } static final String[] namesWithoutData = Arrays.stream(Effect.values()) .filter(effect -> effect.getData() == null) .map(Enum::name) .toArray(String[]::new); + public static String[] getAllNamesWithoutData(){ return namesWithoutData.clone(); } - - -// TODO: add getters, setters, maybe builder class? Add spawn method. } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index 153347a0bc4..bd584145126 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -22,6 +22,7 @@ import java.io.IOException; import java.io.NotSerializableException; import java.io.StreamCorruptedException; +import java.lang.reflect.InvocationTargetException; import java.util.Arrays; public class ParticleModule { @@ -266,47 +267,94 @@ protected Vibration deserialize(Fields fields) throws StreamCorruptedException, } }); - Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Spell.class, "particle.spell") { - @Override - public Fields serialize(Particle.Spell object) { - Fields fields = new Fields(); - fields.putObject("color", object.getColor()); - fields.putPrimitive("power", object.getPower()); - return fields; - } + if (Skript.isRunningMinecraft(1, 21, 9)) { + Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Spell.class, "particle.spell") { + @Override + public Fields serialize(Particle.Spell object) { + Fields fields = new Fields(); + fields.putObject("color", object.getColor()); + fields.putPrimitive("power", object.getPower()); + return fields; + } - @Override - protected Particle.Spell deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { - Color color = fields.getAndRemoveObject("color", Color.class); - float power = fields.getAndRemovePrimitive("power", Float.class); - if (color == null) - throw new NotSerializableException("Color cannot be null for Spell"); - return new Particle.Spell(color, power); - } - }); + @Override + protected Particle.Spell deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Color color = fields.getAndRemoveObject("color", Color.class); + float power = fields.getAndRemovePrimitive("power", Float.class); + if (color == null) + throw new NotSerializableException("Color cannot be null for Spell"); + return new Particle.Spell(color, power); + } + }); + } - Variables.yggdrasil.registerClassResolver( new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Trail.class, "particle.trail") { - @Override - public Fields serialize(Particle.Trail object) { - Fields fields = new Fields(); - fields.putObject("target", object.getTarget()); - fields.putObject("color", object.getColor()); - fields.putPrimitive("duration", object.getDuration()); - return fields; - } + if (Skript.isRunningMinecraft(1, 21, 4)) { + Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Trail.class, "particle.trail") { + @Override + public Fields serialize(Particle.Trail object) { + Fields fields = new Fields(); + fields.putObject("target", object.getTarget()); + fields.putObject("color", object.getColor()); + fields.putPrimitive("duration", object.getDuration()); + return fields; + } - @Override - protected Particle.Trail deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { - Location target = fields.getAndRemoveObject("target", Location.class); - Color color = fields.getAndRemoveObject("color", Color.class); - int duration = fields.getAndRemovePrimitive("duration", Integer.class); - if (target == null) - throw new NotSerializableException("Target cannot be null for Trail"); - if (color == null) - throw new NotSerializableException("Color cannot be null for Trail"); - return new Particle.Trail(target, color, duration); + @Override + protected Particle.Trail deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Location target = fields.getAndRemoveObject("target", Location.class); + Color color = fields.getAndRemoveObject("color", Color.class); + int duration = fields.getAndRemovePrimitive("duration", Integer.class); + if (target == null) + throw new NotSerializableException("Target cannot be null for Trail"); + if (color == null) + throw new NotSerializableException("Color cannot be null for Trail"); + return new Particle.Trail(target, color, duration); + } + }); + } else if (Skript.isRunningMinecraft(1, 21, 2)) { + // + var targetColorClass = Arrays.stream(Particle.class.getClasses()).filter(c -> c.getSimpleName().equals("TargetColor")).findFirst().orElse(null); + if (targetColorClass == null) + throw new RuntimeException("Could not find Particle.TargetColor class for serializer"); + try { + var constructor = targetColorClass.getDeclaredConstructor(Location.class, Color.class); + var getTargetMethod = targetColorClass.getDeclaredMethod("getTarget"); + var getColorMethod = targetColorClass.getDeclaredMethod("getColor"); + //noinspection unchecked + Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>((Class) targetColorClass, "particle.targetcolor") { + @Override + public Fields serialize(Object object) { + Fields fields = new Fields(); + try { + fields.putObject("target", getTargetMethod.invoke(object)); + fields.putObject("color", getColorMethod.invoke(object)); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + return fields; + } + + @Override + protected Object deserialize(Fields fields) throws StreamCorruptedException, NotSerializableException { + Location target = fields.getAndRemoveObject("target", Location.class); + Color color = fields.getAndRemoveObject("color", Color.class); + if (target == null) + throw new NotSerializableException("Target cannot be null for Trail"); + if (color == null) + throw new NotSerializableException("Color cannot be null for Trail"); + try { + return constructor.newInstance(target, color); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + }); + + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); } - }); + // + } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index 0eea7c48afe..328b5e50ece 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -19,15 +19,13 @@ import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.GameEffect; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; -import org.skriptlang.skript.log.runtime.SyntaxRuntimeErrorProducer; -// TODO: better terminology than "effects", as it's getting confusing. -public class EffPlayEffect extends Effect implements SyntaxRuntimeErrorProducer { +public class EffPlayEffect extends Effect { static { Skript.registerEffect(EffPlayEffect.class, - "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations% [as %-player%]", - "[:force] (play|show|draw) %gameeffects/particles% %directions% %locations% (for|to) %-players% [as %-player%]", - "(play|show|draw) %gameeffects% %directions% %locations% (in|with) [a] [view] (radius|range) of %-number%)", + "[:force] (play|show|draw) %gameeffects/particles% [%-directions% %locations%] [as %-player%]", + "[:force] (play|show|draw) %gameeffects/particles% [%-directions% %locations%] (for|to) %-players% [as %-player%]", + "(play|show|draw) %gameeffects% [%-directions% %locations%] (in|with) [a] [view] (radius|range) of %-number%)", "(play|show|draw) %entityeffects% on %entities%"); } @@ -84,6 +82,7 @@ protected void execute(Event event) { Location[] locations = this.locations.getArray(event); Object[] toDraw = this.toDraw.getArray(event); Player[] players = toPlayers != null ? toPlayers.getArray(event) : null; + Player asPlayer = this.asPlayer != null ? this.asPlayer.getSingle(event) : null; for (Object draw : toDraw) { // Game effects @@ -101,8 +100,13 @@ protected void execute(Event event) { } // Particles } else if (draw instanceof ParticleEffect particleEffect) { + particleEffect = particleEffect.copy(); // avoid modifying the original effect + if (asPlayer != null) + particleEffect.source(asPlayer); + particleEffect.force(force); + particleEffect.receivers(players); for (Location location : locations) - particleEffect.spawn(location, force, players); + particleEffect.spawn(location); } } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java similarity index 74% rename from src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java rename to src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java index b03d9af1f1e..9e287a40742 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java @@ -4,6 +4,7 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.util.Patterns; import ch.njol.util.Kleenean; @@ -14,7 +15,7 @@ import org.skriptlang.skript.bukkit.particles.registration.DataGameEffects; import org.skriptlang.skript.bukkit.particles.registration.EffectInfo; -public class ExprGameEffect extends SimpleExpression { +public class ExprGameEffectWithData extends SimpleExpression { private static final Patterns> PATTERNS; @@ -29,7 +30,7 @@ public class ExprGameEffect extends SimpleExpression { } PATTERNS = new Patterns<>(patterns); - Skript.registerExpression(ExprGameEffect.class, GameEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); + Skript.registerExpression(ExprGameEffectWithData.class, GameEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); } private EffectInfo gameEffectInfo; @@ -50,11 +51,15 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is GameEffect gameEffect = new GameEffect(gameEffectInfo.effect()); Object data = gameEffectInfo.dataSupplier().getData(event, expressions, parseResult); - if (data == null) - return new GameEffect[0]; // invalid data, must return nothing. + if (data == null && gameEffect.getEffect() != Effect.ELECTRIC_SPARK) { // electric spark doesn't require an axis + error("Could not obtain required data for " + gameEffect); + return new GameEffect[0]; + } boolean success = gameEffect.setData(data); - if (!success) - return new GameEffect[0]; // invalid data + if (!success) { + error("Could not obtain required data for " + gameEffect); + return new GameEffect[0]; + } return new GameEffect[]{gameEffect}; } @@ -70,8 +75,8 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - Object data = gameEffectInfo.dataSupplier().getData(event, expressions, parseResult); - return gameEffectInfo.toStringFunction().toString(data); + return gameEffectInfo.toStringFunction() + .toString(expressions, parseResult, new SyntaxStringBuilder(event, debug)).toString(); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java index 645394ff6bd..97b5d93c930 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java @@ -4,6 +4,7 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; import ch.njol.skript.lang.util.SimpleExpression; import ch.njol.skript.util.Patterns; import ch.njol.util.Kleenean; @@ -48,7 +49,7 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is protected ParticleEffect @Nullable [] get(Event event) { Object data = effectInfo.dataSupplier().getData(event, expressions, parseResult); if (data == null) { - error("Could not obtain required data for particle " + effectInfo.effect().name()); + error("Could not obtain required data for " + ParticleEffect.toString(effectInfo.effect(), 0)); return null; } ParticleEffect effect = ParticleEffect.of(effectInfo.effect()); @@ -68,8 +69,7 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - Object data = effectInfo.dataSupplier().getData(event, expressions, parseResult); - return effectInfo.toStringFunction().toString(data); + return effectInfo.toStringFunction().toString(expressions, parseResult, new SyntaxStringBuilder(event, debug)).toString(); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java index 1e8e3aa20a5..272e41a11ae 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java @@ -17,7 +17,6 @@ import org.joml.Vector3d; import org.joml.Vector3i; import org.skriptlang.skript.bukkit.particles.ParticleUtils; -import org.skriptlang.skript.bukkit.particles.registration.DataParticles; import java.util.Collection; import java.util.List; @@ -75,6 +74,20 @@ public static String[] getAllNamesWithoutData() { // Instance code + public ParticleEffect(ParticleBuilder builder) { + super(builder.particle()); + this.count(builder.count()); + this.data(builder.data()); + Location loc; + if ((loc = builder.location()) != null) + this.location(loc); + this.offset(builder.offsetX(), builder.offsetY(), builder.offsetZ()); + this.extra(builder.extra()); + this.force(builder.force()); + this.receivers(builder.receivers()); + this.source(builder.source()); + } + protected ParticleEffect(Particle particle) { super(particle); } @@ -86,36 +99,8 @@ public ParticleEffect spawn() { return (ParticleEffect) super.spawn(); } - public ParticleEffect spawn(Location location, Player @Nullable... receivers) { - this.clone() - .location(location) - .receivers(receivers) - .spawn(); - return this; - } - - public ParticleEffect spawn(Location location, @Nullable List receivers) { - this.clone() - .location(location) - .receivers(receivers) - .spawn(); - return this; - } - - public ParticleEffect spawn(Location location, boolean force, Player @Nullable... receivers) { - this.clone() - .location(location) - .force(force) - .receivers(receivers) - .spawn(); - return this; - } - - public ParticleEffect spawn(Location location, boolean force, @Nullable List receivers) { - this.clone() - .location(location) - .force(force) - .receivers(receivers) + public ParticleEffect spawn(Location location) { + this.location(location) .spawn(); return this; } @@ -179,15 +164,7 @@ public String toString() { @Override public String toString(@Nullable Event event, boolean debug) { - if (dataType() == Void.class) - return ENUM_PARSER.toString(particle(), 0); - for (var particleInfo : DataParticles.getParticleInfos()) { - if (particleInfo.effect() == particle()) { - return particleInfo.toStringFunction().toString(data()); - } - } - // Fallback - return ENUM_PARSER.toString(particle(), 0) + " with data " + Classes.toString(data()); + return ENUM_PARSER.toString(particle(), 0); } private static class ParticleParser extends EnumParser { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java index 0f0fd85aab1..d466e8e1088 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java @@ -1,8 +1,15 @@ package org.skriptlang.skript.bukkit.particles.registration; -import ch.njol.skript.registrations.Classes; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import com.destroystokyo.paper.MaterialTags; import org.bukkit.Axis; import org.bukkit.Effect; +import org.bukkit.Material; +import org.bukkit.event.Event; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Unmodifiable; import java.util.ArrayList; @@ -13,11 +20,11 @@ public class DataGameEffects { private static final List> GAME_EFFECT_INFOS = new ArrayList<>(); @SuppressWarnings("unchecked") - private static void registerEffect(Effect effect, String pattern, ToString toString) { + private static void registerEffect(Effect effect, String pattern, ToString toString) { DataGameEffects.registerEffect(effect, pattern, (event, expressions, parseResult) -> (D) expressions[0].getSingle(event), toString); } - private static void registerEffect(Effect effect, String pattern, DataSupplier dataSupplier, ToString toString) { + private static void registerEffect(Effect effect, String pattern, DataSupplier dataSupplier, ToString toString) { GAME_EFFECT_INFOS.add(new EffectInfo<>(effect, pattern, dataSupplier, toString)); } @@ -28,96 +35,126 @@ private static void registerEffect(Effect effect, String pattern, DataSuppli return Collections.unmodifiableList(GAME_EFFECT_INFOS); } + public static @Nullable String toString(Effect effect, Expression @NotNull [] exprs, ParseResult parseResult, Event event, boolean debug) { + for (EffectInfo info : getGameEffectInfos()) { + if (info.effect() == effect) { + return info.toStringFunction().toString(exprs, parseResult, new SyntaxStringBuilder(event, debug)).toString(); + } + } + return null; + } + private static void registerAll() { registerEffect(Effect.RECORD_PLAY, "[record] song (of|using) %itemtype%", - DataSupplier::getMaterialData, - data -> "record song of " + Classes.toString(data)); - - registerEffect(Effect.SMOKE, "[dispenser] black smoke effect [(in|with|using) direction] %direction%", + // + (event, expressions, parseResult) -> { + Material material = DataSupplier.getMaterialData(event, expressions, parseResult); + if (material == null || !MaterialTags.MUSIC_DISCS.isTagged(material)) + return null; + return material; + }, + // + (exprs, parseResult, builder) -> builder.append("record song of", exprs[0])); + + registerEffect(Effect.SMOKE, "[dispenser] black smoke effect [(in|with|using) [the] direction] %direction%", DataSupplier::getBlockFaceData, - data -> "black smoke effect in direction " + Classes.toString(data)); + (exprs, parseResult, builder) -> builder.append("black smoke effect in direction", exprs[0])); - registerEffect(Effect.SHOOT_WHITE_SMOKE, "[dispenser] white smoke effect [(in|with|using) direction] %direction%", + registerEffect(Effect.SHOOT_WHITE_SMOKE, "[dispenser] white smoke effect [(in|with|using) [the] direction] %direction%", DataSupplier::getBlockFaceData, - data -> "white smoke effect in direction " + Classes.toString(data)); + (exprs, parseResult, builder) -> builder.append("white smoke effect in direction", exprs[0])); - registerEffect(Effect.STEP_SOUND, "[foot]step sound [effect] (on|of|using) %itemtype/blockdata%", - data -> "footstep sound of " + Classes.toString(data)); // handle version changes + registerEffect(Effect.STEP_SOUND, "%itemtype/blockdata% [foot]step[s] sound [effect]", + DataSupplier::getBlockData, + (exprs, parseResult, builder) -> builder.append(exprs[0], "footstep sound")); // handle version changes registerEffect(Effect.POTION_BREAK, "%color% [splash] potion break effect", DataSupplier::getColorData, - data -> Classes.toString(data) + " splash potion break effect"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "splash potion break effect")); registerEffect(Effect.INSTANT_POTION_BREAK, "%color% instant [splash] potion break effect", DataSupplier::getColorData, - data -> Classes.toString(data) + " instant splash potion break effect"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "instant splash potion break effect")); - registerEffect(Effect.COMPOSTER_FILL_ATTEMPT, "[composter] fill[ing] (succe[ss|ed]|1:fail[ure]) sound [effect]", + registerEffect(Effect.COMPOSTER_FILL_ATTEMPT, "compost[er] [fill[ing]] (succe(ss|ed)|1:fail[ure|ed]) sound [effect]", (event, expressions, parseResult) -> parseResult.mark == 0, - data -> (data ? "composter filling success sound effect" : "composter filling failure sound effect")); + (exprs, parseResult, builder) -> builder.append((parseResult.mark == 0 ? "composter filling success sound effect" : "composter filling failure sound effect"))); //noinspection removal registerEffect(Effect.VILLAGER_PLANT_GROW, "villager plant grow[th] effect [(with|using) %-number% particles]", DataSupplier::defaultTo10Particles, - data -> "villager plant growth effect with " + Classes.toString(data) + " particles"); + (exprs, parseResult, builder) -> builder.append("villager plant growth effect") + .appendIf(exprs[0] != null, "with", exprs[0], "particles")); registerEffect(Effect.BONE_MEAL_USE, "[fake] bone meal effect [(with|using) %-number% particles]", DataSupplier::defaultTo10Particles, - data -> "bone meal effect with " + Classes.toString(data) + " particles"); + (exprs, parseResult, builder) -> builder.append("bone meal effect with", exprs[0], "particles")); registerEffect(Effect.ELECTRIC_SPARK, "(electric|lightning[ rod]|copper) spark effect [(in|using|along) the (1:x|2:y|3:z) axis]", (event, expressions, parseResult) -> (parseResult.mark == 0 ? null : Axis.values()[parseResult.mark - 1]), - data -> "electric spark effect along the " + (data == null ? "default" : Classes.toString(data)) + " axis"); + (exprs, parseResult, builder) -> builder.append("electric spark effect") + .appendIf(parseResult.mark != 0, "along the", Axis.values()[parseResult.mark - 1], "axis")); - registerEffect(Effect.PARTICLES_SCULK_CHARGE, "sculk (charge|spread) effect [(with|using) data %number%]", - data -> "sculk charge effect with data " + Classes.toString(data)); // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 + registerEffect(Effect.PARTICLES_SCULK_CHARGE, "sculk (charge|spread) effect [(with|using) data %integer%]", + (exprs, parseResult, builder) -> builder.append("sculk charge effect with data", exprs[0])); // data explanation here https://discord.com/channels/135877399391764480/836220422223036467/1211040434852208660 registerEffect(Effect.PARTICLES_AND_SOUND_BRUSH_BLOCK_COMPLETE, "[finish] brush[ing] %itemtype/blockdata% effect", DataSupplier::getBlockData, - data -> "brushing " + Classes.toString(data) + " effect"); + (exprs, parseResult, builder) -> builder.append("brushing", exprs[0], "effect")); registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER, "trial spawner detect[ing|s] [%-number%] player[s] effect", DataSupplier::defaultTo1Player, - data -> "trial spawner detecting " + Classes.toString(data) + " players effect"); + (exprs, parseResult, builder) -> builder.append("trial spawner detecting") + .appendIf(exprs[0] != null, exprs[0]) + .append("players effect")); + + registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS, "ominous trial spawner detect[ing|s] [%-number%] player[s] effect", + DataSupplier::defaultTo1Player, + (exprs, parseResult, builder) -> builder.append("ominous trial spawner detecting") + .appendIf(exprs[0] != null, exprs[0]) + .append("players effect")); - registerEffect(Effect.TRIAL_SPAWNER_SPAWN, "[:ominous] trial spawner spawn[ing] effect", + registerEffect(Effect.TRIAL_SPAWNER_SPAWN, "[:ominous] trial spawner spawn[ing] [mob] effect", DataSupplier::isOminous, - data -> (data ? "ominous trial spawner spawning effect" : "trial spawner spawning effect")); + (exprs, parseResult, builder) -> builder.appendIf(parseResult.hasTag("ominous"), "ominous") + .append("trial spawner spawning effect")); - registerEffect(Effect.TRIAL_SPAWNER_SPAWN_MOB_AT, "[:ominous] trial spawner spawn[ing] mob effect with sound", + registerEffect(Effect.TRIAL_SPAWNER_SPAWN_MOB_AT, "[:ominous] trial spawner spawn[ing] [mob] effect with sound", DataSupplier::isOminous, - data -> (data ? "ominous trial spawner spawning mob effect with sound" : "trial spawner spawning mob effect with sound")); + (exprs, parseResult, builder) -> builder.append((parseResult.hasTag("ominous") ? "ominous trial spawner spawning mob effect with sound" : "trial spawner spawning mob effect with sound"))); registerEffect(Effect.BEE_GROWTH, "bee growth effect [(with|using) %-number% particles]", DataSupplier::defaultTo10Particles, - data -> "bee growth effect with " + Classes.toString(data) + " particles"); + (exprs, parseResult, builder) -> builder.append("bee [plant] grow[th] effect with", exprs[0], "particles")); registerEffect(Effect.VAULT_ACTIVATE, "[:ominous] [trial] vault activate effect", DataSupplier::isOminous, - data -> (data ? "ominous trial vault activate effect" : "trial vault activate effect")); + (exprs, parseResult, builder) -> builder.appendIf(parseResult.hasTag("ominous"), "ominous") + .append("trial vault activate effect")); registerEffect(Effect.VAULT_DEACTIVATE, "[:ominous] [trial] vault deactivate effect", DataSupplier::isOminous, - data -> (data ? "ominous trial vault deactivate effect" : "trial vault deactivate effect")); - - registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS, "ominous trial spawner detect[ing|s] [%-number%] player[s] effect", - DataSupplier::defaultTo1Player, - data -> "ominous trial spawner detecting " + Classes.toString(data) + " players effect"); + (exprs, parseResult, builder) -> builder.appendIf(parseResult.hasTag("ominous"), "ominous") + .append("trial vault deactivate effect")); registerEffect(Effect.TRIAL_SPAWNER_BECOME_OMINOUS, "trial spawner become[ing] [:not] ominous effect", (event, expressions, parseResult) -> !parseResult.hasTag("not"), - data -> (data ? "trial spawner becoming ominous effect" : "trial spawner becoming not ominous effect")); + (exprs, parseResult, builder) -> builder.append("trial spawner becoming") + .appendIf(parseResult.hasTag("not"), "not") + .append("ominous effect")); registerEffect(Effect.TRIAL_SPAWNER_SPAWN_ITEM, "[:ominous] trial spawner spawn[ing] item effect", DataSupplier::isOminous, - data -> (data ? "ominous trial spawner spawning item effect" : "trial spawner spawning item effect")); + (exprs, parseResult, builder) -> builder.appendIf(parseResult.hasTag("ominous"), "ominous") + .append("trial spawner spawning item effect")); registerEffect(Effect.TURTLE_EGG_PLACEMENT, "place turtle egg effect [(with|using) %-number% particles]", DataSupplier::defaultTo10Particles, - data -> "place turtle egg effect with " + Classes.toString(data) + " particles"); + (exprs, parseResult, builder) -> builder.append("place turtle egg effect with", exprs[0], "particles")); registerEffect(Effect.SMASH_ATTACK, "[mace] smash attack effect [(with|using) %-number% particles]", DataSupplier::defaultTo10Particles, - data -> "smash attack effect with " + Classes.toString(data) + " particles"); + (exprs, parseResult, builder) -> builder.append("smash attack effect with", exprs[0], "particles")); } + } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java index 8a565dcedb7..26339f1ae14 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java @@ -1,17 +1,15 @@ package org.skriptlang.skript.bukkit.particles.registration; +import ch.njol.skript.Skript; import ch.njol.skript.aliases.ItemType; -import ch.njol.skript.registrations.Classes; import ch.njol.skript.util.ColorRGB; import ch.njol.skript.util.Timespan; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Particle; -import org.bukkit.Vibration; +import org.bukkit.*; import org.bukkit.entity.Entity; import org.bukkit.inventory.ItemStack; import org.jetbrains.annotations.Unmodifiable; +import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -20,7 +18,7 @@ public class DataParticles { private static final List> PARTICLE_INFOS = new ArrayList<>(); - private static void registerParticle(Particle particle, String pattern, D defaultData, Function dataFunction, ToString toStringFunction) { + private static void registerParticle(Particle particle, String pattern, D defaultData, Function dataFunction, ToString toStringFunction) { registerParticle(particle, pattern, (event, expressions, parseResult) -> { if (expressions[0] == null) return defaultData; // default data if none is provided @@ -32,7 +30,7 @@ private static void registerParticle(Particle particle, String pattern, D }, toStringFunction); } - private static void registerParticle(Particle particle, String pattern, DataSupplier dataSupplier, ToString toStringFunction) { + private static void registerParticle(Particle particle, String pattern, DataSupplier dataSupplier, ToString toStringFunction) { PARTICLE_INFOS.add(new EffectInfo<>(particle, pattern, dataSupplier, toStringFunction)); } @@ -47,31 +45,41 @@ private static void registerAll() { // colors - registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", - // - (event, expressions, parseResult) -> { - ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); - if (color == null) - color = ColorRGB.fromBukkitColor(org.bukkit.Color.WHITE); // default color if none is provided - Number power = (Number) expressions[1].getSingle(event); - if (power == null) - power = 1.0; // default power if none is provided - return new Particle.Spell(color.asBukkitColor(), power.floatValue()); - }, - // - spell -> Classes.toString(ColorRGB.fromBukkitColor(spell.getColor())) + " effect particle of power " + spell.getPower()); + if (Skript.isRunningMinecraft(1, 21, 9)) { + DataSupplier spellData = // + (event, expressions, parseResult) -> { + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) + color = ColorRGB.fromBukkitColor(org.bukkit.Color.WHITE); // default color if none is provided + Number power = (Number) expressions[1].getSingle(event); + if (power == null) + power = 1.0; // default power if none is provided + return new Particle.Spell(color.asBukkitColor(), power.floatValue()); + }; // + + registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", + spellData, + (exprs, parseResult, builder) -> builder.append(exprs[0], "effect particle of power", exprs[1])); + + registerParticle(Particle.INSTANT_EFFECT, "[a[n]] %color% instant effect particle[s] (of|with) power %number%", + spellData, + (exprs, parseResult, builder) -> builder.append(exprs[0], "instant effect particle of power", exprs[1])); + + registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + (exprs, parseResult, builder) -> builder.append(exprs[0], "flash particle")); - registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, - color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), - color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " potion effect particle"); + } - registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE, + registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), - color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " flash particle"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "potion effect particle")); - registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE, - color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), - color -> Classes.toString(ColorRGB.fromBukkitColor(color)) + " tinted leaves particle"); + if (Skript.isRunningMinecraft(1, 21, 5)) { + registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE, + color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), + (exprs, parseResult, builder) -> builder.append(exprs[0], "tinted leaves particle")); + } registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", // @@ -91,7 +99,7 @@ private static void registerAll() { return new Particle.DustOptions(bukkitColor, size.floatValue()); }, // - dustOptions -> Classes.toString(ColorRGB.fromBukkitColor(dustOptions.getColor())) + " dust particle of size " + dustOptions.getSize()); + (exprs, parseResult, builder) -> builder.append(exprs[0], "dust particle of size", exprs[1])); // dust color transition particle registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", @@ -120,34 +128,30 @@ private static void registerAll() { return new Particle.DustTransition(bukkitColor, bukkitToColor, size.floatValue()); }, // - dustTransition -> Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getColor())) + - " dust particle of size " + dustTransition.getSize() + - " that transitions to " + Classes.toString(ColorRGB.fromBukkitColor(dustTransition.getToColor()))); + (exprs, parseResult, builder) -> builder.append(exprs[0], "dust particle of size", exprs[1], "that transitions to", exprs[2])); // blockdata registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", DataSupplier::getBlockData, - blockData -> Classes.toString(blockData) + " block particle"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "block particle")); - registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", - DataSupplier::getBlockData, - blockData -> Classes.toString(blockData) + " block crumble particle"); + if (Skript.isRunningMinecraft(1, 21, 2)) { + registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", + DataSupplier::getBlockData, + (exprs, parseResult, builder) -> builder.append(exprs[0], "block crumble particle")); + } registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", DataSupplier::getBlockData, - blockData -> Classes.toString(blockData) + " block marker particle"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "block marker particle")); registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", DataSupplier::getBlockData, - blockData -> Classes.toString(blockData) + " dust pillar particle"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "dust pillar particle")); registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", DataSupplier::getBlockData, - blockData -> "falling " + Classes.toString(blockData) + " dust particle"); - - registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", - 0.5f, input -> input, - power -> "dragon breath particle of power " + power); + (exprs, parseResult, builder) -> builder.append("falling", exprs[0], "dust particle")); // misc @@ -159,7 +163,7 @@ private static void registerAll() { return new ItemStack(Material.AIR); // default item if none is provided return itemType.getRandom(); }, // - itemStack -> Classes.toString(itemStack) + " item particle"); + (exprs, parseResult, builder) -> builder.append(exprs[0], "item particle")); registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with [a] roll angle [of] %-number%]", // @@ -171,37 +175,15 @@ private static void registerAll() { return 0.0f; // default angle if none is provided return (float) Math.toRadians(angle.floatValue()); }, // - angle -> "sculk charge particle with roll angle " + Math.toDegrees(angle) + " degrees"); - - registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", - // - (event, expressions, parseResult) -> { - org.bukkit.Color bukkitColor; - ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); - if (color == null) { - bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided - } else { - bukkitColor = color.asBukkitColor(); - } - - Location targetLocation = (Location) expressions[1].getSingle(event); - if (targetLocation == null) - return null; - - Number durationTicks= 20; - if (expressions[2] != null) { - Timespan duration = (Timespan) expressions[2].getSingle(event); - if (duration != null) - durationTicks = duration.getAs(Timespan.TimePeriod.TICK); - } + (exprs, parseResult, builder) -> builder.append("sculk charge particle)") + .appendIf(exprs[0] != null, "with a roll angle of", exprs[0])); - return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); - }, // - trail -> Classes.toString(ColorRGB.fromBukkitColor(trail.getColor())) + - " trail particle leading to " + Classes.toString(trail.getTarget()) + - " over " + trail.getDuration() + " ticks"); + registerParticle(Particle.SHRIEK, "[a] shriek particle[s] [delayed by %-timespan%]", 0, + timespan -> ((Timespan) timespan).getAs(Timespan.TimePeriod.TICK), + (exprs, parseResult, builder) -> builder.append("shriek particle") + .appendIf(exprs[0] != null, "delayed by", exprs[0])); - registerParticle(Particle.VIBRATION, "[a] vibration particle moving to[wards] %entity/location% over [a duration of] %timespan%", + registerParticle(Particle.VIBRATION, "[a] vibration particle moving to[wards] %entity/location% [over [a duration of] %-timespan%]", // (event, expressions, parseResult) -> { Object target = expressions[0].getSingle(event); @@ -223,11 +205,84 @@ private static void registerAll() { } return new Vibration(destination, duration); }, // - vibration -> "vibration particle moving to " + - (vibration.getDestination() instanceof Vibration.Destination.BlockDestination blockDestination ? - Classes.toString(blockDestination.getLocation()) : - Classes.toString(((Vibration.Destination.EntityDestination) vibration.getDestination()).getEntity()) - ) + - " over " + vibration.getArrivalTime() + " ticks"); + (exprs, parseResult, builder) -> builder.append("vibration particle moving towards", exprs[0]) + .appendIf(exprs[1] != null, "over", exprs[1])); + + if (Skript.isRunningMinecraft(1, 21, 4)) { + registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Location targetLocation = (Location) expressions[1].getSingle(event); + if (targetLocation == null) + return null; + + Number durationTicks = 20; + if (expressions[2] != null) { + Timespan duration = (Timespan) expressions[2].getSingle(event); + if (duration != null) + durationTicks = duration.getAs(Timespan.TimePeriod.TICK); + } + + return new Particle.Trail(targetLocation, bukkitColor, durationTicks.intValue()); + }, // + (exprs, parseResult, builder) -> builder.append(exprs[0], "trail particle leading to", exprs[1]) + .appendIf(exprs[2] != null, "over", exprs[2])); + } else if (Skript.isRunningMinecraft(1, 21, 2)) { + // need to get Particle.TargetColor via reflection (1.21.2 - 1.21.3) + // + Class[] classes = Particle.class.getClasses(); + Class targetColorClass = null; + for (Class cls : classes) { + if (cls.getSimpleName().equals("TargetColor")) { + targetColorClass = cls; + break; + } + } + if (targetColorClass != null) { + try { + var constructor = targetColorClass.getDeclaredConstructor(Location.class, Color.class); + registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location%", + // + (event, expressions, parseResult) -> { + org.bukkit.Color bukkitColor; + ch.njol.skript.util.Color color = (ch.njol.skript.util.Color) expressions[0].getSingle(event); + if (color == null) { + bukkitColor = org.bukkit.Color.WHITE; // default color if none is provided + } else { + bukkitColor = color.asBukkitColor(); + } + + Location targetLocation = (Location) expressions[1].getSingle(event); + if (targetLocation == null) + return null; + + try { + return constructor.newInstance(targetLocation, bukkitColor); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + }, // + (exprs, parseResult, builder) -> builder.append(exprs[0], "trail particle moving to", exprs[1])); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + // + } + + if (Skript.isRunningMinecraft(1, 21, 9)) { + registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", + 0.5f, input -> input, + (exprs, parseResult, builder) -> builder.append("dragon breath particle") + .appendIf(exprs[0] != null, "of power", exprs[0])); + } } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java index 13f2017f4e7..221cc2c6811 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java @@ -42,6 +42,13 @@ public interface DataSupplier { return Direction.toNearestBlockFace(direction.getDirection()); } + static @Nullable BlockFace getCartesianBlockFaceData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + Object input = expressions[0].getSingle(event); + if (!(input instanceof Direction direction)) + return null; + return Direction.toNearestCartesianBlockFace(direction.getDirection()); + } + static @Nullable BlockData getBlockData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { Object input = expressions[0].getSingle(event); if (input instanceof ItemType itemType) @@ -63,6 +70,8 @@ static boolean isOminous(Event event, Expression[] expressions, @NotNull Pars } static int defaultTo10Particles(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + if (expressions[0] == null) + return 10; Object input = expressions[0].getSingle(event); if (!(input instanceof Number number)) return 10; @@ -70,6 +79,8 @@ static int defaultTo10Particles(Event event, Expression @NotNull [] expressio } static int defaultTo1Player(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + if (expressions[0] == null) + return 1; Object input = expressions[0].getSingle(event); if (!(input instanceof Number number)) return 1; diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java index 7d0c68fcf35..ecaa4c605de 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/EffectInfo.java @@ -14,5 +14,5 @@ public record EffectInfo( E effect, String pattern, DataSupplier dataSupplier, - ToString toStringFunction + ToString toStringFunction ) { } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java index c8142f599d2..25cc47a49a5 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java @@ -1,12 +1,19 @@ package org.skriptlang.skript.bukkit.particles.registration; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import org.jetbrains.annotations.NotNull; + @FunctionalInterface -public interface ToString { +public interface ToString { /** * Converts the particle and provided data to a string representation. * - * @param data The particle data - * @return The string representation + * @param exprs The expressions used to parse the data + * @param parseResult The parse result from parsing + * @param builder The {@link SyntaxStringBuilder} to append to + * @return The {@link SyntaxStringBuilder} with the string representation appended */ - String toString(D data); + SyntaxStringBuilder toString(Expression @NotNull [] exprs, ParseResult parseResult, SyntaxStringBuilder builder); } diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index 60c8710d7af..d51105339e9 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -2880,11 +2880,10 @@ entity effect: arrow_particles: tipped arrow particles effect rabbit_jump: rabbit jump animation reset_spawner_minecart_delay: reset spawner minecart delay effect - hurt: hurt animation # death was superseded by entity_death 1.12.2 - entity_death: death animation death: death animation + entity_death: death animation egg_break: egg break effect snowball_break: snowball break effect @@ -2893,7 +2892,7 @@ entity effect: hoglin_attack: hoglin attack animation iron_golen_attack: iron golem attack animation ravager_attack: ravager attack animation - ravager_roared: ravager roared animation + ravager_roared: ravager roared effect warden_attack: warden attack animation zoglin_attack: zoglin attack animation entity_attack: entity attack animation @@ -2963,12 +2962,13 @@ entity effect: protected_from_death: protected from death effect # non-functional - guardian_target: guardian target sound effect - thorns_hurt: thorns hurt effect - hurt_explosion: explosion damage effect - hurt_berry_bush: hurt berry bush effect - cat_tame_fail: cat tame fail effect - cat_tame_success: cat tame success effect + guardian_target: guardian target sound effect (non-functional) + thorns_hurt: thorns hurt effect (non-functional) + hurt: damage animation (non-functional) + hurt_explosion: explosion damage effect (non-functional) + hurt_berry_bush: hurt berry bush effect (non-functional) + cat_tame_fail: cat tame fail effect (non-functional) + cat_tame_success: cat tame success effect (non-functional) # -- Particle Effects -- # all patterns should include the word 'particle' @@ -3467,9 +3467,13 @@ types: frogvariant: frog variant¦s @a itemcomponent: item component¦s @an equippablecomponent: equippable component¦s @an + nameable: nameable thing¦s @a gameeffect: game effect¦s @a entityeffect: entity effect¦s @a particle: particle effect¦s @a + convergingparticle: converging particle¦s @a + directionalparticle: directional particle¦s @a + scalableparticle: scalable particle¦s @a # Skript weathertype: weather type¦s @a diff --git a/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk b/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk index ea10cb8b3bd..9f879abf65c 100644 --- a/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk +++ b/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk @@ -49,4 +49,55 @@ test "directional particle effects": draw {_directional_effects::*} at location(0, 0, 0) +test "particle effects with data": + parse if running minecraft "1.21.9": + draw red entity effect particle at test-location + draw red flash particle at test-location + draw dragon breath particle of power 4 at test-location + + parse if running minecraft "1.21.5": + draw red tinted leaves particle at test-location + + parse if running minecraft "1.21.4": + draw a red trail particle moving to test-location ~ vector(1,0,10) over 10 seconds at test-location + parse if running minecraft "1.21.2": + draw dirt crumble particle at test-location + draw a red trail particle moving to test-location ~ vector(1,0,10) at test-location + + draw red dust particle at test-location + draw red dust particle that transitions to white at test-location + draw diamond sword item particle at test-location + draw dirt block particle at test-location + draw dirt marker particle at test-location + draw dirt dust pillar particle at test-location + draw falling dirt dust particle at test-location + draw sculk charge particle with roll angle of 10 degrees at test-location + draw a vibration particle moving to test-location ~ vector(1,0,10) over 5 seconds at test-location + +test "game effects": + draw all game effects at location(0, 0, 0) + +test "game effects with data": + play record song of music disc cat at test-location # broken in paper versions but should still parse and execute without errors + play black smoke effect upwards at test-location + play white smoke effect upwards at test-location + play stone footsteps sound effect at test-location + play red potion break effect at test-location + play red instant potion break effect at test-location + play compost success sound at test-location + play compost fail sound at test-location + play villager plant grow effect at test-location + play fake bone meal effect at test-location + play copper spark effect along the x axis at test-location + play sculk charge effect using data 64 at test-location + play brush stone effect at test-location + play trial spawner detecting players effect at test-location + play ominous trial spawner detecting players effect at test-location + play trial spawner spawning mob effect at test-location + play ominous trial spawner spawning mob effect at test-location + play trial spawner spawning mob effect with sound at test-location + play ominous trial spawner spawning mob effect with sound at test-location + play bee growth effect at test-location + + From c7ffe10412bbbb3a86d07d5ef974ade214c4bae6 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:32:04 -0800 Subject: [PATCH 13/16] JavaDocs and proper module --- src/main/java/ch/njol/skript/Skript.java | 4 +- .../skript/bukkit/particles/GameEffect.java | 34 ++++- .../bukkit/particles/ParticleModule.java | 44 ++++-- .../bukkit/particles/ParticleUtils.java | 59 +++++++- .../expressions/ExprParticleScale.java | 18 ++- .../particleeffects/ConvergingEffect.java | 19 ++- .../particleeffects/DirectionalEffect.java | 34 ++++- .../particleeffects/ParticleEffect.java | 131 +++++++++++++++--- .../particleeffects/ScalableEffect.java | 49 ++++++- .../registration/DataGameEffects.java | 50 +++++-- .../particles/registration/DataParticles.java | 31 ++++- .../particles/registration/DataSupplier.java | 67 ++++++++- .../particles/registration/ToString.java | 5 + 13 files changed, 481 insertions(+), 64 deletions(-) diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index 9deeaa9a353..1e0cdaab360 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -596,12 +596,12 @@ public void onEnable() { TagModule.load(); FurnaceModule.load(); LootTableModule.load(); - ParticleModule.load(); skript.loadModules( new DamageSourceModule(), new ItemComponentModule(), new BrewingModule(), - new CommonModule() + new CommonModule(), + new ParticleModule() ); } catch (final Exception e) { exception(e, "Could not load required .class files: " + e.getLocalizedMessage()); diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java index 050d66ca063..91b5a0af45f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -29,13 +29,21 @@ public class GameEffect { /** * The optional extra data that some {@link Effect}s require. */ - @Nullable - private Object data; + private @Nullable Object data; + /** + * Creates a new GameEffect with the given effect. + * @param effect the effect + */ public GameEffect(Effect effect) { this.effect = effect; } + /** + * Parses a GameEffect from the given input string. Prints errors if the parsed effect requires data. + * @param input the input string + * @return the parsed GameEffect, or null if the input is invalid + */ public static GameEffect parse(String input) { Effect effect = ENUM_UTILS.parse(input.toLowerCase(Locale.ENGLISH), ParseContext.DEFAULT); if (effect == null) @@ -47,15 +55,27 @@ public static GameEffect parse(String input) { return new GameEffect(effect); } + /** + * The backing {@link Effect}. + * @return the effect + */ public Effect getEffect() { return effect; } - @Nullable - public Object getData() { + /** + * The optional data for this effect. + * @return the data, or null if none is set (or not required) + */ + public @Nullable Object getData() { return data; } + /** + * Sets the data for this effect. The data must be of the correct type for the effect. + * @param data the data to set. May only be null for the ELECTRIC_SPARK effect. + * @return true if the data was set correctly, false otherwise + */ public boolean setData(Object data) { if (effect.getData() != null && effect.getData().isInstance(data)) { this.data = data; @@ -104,11 +124,17 @@ public String toString() { return toString(0); } + /** + * A cached array of all effect names that do not require data. + */ static final String[] namesWithoutData = Arrays.stream(Effect.values()) .filter(effect -> effect.getData() == null) .map(Enum::name) .toArray(String[]::new); + /** + * @return an array of all effect names that do not require data. + */ public static String[] getAllNamesWithoutData(){ return namesWithoutData.clone(); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java index bd584145126..3749cb6ca5e 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -11,7 +11,10 @@ import ch.njol.skript.variables.Variables; import ch.njol.yggdrasil.Fields; import ch.njol.yggdrasil.SimpleClassSerializer; +import ch.njol.yggdrasil.SimpleClassSerializer.NonInstantiableClassSerializer; import org.bukkit.*; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.SkriptAddon; import org.skriptlang.skript.bukkit.particles.particleeffects.ConvergingEffect; import org.skriptlang.skript.bukkit.particles.particleeffects.DirectionalEffect; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; @@ -25,19 +28,29 @@ import java.lang.reflect.InvocationTargetException; import java.util.Arrays; -public class ParticleModule { +/** + * Module for particle and game effect related classes and elements. + */ +public class ParticleModule implements AddonModule { - - public static void load () throws IOException { + @Override + public void load(SkriptAddon addon) { registerClasses(); registerDataSerializers(); DataGameEffects.getGameEffectInfos(); DataParticles.getParticleInfos(); // load elements! - Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); + try { + Skript.getAddonInstance().loadClasses("org.skriptlang.skript.bukkit.particles", "elements"); + } catch (IOException e) { + throw new RuntimeException(e); + } } + /** + * Registers particle and game effect related classes. + */ private static void registerClasses() { // gane effects Classes.registerClass(new ClassInfo<>(GameEffect.class, "gameeffect") @@ -205,10 +218,15 @@ public String toVariableNameString(ParticleEffect effect) { .defaultExpression(new EventValueExpression<>(ScalableEffect.class))); } + /** + * Registers data serializers for particle data classes. + * Particles need their data classes to be serializable, but we don't really want classinfos for them since + * they are not meant to be used directly in Skript. {@link SimpleClassSerializer} is perfect for this. + */ private static void registerDataSerializers() { // allow serializing particle data classes Variables.yggdrasil.registerSingleClass(Color.class, "particle.color"); - Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.DustOptions.class, "particle.dustoptions") { + Variables.yggdrasil.registerClassResolver(new NonInstantiableClassSerializer<>(Particle.DustOptions.class, "particle.dustoptions") { @Override public Fields serialize(Particle.DustOptions object) { Fields fields = new Fields(); @@ -227,7 +245,7 @@ protected Particle.DustOptions deserialize(Fields fields) throws StreamCorrupted } }); - Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.DustTransition.class, "particle.dusttransition") { + Variables.yggdrasil.registerClassResolver(new NonInstantiableClassSerializer<>(Particle.DustTransition.class, "particle.dusttransition") { @Override public Fields serialize(Particle.DustTransition object) { Fields fields = new Fields(); @@ -248,7 +266,7 @@ protected Particle.DustTransition deserialize(Fields fields) throws StreamCorrup } }); - Variables.yggdrasil.registerClassResolver( new SimpleClassSerializer.NonInstantiableClassSerializer<>(Vibration.class, "particle.vibration") { + Variables.yggdrasil.registerClassResolver( new NonInstantiableClassSerializer<>(Vibration.class, "particle.vibration") { @Override public Fields serialize(Vibration object) { Fields fields = new Fields(); @@ -268,7 +286,7 @@ protected Vibration deserialize(Fields fields) throws StreamCorruptedException, }); if (Skript.isRunningMinecraft(1, 21, 9)) { - Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Spell.class, "particle.spell") { + Variables.yggdrasil.registerClassResolver(new NonInstantiableClassSerializer<>(Particle.Spell.class, "particle.spell") { @Override public Fields serialize(Particle.Spell object) { Fields fields = new Fields(); @@ -289,7 +307,7 @@ protected Particle.Spell deserialize(Fields fields) throws StreamCorruptedExcept } if (Skript.isRunningMinecraft(1, 21, 4)) { - Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>(Particle.Trail.class, "particle.trail") { + Variables.yggdrasil.registerClassResolver(new NonInstantiableClassSerializer<>(Particle.Trail.class, "particle.trail") { @Override public Fields serialize(Particle.Trail object) { Fields fields = new Fields(); @@ -321,7 +339,7 @@ protected Particle.Trail deserialize(Fields fields) throws StreamCorruptedExcept var getTargetMethod = targetColorClass.getDeclaredMethod("getTarget"); var getColorMethod = targetColorClass.getDeclaredMethod("getColor"); //noinspection unchecked - Variables.yggdrasil.registerClassResolver(new SimpleClassSerializer.NonInstantiableClassSerializer<>((Class) targetColorClass, "particle.targetcolor") { + Variables.yggdrasil.registerClassResolver(new NonInstantiableClassSerializer<>((Class) targetColorClass, "particle.targetcolor") { @Override public Fields serialize(Object object) { Fields fields = new Fields(); @@ -357,8 +375,10 @@ protected Object deserialize(Fields fields) throws StreamCorruptedException, Not } } - - + /** + * Serializer for ParticleEffect. + * Does not store receivers/locations, only the particle effect data. + */ static class ParticleSerializer extends Serializer { @Override public Fields serialize(ParticleEffect effect) { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java index ec52254f238..90db38d6114 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java @@ -11,6 +11,10 @@ import java.util.Collection; import java.util.List; +/** + * Utility class for working with Bukkit Particles and their behaviors. + * Currently used for categorizing particles based on shared characteristics. + */ @SuppressWarnings("UnstableApiUsage") public class ParticleUtils { @@ -100,53 +104,106 @@ public class ParticleUtils { private static Collection risingParticlesCache = null; private static Collection scalableParticlesCache = null; - + /** + * Checks if the given particle is directional, i.e. offset will be treated as a direction/velocity vector if count is 0. + * + * @param particle the particle to check + * @return true if the particle is directional, false otherwise + */ public static boolean isDirectional(@NotNull Particle particle) { if (directionalParticlesCache == null) directionalParticlesCache = DIRECTIONAL_PARTICLES.resolve(Registry.PARTICLE_TYPE); return directionalParticlesCache.contains(particle); } + /** + * Checks if the given particle is converging. These particles spawn away from a point based on offset, then move towards it. + * + * @param particle the particle to check + * @return true if the particle is converging, false otherwise + */ public static boolean isConverging(@NotNull Particle particle) { if (convergingParticlesCache == null) convergingParticlesCache = CONVERGING_PARTICLES.resolve(Registry.PARTICLE_TYPE); return convergingParticlesCache.contains(particle); } + /** + * Checks if the given particle is rising. These particles are directional, but have an overriding upward motion. + * + * @param particle the particle to check + * @return true if the particle is rising, false otherwise + */ public static boolean isRising(@NotNull Particle particle) { if (risingParticlesCache == null) risingParticlesCache = RISING_PARTICLES.resolve(Registry.PARTICLE_TYPE); return risingParticlesCache.contains(particle); } + /** + * Checks if the given particle is scalable, i.e. offset is used to scale the particle size. + * + * @param particle the particle to check + * @return true if the particle is scalable, false otherwise + */ public static boolean isScalable(@NotNull Particle particle) { if (scalableParticlesCache == null) scalableParticlesCache = SCALABLE_PARTICLES.resolve(Registry.PARTICLE_TYPE); return scalableParticlesCache.contains(particle); } + /** + * Checks if the given particle uses velocity, i.e. is either directional or rising. + * + * @param particle the particle to check + * @return true if the particle uses velocity, false otherwise + */ public static boolean usesVelocity(@NotNull Particle particle) { return isDirectional(particle) || isRising(particle); } + /** + * Gets an unmodifiable collection of all directional particles. + * + * @return the collection of directional particles + * @see #isDirectional(Particle) + */ public static @Unmodifiable @NotNull Collection getDirectionalParticles() { if (directionalParticlesCache == null) directionalParticlesCache = DIRECTIONAL_PARTICLES.resolve(Registry.PARTICLE_TYPE); return directionalParticlesCache; } + /** + * Gets an unmodifiable collection of all converging particles. + * + * @return the collection of converging particles + * @see #isConverging(Particle) + */ public static @Unmodifiable @NotNull Collection getConvergingParticles() { if (convergingParticlesCache == null) convergingParticlesCache = CONVERGING_PARTICLES.resolve(Registry.PARTICLE_TYPE); return convergingParticlesCache; } + /** + * Gets an unmodifiable collection of all rising particles. + * + * @return the collection of rising particles + * @see #isRising(Particle) + */ public static @Unmodifiable @NotNull Collection getRisingParticles() { if (risingParticlesCache == null) risingParticlesCache = RISING_PARTICLES.resolve(Registry.PARTICLE_TYPE); return risingParticlesCache; } + /** + * Gets an unmodifiable collection of all scalable particles. + * + * @return the collection of scalable particles + * @see #isScalable(Particle) + */ public static @Unmodifiable @NotNull Collection getScalableParticles() { if (scalableParticlesCache == null) scalableParticlesCache = SCALABLE_PARTICLES.resolve(Registry.PARTICLE_TYPE); diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java index 3fa63545b4d..cd438051c73 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java @@ -14,7 +14,9 @@ public class ExprParticleScale extends SimplePropertyExpression new") public static @NotNull ParticleEffect of(Particle particle) { if (ParticleUtils.isConverging(particle)) { @@ -49,11 +54,42 @@ public class ParticleEffect extends ParticleBuilder implements Debuggable { return new ParticleEffect(particle); } + /** + * Creates the appropriate ParticleEffect with the properties of the provided {@link ParticleBuilder} + * @param builder The builder to copy values from + * @return The appropriate ParticleEffect instance with the properties copied from the builder + */ + @Contract("_ -> new") + public static @NotNull ParticleEffect of(@NotNull ParticleBuilder builder) { + Particle particle = builder.particle(); + ParticleEffect effect = ParticleEffect.of(particle); + effect.count(builder.count()); + effect.data(builder.data()); + Location loc; + if ((loc = builder.location()) != null) + effect.location(loc); + effect.offset(builder.offsetX(), builder.offsetY(), builder.offsetZ()); + effect.extra(builder.extra()); + effect.force(builder.force()); + effect.receivers(builder.receivers()); + effect.source(builder.source()); + return effect; + } + // Skript parsing dependencies + /** + * Parser for particles without data + */ private static final ParticleParser ENUM_PARSER = new ParticleParser(); - public static ParticleEffect parse(String input, ParseContext context) { + /** + * Parses a particle effect from a string input. Prints errors if the particle requires data. + * @param input the input string + * @param context the parse context + * @return the parsed ParticleEffect, or null if parsing failed + */ + public static @Nullable ParticleEffect parse(String input, ParseContext context) { Particle particle = ENUM_PARSER.parse(input.toLowerCase(Locale.ENGLISH), context); if (particle == null) return null; @@ -64,30 +100,31 @@ public static ParticleEffect parse(String input, ParseContext context) { return ParticleEffect.of(particle); } + /** + * Converts a Particle to its string representation. + * @param particle the particle + * @param flags parsing flags + * @return the string representation + */ public static String toString(Particle particle, int flags) { return ENUM_PARSER.toString(particle, flags); } - public static String[] getAllNamesWithoutData() { + /** + * Gets all particle names that do not require data. + * @return array of particle names + */ + public static String @NotNull [] getAllNamesWithoutData() { return ENUM_PARSER.getPatternsWithoutData(); } // Instance code - public ParticleEffect(ParticleBuilder builder) { - super(builder.particle()); - this.count(builder.count()); - this.data(builder.data()); - Location loc; - if ((loc = builder.location()) != null) - this.location(loc); - this.offset(builder.offsetX(), builder.offsetY(), builder.offsetZ()); - this.extra(builder.extra()); - this.force(builder.force()); - this.receivers(builder.receivers()); - this.source(builder.source()); - } - + /** + * Internal constructor. + * Use {@link ParticleEffect#of(Particle)} instead. + * @param particle The particle type + */ protected ParticleEffect(Particle particle) { super(particle); } @@ -99,36 +136,75 @@ public ParticleEffect spawn() { return (ParticleEffect) super.spawn(); } + /** + * Ease of use method to spawn at a location. Modifies the location value of this effect. + * @param location the location to spawn at. + * @return This effect, with the location value modified. + */ public ParticleEffect spawn(Location location) { this.location(location) .spawn(); return this; } + /** + * @return The offset of this particle as a JOML vector + */ public Vector3d offset() { return new Vector3d(offsetX(), offsetY(), offsetZ()); } - public ParticleEffect offset(Vector3d offset) { + /** + * Set the offset from a JOML vector + * @param offset the new offset + * @return This effect, with the offset modified. + */ + public ParticleEffect offset(@NotNull Vector3d offset) { return (ParticleEffect) super.offset(offset.x(), offset.y(), offset.z()); } - public ParticleEffect receivers(Vector3i radii) { + /** + * Set the receiver radii from a JOML vector + * @param radii the new radii to check for receivers in + * @return This effect, with the receivers modified. + */ + public ParticleEffect receivers(@NotNull Vector3i radii) { return (ParticleEffect) super.receivers(radii.x(), radii.y(), radii.z()); } - public ParticleEffect receivers(Vector3d radii) { + /** + * Set the receiver radii from a JOML vector. Values are truncated to ints. + * @param radii the new radii to check for receivers in + * @return This effect, with the receivers modified. + */ + public ParticleEffect receivers(@NotNull Vector3d radii) { return (ParticleEffect) super.receivers((int) radii.x(), (int) radii.y(), (int) radii.z()); } + /** + * @return Whether this effect will use its offset value as a normal distribution (count > 0) + */ public boolean isUsingNormalDistribution() { return count() != 0; } + /** + * An alias for the offset. Prefer using this when working with particles that have counts greater than 0. + * When {@link #isUsingNormalDistribution()} is false, the returned value will not be the distribution and + * will instead depend on the particle's specific behavior when count = 0. + * @return the distribution of this particle. The distribution is defined as 3 normal distributions in the x/y/z axes, + * with the returned vector containing the standard deviations. The mean will always be 0. + */ public Vector3d getDistribution() { - return isUsingNormalDistribution() ? offset() : null; + return offset(); } + /** + * Sets the distribution for this particle. The distribution is defined as 3 normal distributions in the x/y/z axes, + * with the provided vector containing the standard deviations. The mean will always be 0. + * Sets the count to 1 if it was 0. + * @param distribution The new standard deviations to use. + */ public void setDistribution(Vector3d distribution) { if (!isUsingNormalDistribution()) { count(1); @@ -144,15 +220,27 @@ public ParticleEffect data(@Nullable T data) { return (ParticleEffect) super.data(data); } + /** + * Helper method to check if this effect accepts the provided data. Depends on the current particle. + * @param data The data to check. + * @return Whether the data is of the right class. + */ public boolean acceptsData(@Nullable Object data) { if (data == null) return true; return dataType().isInstance(data); } + /** + * Alias for {@code this.particle().getDataType()} + * @return The data type of the current particle. + */ public Class dataType() { return particle().getDataType(); } + /** + * @return a copy of this effect. + */ public ParticleEffect copy() { return (ParticleEffect) this.clone(); } @@ -167,6 +255,9 @@ public String toString(@Nullable Event event, boolean debug) { return ENUM_PARSER.toString(particle(), 0); } + /** + * A custom {@link EnumParser} that excludes particles with data from being parsed directly. + */ private static class ParticleParser extends EnumParser { public ParticleParser() { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java index cec4abb409f..1ac2d0e2b57 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java @@ -1,18 +1,22 @@ package org.skriptlang.skript.bukkit.particles.particleeffects; -import com.destroystokyo.paper.ParticleBuilder; import com.google.common.base.Function; import org.bukkit.Particle; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +/** + * A particle effect that can be scaled. + * Currently used for `sweep attack` and `explosion` particles. + */ public class ScalableEffect extends ParticleEffect { private double scale; private final ScalingFunction scalingFunction; - // Sweep: scale = 1.0 - offsetX * 0.5 -> offsetX = (1.0 - scale) * 2.0 - // Explosion: scale = 2.0 * (1.0 - offsetX * 0.5) -> offsetX = (2.0 - scale) + /** + * Enum representing different scaling functions for scalable particles. + */ private enum ScalingFunction { SWEEP(scale -> 2 - (2 * scale)), EXPLOSION(scale -> 2 - scale); @@ -28,6 +32,13 @@ public double apply(double scale) { } } + /** + * Gets the appropriate scaling function for the given particle. + * + * @param particle The particle type + * @return The scaling function + * @throws IllegalArgumentException if the particle is not scalable + */ private ScalingFunction getScalingFunction(@NotNull Particle particle) { return switch (particle) { case SWEEP_ATTACK -> ScalingFunction.SWEEP; @@ -36,6 +47,11 @@ private ScalingFunction getScalingFunction(@NotNull Particle particle) { }; } + /** + * Internal constructor. + * Use {@link ParticleEffect#of(Particle)} instead. + * @param particle The particle type + */ @ApiStatus.Internal public ScalableEffect(Particle particle) { super(particle); @@ -43,10 +59,21 @@ public ScalableEffect(Particle particle) { this.scalingFunction = getScalingFunction(particle); } + /** + * Checks if the effect will use the offset as scale. + * The scale is only applied if the count is 0. + * @return true if the effect will use the scale, false otherwise + */ public boolean hasScale() { return this.count() == 0; } + /** + * Sets the scale of the particles by setting the offset and count. + * This will set the count to 0 to ensure the scale is applied. + * @param scale the scale value + * @return this effect for chaining + */ public ParticleEffect scale(double scale) { this.scale = scale; count(0); @@ -54,14 +81,22 @@ public ParticleEffect scale(double scale) { return this; } + /** + * Gets the current scale of the particles. + * @return the scale value + */ public double scale() { return scale; } + /** + * @return a copy of this scalable effect + */ @Override - public ParticleBuilder clone() { - ScalableEffect clone = (ScalableEffect) super.clone(); - clone.scale = this.scale; - return clone; + public ScalableEffect copy() { + ScalableEffect copy = (ScalableEffect) super.copy(); + copy.scale = this.scale; + return copy; } + } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java index d466e8e1088..364dedd074d 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java @@ -16,18 +16,39 @@ import java.util.Collections; import java.util.List; +/** + * Registry and utility class for game effects that require data. + */ public class DataGameEffects { private static final List> GAME_EFFECT_INFOS = new ArrayList<>(); + /** + * Registers a new effect with a single data parameter. + * @param effect the effect + * @param pattern the pattern to use (must contain exactly one non-null expression) + * @param toString the toString function for this pattern + * @param the data type + */ @SuppressWarnings("unchecked") private static void registerEffect(Effect effect, String pattern, ToString toString) { DataGameEffects.registerEffect(effect, pattern, (event, expressions, parseResult) -> (D) expressions[0].getSingle(event), toString); } + /** + * Registers a new effect with a custom data supplier. + * @param effect the effect + * @param pattern the pattern to use + * @param dataSupplier the data supplier for this effect + * @param toString the toString function for this pattern + * @param the data type + */ private static void registerEffect(Effect effect, String pattern, DataSupplier dataSupplier, ToString toString) { GAME_EFFECT_INFOS.add(new EffectInfo<>(effect, pattern, dataSupplier, toString)); } + /** + * @return An unmodifiable list of all registered game effect infos. + */ public static @Unmodifiable List> getGameEffectInfos() { if (GAME_EFFECT_INFOS.isEmpty()) { registerAll(); @@ -35,6 +56,15 @@ private static void registerEffect(Effect effect, String pattern, DataSuppli return Collections.unmodifiableList(GAME_EFFECT_INFOS); } + /** + * Looks up and calls the toString function for the given effect, using the provided context. + * @param effect the effect + * @param exprs the expressions to use in the toString function + * @param parseResult the parse result + * @param event the event + * @param debug whether to include debug information + * @return the string representation, or null if no matching effect was found + */ public static @Nullable String toString(Effect effect, Expression @NotNull [] exprs, ParseResult parseResult, Event event, boolean debug) { for (EffectInfo info : getGameEffectInfos()) { if (info.effect() == effect) { @@ -44,6 +74,10 @@ private static void registerEffect(Effect effect, String pattern, DataSuppli return null; } + /** + * Registers all game effects that require data. + * Game effects without data are automatically handled by the enum parser. + */ private static void registerAll() { registerEffect(Effect.RECORD_PLAY, "[record] song (of|using) %itemtype%", // @@ -61,7 +95,7 @@ private static void registerAll() { (exprs, parseResult, builder) -> builder.append("black smoke effect in direction", exprs[0])); registerEffect(Effect.SHOOT_WHITE_SMOKE, "[dispenser] white smoke effect [(in|with|using) [the] direction] %direction%", - DataSupplier::getBlockFaceData, + DataSupplier::getCartesianBlockFaceData, (exprs, parseResult, builder) -> builder.append("white smoke effect in direction", exprs[0])); registerEffect(Effect.STEP_SOUND, "%itemtype/blockdata% [foot]step[s] sound [effect]", @@ -82,12 +116,12 @@ private static void registerAll() { //noinspection removal registerEffect(Effect.VILLAGER_PLANT_GROW, "villager plant grow[th] effect [(with|using) %-number% particles]", - DataSupplier::defaultTo10Particles, + DataSupplier::getNumberDefault10, (exprs, parseResult, builder) -> builder.append("villager plant growth effect") .appendIf(exprs[0] != null, "with", exprs[0], "particles")); registerEffect(Effect.BONE_MEAL_USE, "[fake] bone meal effect [(with|using) %-number% particles]", - DataSupplier::defaultTo10Particles, + DataSupplier::getNumberDefault10, (exprs, parseResult, builder) -> builder.append("bone meal effect with", exprs[0], "particles")); registerEffect(Effect.ELECTRIC_SPARK, "(electric|lightning[ rod]|copper) spark effect [(in|using|along) the (1:x|2:y|3:z) axis]", @@ -103,13 +137,13 @@ private static void registerAll() { (exprs, parseResult, builder) -> builder.append("brushing", exprs[0], "effect")); registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER, "trial spawner detect[ing|s] [%-number%] player[s] effect", - DataSupplier::defaultTo1Player, + DataSupplier::getNumberDefault1, (exprs, parseResult, builder) -> builder.append("trial spawner detecting") .appendIf(exprs[0] != null, exprs[0]) .append("players effect")); registerEffect(Effect.TRIAL_SPAWNER_DETECT_PLAYER_OMINOUS, "ominous trial spawner detect[ing|s] [%-number%] player[s] effect", - DataSupplier::defaultTo1Player, + DataSupplier::getNumberDefault1, (exprs, parseResult, builder) -> builder.append("ominous trial spawner detecting") .appendIf(exprs[0] != null, exprs[0]) .append("players effect")); @@ -124,7 +158,7 @@ private static void registerAll() { (exprs, parseResult, builder) -> builder.append((parseResult.hasTag("ominous") ? "ominous trial spawner spawning mob effect with sound" : "trial spawner spawning mob effect with sound"))); registerEffect(Effect.BEE_GROWTH, "bee growth effect [(with|using) %-number% particles]", - DataSupplier::defaultTo10Particles, + DataSupplier::getNumberDefault10, (exprs, parseResult, builder) -> builder.append("bee [plant] grow[th] effect with", exprs[0], "particles")); registerEffect(Effect.VAULT_ACTIVATE, "[:ominous] [trial] vault activate effect", @@ -149,11 +183,11 @@ private static void registerAll() { .append("trial spawner spawning item effect")); registerEffect(Effect.TURTLE_EGG_PLACEMENT, "place turtle egg effect [(with|using) %-number% particles]", - DataSupplier::defaultTo10Particles, + DataSupplier::getNumberDefault10, (exprs, parseResult, builder) -> builder.append("place turtle egg effect with", exprs[0], "particles")); registerEffect(Effect.SMASH_ATTACK, "[mace] smash attack effect [(with|using) %-number% particles]", - DataSupplier::defaultTo10Particles, + DataSupplier::getNumberDefault10, (exprs, parseResult, builder) -> builder.append("smash attack effect with", exprs[0], "particles")); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java index 26339f1ae14..7ae93010cf4 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java @@ -7,6 +7,7 @@ import org.bukkit.*; import org.bukkit.entity.Entity; import org.bukkit.inventory.ItemStack; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Unmodifiable; import java.lang.reflect.InvocationTargetException; @@ -15,9 +16,23 @@ import java.util.List; import java.util.function.Function; +/** + * Registry and utility class for particles that require data. + */ public class DataParticles { + private static final List> PARTICLE_INFOS = new ArrayList<>(); + /** + * Registers a new particle with a single data parameter, and a default value for when none is provided. + * @param particle the particle + * @param pattern the pattern to use (must contain exactly one expression, which may be nullable) + * @param defaultData the default data to use if the expression is null or evaluates to null + * @param dataFunction the function to convert the expression value to the data type (use input->input if they are the same) + * @param toStringFunction the toString function for this pattern + * @param the expression type + * @param the data type + */ private static void registerParticle(Particle particle, String pattern, D defaultData, Function dataFunction, ToString toStringFunction) { registerParticle(particle, pattern, (event, expressions, parseResult) -> { if (expressions[0] == null) @@ -30,17 +45,31 @@ private static void registerParticle(Particle particle, String pattern, D }, toStringFunction); } + /** + * Registers a new particle with a custom data supplier. + * @param particle the particle + * @param pattern the pattern to use + * @param dataSupplier the data supplier for this particle + * @param toStringFunction the toString function for this pattern + * @param the data type + */ private static void registerParticle(Particle particle, String pattern, DataSupplier dataSupplier, ToString toStringFunction) { PARTICLE_INFOS.add(new EffectInfo<>(particle, pattern, dataSupplier, toStringFunction)); } - public static @Unmodifiable List> getParticleInfos() { + /** + * @return An unmodifiable list of all registered particle infos. + */ + public static @Unmodifiable @NotNull List> getParticleInfos() { if (PARTICLE_INFOS.isEmpty()) { registerAll(); } return Collections.unmodifiableList(PARTICLE_INFOS); } + /** + * Registers all particles with data. + */ private static void registerAll() { // colors diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java index 221cc2c6811..450381db9f8 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java @@ -12,6 +12,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +/** + * A functional interface for supplying data to effects from parsed expressions. + * Effectively an {@link Expression} in disguise. Accepts the parsed context of expressions and parse result, + * spits out the required data value. + * @param the data type to supply + */ @FunctionalInterface public interface DataSupplier { /** @@ -28,6 +34,13 @@ public interface DataSupplier { // Helper functions for common data types // + /** + * Gets material data from an ItemType expression. + * @param event the event + * @param expressions Expected to contain a single ItemType expression + * @param parseResult the parse result (unused) + * @return the material, or null if the input was not an ItemType + */ static @Nullable Material getMaterialData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { Object input = expressions[0].getSingle(event); if (!(input instanceof ItemType itemType)) @@ -35,6 +48,13 @@ public interface DataSupplier { return itemType.getMaterial(); } + /** + * Gets block face data from a Direction expression. + * @param event the event + * @param expressions Expected to contain a single Direction expression + * @param parseResult the parse result (unused) + * @return the block face, or null if the input was not a Direction + */ static @Nullable BlockFace getBlockFaceData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { Object input = expressions[0].getSingle(event); if (!(input instanceof Direction direction)) @@ -42,6 +62,14 @@ public interface DataSupplier { return Direction.toNearestBlockFace(direction.getDirection()); } + /** + * Gets cartesian block face data from a Direction expression. The white smoke effect only allows + * the six cardinal directions, so this function maps any direction to the nearest of those. + * @param event the event + * @param expressions Expected to contain a single Direction expression + * @param parseResult the parse result (unused) + * @return the cartesian block face, or null if the input was not a Direction + */ static @Nullable BlockFace getCartesianBlockFaceData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { Object input = expressions[0].getSingle(event); if (!(input instanceof Direction direction)) @@ -49,6 +77,13 @@ public interface DataSupplier { return Direction.toNearestCartesianBlockFace(direction.getDirection()); } + /** + * Gets block data from an ItemType or BlockData expression. + * @param event the event + * @param expressions Expected to contain a single ItemType or BlockData expression + * @param parseResult the parse result (unused) + * @return the block data, or null if the input was not an ItemType or BlockData + */ static @Nullable BlockData getBlockData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { Object input = expressions[0].getSingle(event); if (input instanceof ItemType itemType) @@ -58,6 +93,13 @@ public interface DataSupplier { return null; } + /** + * Gets color data from a skript Color expression. + * @param event the event + * @param expressions Expected to contain a single Color expression + * @param parseResult the parse result (unused) + * @return the color, or null if the input was not a Color + */ static @Nullable Color getColorData(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { Object input = expressions[0].getSingle(event); if (!(input instanceof ch.njol.skript.util.Color color)) @@ -65,11 +107,25 @@ public interface DataSupplier { return color.asBukkitColor(); } + /** + * Checks if the "ominous" tag was present in the parse result. + * @param event the event (unused) + * @param expressions the expressions (unused) + * @param parseResult the parse result + * @return true if the "ominous" tag was present, false otherwise + */ static boolean isOminous(Event event, Expression[] expressions, @NotNull ParseResult parseResult) { return parseResult.hasTag("ominous"); } - static int defaultTo10Particles(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + /** + * Gets a number from the first expression, defaulting to 10 if not present or invalid. + * @param event the event + * @param expressions Expected to contain a single Number expression, may be nullable + * @param parseResult the parse result (unused) + * @return the number, or 10 if not present or invalid + */ + static int getNumberDefault10(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { if (expressions[0] == null) return 10; Object input = expressions[0].getSingle(event); @@ -78,7 +134,14 @@ static int defaultTo10Particles(Event event, Expression @NotNull [] expressio return number.intValue(); } - static int defaultTo1Player(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { + /** + * Gets a number from the first expression, defaulting to 1 if not present or invalid. + * @param event the event + * @param expressions Expected to contain a single Number expression, may be nullable + * @param parseResult the parse result (unused) + * @return the number, or 1 if not present or invalid + */ + static int getNumberDefault1(Event event, Expression @NotNull [] expressions, ParseResult parseResult) { if (expressions[0] == null) return 1; Object input = expressions[0].getSingle(event); diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java index 25cc47a49a5..e5de2f6a573 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java @@ -3,8 +3,13 @@ import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.SkriptParser.ParseResult; import ch.njol.skript.lang.SyntaxStringBuilder; +import org.bukkit.event.Event; import org.jetbrains.annotations.NotNull; +/** + * A functional interface for converting a particle and its data to a string representation. + * Effectively a custom {@link ch.njol.skript.lang.Debuggable#toString(Event, boolean)} method. + */ @FunctionalInterface public interface ToString { /** From e6489716512e72b9f57984e68e7d572912d152ae Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Sun, 30 Nov 2025 23:36:35 -0800 Subject: [PATCH 14/16] Update CODEOWNERS --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4ca29409aec..cbbbdff4c40 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -70,3 +70,6 @@ # EntityData /src/main/java/ch/njol/skript/entity @Absolutionism @skriptlang/core-developers + +# Particles/Effects +/src/main/java/org/skriptlang/skript/bukkit/particles @sovdeeth @skriptlang/core-developers From 7de62eb06484cc5ec5237953a8e58657830dfd65 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:40:17 -0800 Subject: [PATCH 15/16] Final syntaxes, polish, docs, and visual effect removal --- .../ch/njol/skript/classes/EnumParser.java | 10 +- .../skript/classes/data/SkriptClasses.java | 29 - .../njol/skript/effects/EffVisualEffect.java | 129 --- .../skript/util/visual/ParticleOption.java | 36 - .../njol/skript/util/visual/VisualEffect.java | 169 ---- .../skript/util/visual/VisualEffectType.java | 111 --- .../skript/util/visual/VisualEffects.java | 268 ------ .../elements/effects/EffPlayEffect.java | 23 +- .../expressions/ExprGameEffectWithData.java | 15 + .../expressions/ExprParticleCount.java | 18 + .../expressions/ExprParticleDistribution.java | 32 +- .../expressions/ExprParticleOffset.java | 16 + .../expressions/ExprParticleScale.java | 17 + .../expressions/ExprParticleSpeed.java | 20 +- .../expressions/ExprParticleWithData.java | 36 +- .../expressions/ExprParticleWithOffset.java | 109 +++ .../expressions/ExprParticleWithSpeed.java | 75 ++ .../particleeffects/ParticleEffect.java | 18 +- .../particles/registration/DataParticles.java | 42 +- src/main/resources/lang/default.lang | 863 +++--------------- 20 files changed, 510 insertions(+), 1526 deletions(-) delete mode 100644 src/main/java/ch/njol/skript/effects/EffVisualEffect.java delete mode 100644 src/main/java/ch/njol/skript/util/visual/ParticleOption.java delete mode 100644 src/main/java/ch/njol/skript/util/visual/VisualEffect.java delete mode 100644 src/main/java/ch/njol/skript/util/visual/VisualEffectType.java delete mode 100644 src/main/java/ch/njol/skript/util/visual/VisualEffects.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithOffset.java create mode 100644 src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithSpeed.java diff --git a/src/main/java/ch/njol/skript/classes/EnumParser.java b/src/main/java/ch/njol/skript/classes/EnumParser.java index e286afe1cc0..2e7d2bc7bde 100644 --- a/src/main/java/ch/njol/skript/classes/EnumParser.java +++ b/src/main/java/ch/njol/skript/classes/EnumParser.java @@ -65,11 +65,17 @@ void refresh() { String first = strippedOption.getFirst(); Integer second = strippedOption.getSecond(); + NonNullPair singlePlural = Noun.getPlural(first); + String single = singlePlural.getFirst(); + String plural = singlePlural.getSecond(); + if (names[ordinal] == null) { // Add to name array if needed - names[ordinal] = first; + names[ordinal] = single; } - parseMap.put(first, constant); + parseMap.put(single, constant); + if (!plural.isEmpty()) + parseMap.put(plural, constant); if (second != -1) { // There is a gender present parseMap.put(Noun.getArticleWithSpace(second, Language.F_INDEFINITE_ARTICLE) + first, constant); } diff --git a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java index 578d0a374a4..99bd576b652 100644 --- a/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java +++ b/src/main/java/ch/njol/skript/classes/data/SkriptClasses.java @@ -473,35 +473,6 @@ public String toVariableNameString(final Experience xp) { }) .serializer(new YggdrasilSerializer<>())); -// Classes.registerClass(new ClassInfo<>(VisualEffect.class, "visualeffect") -// .name("Visual Effect") -// .description("A visible effect, e.g. particles.") -// .examples("show wolf hearts on the clicked wolf", -// "play mob spawner flames at the targeted block to the player") -// .usage(VisualEffects.getAllNames()) -// .since("2.1") -// .user("(visual|particle) effects?") -// .after("itemtype") -// .parser(new Parser() { -// @Override -// @Nullable -// public VisualEffect parse(String s, ParseContext context) { -// return VisualEffects.parse(s); -// } -// -// @Override -// public String toString(VisualEffect e, int flags) { -// return e.toString(flags); -// } -// -// @Override -// public String toVariableNameString(VisualEffect e) { -// return e.toString(); -// } -// -// }) -// .serializer(new YggdrasilSerializer<>())); - Classes.registerClass(new ClassInfo<>(GameruleValue.class, "gamerulevalue") .user("gamerule values?") .name("Gamerule Value") diff --git a/src/main/java/ch/njol/skript/effects/EffVisualEffect.java b/src/main/java/ch/njol/skript/effects/EffVisualEffect.java deleted file mode 100644 index f62b0fb3365..00000000000 --- a/src/main/java/ch/njol/skript/effects/EffVisualEffect.java +++ /dev/null @@ -1,129 +0,0 @@ -package ch.njol.skript.effects; - -import org.bukkit.Location; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.bukkit.event.Event; -import org.jetbrains.annotations.Nullable; - -import ch.njol.skript.Skript; -import ch.njol.skript.doc.Description; -import ch.njol.skript.doc.Examples; -import ch.njol.skript.doc.Name; -import ch.njol.skript.doc.Since; -import ch.njol.skript.lang.Effect; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.Literal; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.util.Direction; -import ch.njol.skript.util.visual.VisualEffect; -import ch.njol.util.Kleenean; - -@Name("Play Effect") -@Description({"Plays a visual effect at a given location or on a given entity.", - "Please note that some effects can only be played on entities, e.g. wolf hearts or the hurt effect, and that these are always visible to all players."}) -@Examples({"play wolf hearts on the clicked wolf", - "show mob spawner flames at the targeted block to the player"}) -@Since("2.1") -public class EffVisualEffect extends Effect { - - static { -// Skript.registerEffect(EffVisualEffect.class, -// "(play|show) %visualeffects% (on|%directions%) %entities/locations% [(to %-players%|in (radius|range) of %-number%)]", -// "(play|show) %number% %visualeffects% (on|%directions%) %entities/locations% [(to %-players%|in (radius|range) of %-number%)]"); - } - - @SuppressWarnings("NotNullFieldNotInitialized") - private Expression effects; - @SuppressWarnings("NotNullFieldNotInitialized") - private Expression direction; - @SuppressWarnings("NotNullFieldNotInitialized") - private Expression where; - - @Nullable - private Expression players; - @Nullable - private Expression radius; - @Nullable - private Expression count; - - @SuppressWarnings({"unchecked", "null"}) - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - int base = 0; - if (matchedPattern == 1) { - count = (Expression) exprs[0]; - base = 1; - } - - effects = (Expression) exprs[base]; - direction = (Expression) exprs[base + 1]; - where = exprs[base + 2]; - players = (Expression) exprs[base + 3]; - radius = (Expression) exprs[base + 4]; - - if (effects instanceof Literal) { - //noinspection ConstantConditions - VisualEffect[] effs = effects.getArray(null); - - boolean hasLocationEffect = false; - boolean hasEntityEffect = false; - for (VisualEffect e : effs) { - if (e.getType().isEntityEffect()) - hasEntityEffect = true; - else - hasLocationEffect = true; - } - - if (!hasLocationEffect && players != null) - Skript.warning("Entity effects are visible to all players"); - if (!hasLocationEffect && !direction.isDefault()) - Skript.warning("Entity effects are always played on an entity"); - if (hasEntityEffect && !where.canReturn(Entity.class)) { - Skript.error("Entity effects can only be played on entities"); - return false; - } - } - - return true; - } - - @Override - protected void execute(Event e) { - VisualEffect[] effects = this.effects.getArray(e); - Direction[] directions = direction.getArray(e); - Object[] os = where.getArray(e); - Player[] ps = players != null ? players.getArray(e) : null; - Number rad = radius != null ? radius.getSingle(e) : 32; // 32=default particle radius - Number cnt = count != null ? count.getSingle(e) : 0; - - // noinspection ConstantConditions - if (effects == null || directions == null || os == null || rad == null || cnt == null) - return; - - for (Direction d : directions) { - for (Object o : os) { - if (o instanceof Entity) { - for (VisualEffect eff : effects) { - eff.play(ps, d.getRelative((Entity) o), (Entity) o, cnt.intValue(), rad.intValue()); - } - } else if (o instanceof Location) { - for (VisualEffect eff : effects) { - if (eff.getType().isEntityEffect()) - continue; - eff.play(ps, d.getRelative((Location) o), null, cnt.intValue(), rad.intValue()); - } - } else { - assert false; - } - } - } - } - - @Override - public String toString(@Nullable Event e, boolean debug) { - return "play " + effects.toString(e, debug) + " " + direction.toString(e, debug) + " " - + where.toString(e, debug) + (players != null ? " to " + players.toString(e, debug) : ""); - } - -} diff --git a/src/main/java/ch/njol/skript/util/visual/ParticleOption.java b/src/main/java/ch/njol/skript/util/visual/ParticleOption.java deleted file mode 100644 index 0fae7e7b214..00000000000 --- a/src/main/java/ch/njol/skript/util/visual/ParticleOption.java +++ /dev/null @@ -1,36 +0,0 @@ -package ch.njol.skript.util.visual; - -import ch.njol.skript.util.Color; - -public class ParticleOption { - - org.bukkit.Color color; - float size; - - public ParticleOption(Color color, float size) { - this.color = color.asBukkitColor(); - this.size = size; - } - - public org.bukkit.Color getBukkitColor() { - return color; - } - - public float getRed() { - return (float) color.getRed() / 255.0f; - } - - public float getGreen() { - return (float) color.getGreen() / 255.0f; - } - - public float getBlue() { - return (float) color.getBlue() / 255.0f; - } - - @Override - public String toString() { - return "ParticleOption{color=" + color + ", size=" + size + "}"; - } - -} diff --git a/src/main/java/ch/njol/skript/util/visual/VisualEffect.java b/src/main/java/ch/njol/skript/util/visual/VisualEffect.java deleted file mode 100644 index ebff4d02fe6..00000000000 --- a/src/main/java/ch/njol/skript/util/visual/VisualEffect.java +++ /dev/null @@ -1,169 +0,0 @@ -package ch.njol.skript.util.visual; - -import ch.njol.skript.Skript; -import ch.njol.skript.lang.Expression; -import ch.njol.skript.lang.SkriptParser.ParseResult; -import ch.njol.skript.lang.SyntaxElement; -import ch.njol.skript.lang.util.ContextlessEvent; -import ch.njol.util.Kleenean; -import ch.njol.yggdrasil.YggdrasilSerializable; -import org.bukkit.Bukkit; -import org.bukkit.Effect; -import org.bukkit.Location; -import org.bukkit.Material; -import org.bukkit.Particle; -import org.bukkit.entity.Entity; -import org.bukkit.entity.Player; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; - -public class VisualEffect implements SyntaxElement, YggdrasilSerializable { - - private VisualEffectType type; - - @Nullable - private Object data; - private float speed = 0f; - private float dX, dY, dZ = 0f; - - public VisualEffect() {} - - @SuppressWarnings({"null", "ConstantConditions"}) - @Override - public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { - type = VisualEffects.get(matchedPattern); - - if (exprs.length > 4) { - int exprCount = exprs.length - 4; // some effects might have multiple expressions - ContextlessEvent event = ContextlessEvent.get(); - if (exprCount == 1) { - data = exprs[0] != null ? exprs[0].getSingle(event) : null; - } else { // provide an array of expression values - Object[] dataArray = new Object[exprCount]; - for (int i = 0; i < exprCount; i++) - dataArray[i] = exprs[i] != null ? exprs[i].getSingle(event) : null; - data = dataArray; - } - } - - if (parseResult.hasTag("barrierbm")) { // barrier compatibility - data = Bukkit.createBlockData(Material.BARRIER); - } else if (parseResult.hasTag("lightbm")) { // light compatibility - data = Bukkit.createBlockData(Material.LIGHT); - } - - if ((parseResult.mark & 1) != 0) { - dX = ((Number) exprs[exprs.length - 4].getSingle(null)).floatValue(); - dY = ((Number) exprs[exprs.length - 3].getSingle(null)).floatValue(); - dZ = ((Number) exprs[exprs.length - 2].getSingle(null)).floatValue(); - } - - if ((parseResult.mark & 2) != 0) { - speed = ((Number) exprs[exprs.length - 1].getSingle(null)).floatValue(); - } - - return true; - } - - public void play(@Nullable Player[] ps, Location l, @Nullable Entity e, int count, int radius) { - assert e == null || l.equals(e.getLocation()); - - if (type.isEffect()) { - Effect effect = type.getEffect(); - Object data = type.getData(this.data, l); - - if (ps == null) { - l.getWorld().playEffect(l, effect, data, radius); - } else { - for (Player p : ps) - p.playEffect(l, effect, data); - } - - } else if (type.isEntityEffect()) { - if (e != null) - e.playEffect(type.getEntityEffect()); - - } else if (type.isParticle()) { - Particle particle = type.getParticle(); - Object data = type.getData(this.data, l); - - // Check that data has correct type (otherwise bad things will happen) - if (data != null && !particle.getDataType().isAssignableFrom(data.getClass()) - && !(data instanceof ParticleOption)) { - data = null; - if (Skript.debug()) - Skript.warning("Incompatible particle data, resetting it!"); - } - - // Some particles use offset as RGB color codes - if (type.isColorable() && data instanceof ParticleOption) { - ParticleOption option = ((ParticleOption) data); - dX = option.getRed(); - dY = option.getGreen(); - dZ = option.getBlue(); - speed = 1; - data = null; - } - - int loopCount = count == 0 ? 1 : count; - if (ps == null) { - // Colored particles must be played one at time; otherwise, colors are broken - if (type.isColorable()) { - for (int i = 0; i < loopCount; i++) { - l.getWorld().spawnParticle(particle, l, 0, dX, dY, dZ, speed, data); - } - } else { - l.getWorld().spawnParticle(particle, l, count, dX, dY, dZ, speed, data); - } - } else { - for (Player p : ps) { - if (type.isColorable()) { - for (int i = 0; i < loopCount; i++) { - p.spawnParticle(particle, l, 0, dX, dY, dZ, speed, data); - } - } else { - p.spawnParticle(particle, l, count, dX, dY, dZ, speed, data); - } - } - } - } else { - throw new IllegalStateException(); - } - } - - public VisualEffectType getType() { - return type; - } - - @Override - public String toString() { - return toString(0); - } - - public String toString(int flags) { - return type.getName().toString(flags); - } - - @Override - public int hashCode() { - return Objects.hash(type, data); - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - VisualEffect that = (VisualEffect) o; - return type == that.type && Objects.equals(data, that.data); - } - - @Override - public @NotNull String getSyntaxTypeName() { - return "visual effect"; // why is this a unique syntax element, it really needs to go - } - -} diff --git a/src/main/java/ch/njol/skript/util/visual/VisualEffectType.java b/src/main/java/ch/njol/skript/util/visual/VisualEffectType.java deleted file mode 100644 index fa8dd41d9ce..00000000000 --- a/src/main/java/ch/njol/skript/util/visual/VisualEffectType.java +++ /dev/null @@ -1,111 +0,0 @@ -package ch.njol.skript.util.visual; - -import ch.njol.skript.localization.Language; -import ch.njol.skript.localization.Noun; -import ch.njol.skript.log.BlockingLogHandler; -import ch.njol.skript.log.SkriptLogger; -import org.bukkit.Effect; -import org.bukkit.EntityEffect; -import org.bukkit.Location; -import org.bukkit.Particle; -import org.jetbrains.annotations.Nullable; - -import java.util.Objects; -import java.util.function.BiFunction; - -public class VisualEffectType { - - private static final String LANGUAGE_NODE = "visual effects"; - - private final Enum effect; - - private String pattern; - private Noun name; - - private boolean colorable = false; - private BiFunction dataSupplier = (o, location) -> null; - - private VisualEffectType(Enum effect) { - this.effect = effect; - } - - public void setColorable() { - colorable = true; - } - - public boolean isColorable() { - return colorable; - } - - public void withData(BiFunction dataSupplier) { - this.dataSupplier = dataSupplier; - } - - @Nullable - public Object getData(Object raw, Location location) { - return dataSupplier.apply(raw, location); - } - - public String getId() { - return effect.getDeclaringClass().getSimpleName() + "." + effect.name(); - } - - public Noun getName() { - return name; - } - - public String getPattern() { - return pattern; - } - - public boolean isEffect() { - return effect instanceof Effect; - } - - public boolean isEntityEffect() { - return effect instanceof EntityEffect; - } - - public boolean isParticle() { - return effect instanceof Particle; - } - - public Effect getEffect() { - if (!isEffect()) - throw new IllegalStateException(); - return (Effect) effect; - } - - public EntityEffect getEntityEffect() { - if (!isEntityEffect()) - throw new IllegalStateException(); - return (EntityEffect) effect; - } - - public Particle getParticle() { - if (!isParticle()) - throw new IllegalStateException(); - return (Particle) effect; - } - - @Nullable - static VisualEffectType of(Enum effect) { - Objects.requireNonNull(effect); - - VisualEffectType type = new VisualEffectType(effect); - String node = LANGUAGE_NODE + "." + type.getId(); - - if (!Language.keyExistsDefault(node + ".pattern")) - return null; - - String pattern = Language.get(node + ".pattern"); - - type.name = new Noun(node + ".name"); - - String areaPattern = Language.get_(LANGUAGE_NODE + ".area_expression"); - type.pattern = pattern + " " + (areaPattern != null ? areaPattern : ""); - - return type; - } - -} diff --git a/src/main/java/ch/njol/skript/util/visual/VisualEffects.java b/src/main/java/ch/njol/skript/util/visual/VisualEffects.java deleted file mode 100644 index c85ec353407..00000000000 --- a/src/main/java/ch/njol/skript/util/visual/VisualEffects.java +++ /dev/null @@ -1,268 +0,0 @@ -package ch.njol.skript.util.visual; - -import ch.njol.skript.Skript; -import ch.njol.skript.aliases.ItemType; -import ch.njol.skript.bukkitutil.ItemUtils; -import ch.njol.skript.lang.SkriptParser; -import ch.njol.skript.lang.SyntaxElementInfo; -import ch.njol.skript.localization.Language; -import ch.njol.skript.localization.Noun; -import ch.njol.skript.util.Color; -import ch.njol.skript.util.ColorRGB; -import ch.njol.skript.util.Direction; -import ch.njol.skript.util.SkriptColor; -import ch.njol.skript.util.Timespan; -import ch.njol.skript.variables.Variables; -import ch.njol.util.StringUtils; -import ch.njol.util.coll.iterator.SingleItemIterator; -import org.bukkit.*; -import org.bukkit.block.BlockFace; -import org.bukkit.entity.Entity; -import org.bukkit.inventory.ItemStack; -import org.bukkit.potion.PotionEffect; -import org.bukkit.potion.PotionEffectType; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.stream.Stream; - -public class VisualEffects { - - private static final boolean NEW_EFFECT_DATA = Skript.classExists("org.bukkit.block.data.BlockData"); - private static final boolean SPELL_CLASS_EXISTS = Skript.classExists("org.bukkit.Particle$Spell"); - - private static final Map> effectTypeModifiers = new HashMap<>(); - private static SyntaxElementInfo elementInfo; - private static VisualEffectType[] visualEffectTypes; - - static { - Variables.yggdrasil.registerSingleClass(VisualEffectType.class, "VisualEffect.NewType"); - Variables.yggdrasil.registerSingleClass(Effect.class, "Bukkit_Effect"); - Variables.yggdrasil.registerSingleClass(EntityEffect.class, "Bukkit_EntityEffect"); - } - - @Nullable - public static VisualEffect parse(String s) { - if (elementInfo == null) - return null; - return SkriptParser.parseStatic( - Noun.stripIndefiniteArticle(s), new SingleItemIterator<>(elementInfo), null); - } - - public static VisualEffectType get(int i) { - return visualEffectTypes[i]; - } - - public static String getAllNames() { - List names = new ArrayList<>(); - for (VisualEffectType visualEffectType : visualEffectTypes) { - names.add(visualEffectType.getName()); - } - return StringUtils.join(names, ", "); - } - - private static void generateTypes() { - List types = new ArrayList<>(); - Stream.of(Effect.class, EntityEffect.class, Particle.class) - .map(Class::getEnumConstants) - .flatMap(Arrays::stream) - .map(VisualEffectType::of) - .filter(Objects::nonNull) - .forEach(types::add); - - for (VisualEffectType type : types) { - String id = type.getId(); - if (effectTypeModifiers.containsKey(id)) - effectTypeModifiers.get(id).accept(type); - } - - visualEffectTypes = types.toArray(new VisualEffectType[0]); - String[] patterns = new String[visualEffectTypes.length]; - for (int i = 0; i < visualEffectTypes.length; i++) { - patterns[i] = visualEffectTypes[i].getPattern(); - } - elementInfo = new SyntaxElementInfo<>(patterns, VisualEffect.class, VisualEffect.class.getName()); - } - - private static void registerColorable(String id) { - effectTypeModifiers.put(id, VisualEffectType::setColorable); - } - - private static void registerDataSupplier(String id, BiFunction dataSupplier) { - Consumer consumer = type -> type.withData(dataSupplier); - if (effectTypeModifiers.containsKey(id)) { - consumer = effectTypeModifiers.get(id).andThen(consumer); - } - effectTypeModifiers.put(id, consumer); - } - - // only applies to some older versions where ITEM_CRACK exists - private static final boolean IS_ITEM_CRACK_MATERIAL = - Skript.fieldExists(Particle.class, "ITEM_CRACK") - && Particle.valueOf("ITEM_CRACK").getDataType() == Material.class; - - static { - Language.addListener(() -> { - if (visualEffectTypes != null) // Already registered - return; - - // Data suppliers - registerDataSupplier("Effect.POTION_BREAK", (raw, location) -> - new PotionEffect(raw == null ? PotionEffectType.SPEED : (PotionEffectType) raw, 1, 0)); - registerDataSupplier("Effect.SMOKE", (raw, location) -> { - if (raw == null) - return BlockFace.SELF; - return Direction.getFacing(((Direction) raw).getDirection(location), false); - }); - - // Useful: https://minecraft.wiki/w/Particle_format - - /* - * Particles with BlockData DataType - */ - final BiFunction blockDataSupplier = (raw, location) -> { - if (raw instanceof Object[]) { // workaround for modern pattern since it contains a choice - Object[] data = (Object[]) raw; - raw = data[0] != null ? data[0] : data[1]; - } - if (raw == null) - return Bukkit.createBlockData(Material.AIR); - if (raw instanceof ItemType) { - ItemType type = (ItemType) raw; - ItemStack random = type.getRandom(); - return Bukkit.createBlockData(random != null ? random.getType() : type.getMaterial()); - } - return raw; - }; - registerDataSupplier("Particle.BLOCK", blockDataSupplier); - registerDataSupplier("Particle.BLOCK_CRACK", blockDataSupplier); - registerDataSupplier("Particle.BLOCK_DUST", blockDataSupplier); - - registerDataSupplier("Particle.BLOCK_MARKER", blockDataSupplier); - - registerDataSupplier("Particle.DUST_PILLAR", blockDataSupplier); - - registerDataSupplier("Particle.FALLING_DUST", blockDataSupplier); - - /* - * Particles with DustOptions DataType - */ - final Color defaultColor = SkriptColor.LIGHT_RED; - final BiFunction dustOptionsSupplier = (raw, location) -> { - Object[] data = (Object[]) raw; - Color color = data[0] != null ? (Color) data[0] : defaultColor; - float size = data[1] != null ? (Float) data[1] : 1; - return new Particle.DustOptions(color.asBukkitColor(), size); - }; - registerDataSupplier("Particle.DUST", dustOptionsSupplier); - registerDataSupplier("Particle.REDSTONE", dustOptionsSupplier); - - /* - * Particles with Color DataType - */ - registerDataSupplier("Particle.ENTITY_EFFECT", (raw, location) -> { - if (raw == null) - return defaultColor.asBukkitColor(); - return ((Color) raw).asBukkitColor(); - }); - final BiFunction oldColorSupplier = (raw, location) -> { - Color color = raw != null ? (Color) raw : defaultColor; - return new ParticleOption(color, 1); - }; - registerColorable("Particle.SPELL_MOB"); - registerDataSupplier("Particle.SPELL_MOB", oldColorSupplier); - registerColorable("Particle.SPELL_MOB_AMBIENT"); - registerDataSupplier("Particle.SPELL_MOB_AMBIENT", oldColorSupplier); - - final BiFunction itemStackSupplier = (raw, location) -> { - ItemStack itemStack = null; - if (raw instanceof ItemType) - itemStack = ((ItemType) raw).getRandom(); - if (itemStack == null || ItemUtils.isAir(itemStack.getType())) // item crack air is not allowed - itemStack = new ItemStack(Material.IRON_SWORD); - if (IS_ITEM_CRACK_MATERIAL) - return itemStack.getType(); - return itemStack; - }; - registerDataSupplier("Particle.ITEM", itemStackSupplier); - registerDataSupplier("Particle.ITEM_CRACK", itemStackSupplier); - - /* - * Particles with other DataTypes - */ - registerDataSupplier("Particle.DUST_COLOR_TRANSITION", (raw, location) -> { - Object[] data = (Object[]) raw; - Color fromColor = data[0] != null ? (Color) data[0] : defaultColor; - Color toColor = data[1] != null ? (Color) data[1] : defaultColor; - float size = data[2] != null ? (Float) data[2] : 1; - return new Particle.DustTransition(fromColor.asBukkitColor(), toColor.asBukkitColor(), size); - }); - - // uses color differently - registerColorable("Particle.NOTE"); - // TODO test how this works - registerDataSupplier("Particle.NOTE", (raw, location) -> { - int colorValue = (int) (((Number) raw).floatValue() * 255); - ColorRGB color = ColorRGB.fromRGB(colorValue, 0, 0); - return new ParticleOption(color, 1); - }); - - // Float DataType, represents "the angle the particle displays at in radians" - registerDataSupplier("Particle.SCULK_CHARGE", (raw, location) -> raw != null ? raw : 0); - - // Integer DataType, represents "the delay in ticks" - registerDataSupplier("Particle.SHRIEK", (raw, location) -> { - int delay = 0; - if (raw instanceof Timespan) - delay = (int) Math.min(Math.max(((Timespan) raw).getAs(Timespan.TimePeriod.TICK), 0), Integer.MAX_VALUE); - return delay; - }); - - registerDataSupplier("Particle.VIBRATION", (raw, location) -> VibrationUtils.buildVibration((Object[]) raw, location)); - - if (SPELL_CLASS_EXISTS) { - registerDataSupplier("Particle.EFFECT", (input, location) -> - new Particle.Spell(org.bukkit.Color.WHITE, 1f)); - registerDataSupplier("Particle.INSTANT_EFFECT", (input, location) -> - new Particle.Spell(org.bukkit.Color.WHITE, 1f)); - - registerDataSupplier("Particle.DRAGON_BREATH", (input, location) -> 1f); - - registerDataSupplier("Particle.FLASH", (input, location) -> org.bukkit.Color.WHITE); - } - - generateTypes(); - }); - } - - // exists to avoid NoClassDefFoundError from Vibration - @SuppressWarnings({"removal"}) - private static final class VibrationUtils { - private static Vibration buildVibration(Object[] data, Location location) { - int arrivalTime = -1; - if (data[1] != null) - arrivalTime = (int) Math.min(Math.max(((Timespan) data[1]).getAs(Timespan.TimePeriod.TICK), 0), Integer.MAX_VALUE); - if (data[0] instanceof Entity) { - Entity entity = (Entity) data[0]; - if (arrivalTime == -1) - arrivalTime = (int) (location.distance(entity.getLocation()) / 20); - // new constructor only exists on newer versions - return new Vibration(location, new Vibration.Destination.EntityDestination(entity), arrivalTime); - } - // assume it's a location - Location destination = data[0] != null ? (Location) data[0] : location; - if (arrivalTime == -1) - arrivalTime = (int) (location.distance(destination) / 20); - // new constructor only exists on newer versions - return new Vibration(location, new Vibration.Destination.BlockDestination(destination), arrivalTime); - } - } - -} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index 328b5e50ece..51cea64e248 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -2,6 +2,9 @@ import ch.njol.skript.Skript; import ch.njol.skript.config.Node; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; import ch.njol.skript.entity.EntityData; import ch.njol.skript.lang.Effect; import ch.njol.skript.lang.Expression; @@ -20,12 +23,30 @@ import org.skriptlang.skript.bukkit.particles.GameEffect; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +@Name("Play or Draw an Effect") +@Description(""" + Plays or draws a specific effect at a location, to a player, or on an entity. + Effects can be: + * Particles. + * Game effects, which consist of combinations of particles and sounds, like the bone meal particles, \ + the sound of footsteps on a specific block, or the particles and sound of breaking a splash potion. + * Entity effects, which are particles or animations that are entity-specific and can only be played on \ + a compatible entity. For example, the ravager attack animation can be played with this effect. + + All effects vary significantly in availability from version to version, and some may simply not function on your \ + version of Minecraft. Some effects, like the death animation entity effect, may cause client glitches and should be \ + used carefully! + """) +@Example("draw 2 smoke particles at player") +@Example("force draw 10 red dust particles of size 3 for player") +@Example("play blue instant splash potion break effect with a view radius of 10") +@Example("show ravager attack animation on player's target") public class EffPlayEffect extends Effect { static { Skript.registerEffect(EffPlayEffect.class, "[:force] (play|show|draw) %gameeffects/particles% [%-directions% %locations%] [as %-player%]", "[:force] (play|show|draw) %gameeffects/particles% [%-directions% %locations%] (for|to) %-players% [as %-player%]", - "(play|show|draw) %gameeffects% [%-directions% %locations%] (in|with) [a] [view] (radius|range) of %-number%)", + "(play|show|draw) %gameeffects% [%-directions% %locations%] (in|with) [a] [view] (radius|range) [of] %number%", "(play|show|draw) %entityeffects% on %entities%"); } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java index 9e287a40742..39b2e09d238 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java @@ -1,6 +1,10 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -15,6 +19,17 @@ import org.skriptlang.skript.bukkit.particles.registration.DataGameEffects; import org.skriptlang.skript.bukkit.particles.registration.EffectInfo; +@Name("Game Effects with Data") +@Description(""" + Creates game effects that require some extra information, such as colors, particle counts, or block data. + Game effects consist of combinations particles and/or sounds that are used in Minecraft, such as \ + the bone meal particles, the sound of footsteps on a specific block, or the particles and sound of breaking a splash potion. + Game effects not present here do not require data and can be found in the Game Effect type. + Data requirements vary from version to version, so these docs are only accurate for the most recent Minecraft \ + version at time of release. + """) +@Example("play compost success sound effect to player") +@Since("INSERT VERSION") public class ExprGameEffectWithData extends SimpleExpression { private static final Patterns> PATTERNS; diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java index cace90cbaaa..9cbd06bb40c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleCount.java @@ -1,11 +1,29 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +@Name("Particle Count") +@Description(""" + Sets how many particles to draw. + Particle count has an influence on how the 'offset' and 'extra' values of a particle apply. + Offsets are treated as distributions if particle count is greater than 0. + Offsets are treated as velocity or some other special behavior if particle count is 0. + + This means that setting the particle count may change how your particle behaves. Be careful! + + More detailed information on particle behavior can be found at \ + Paper's particle documentation. + """) +@Example("draw 7 blue dust particles at player") +@Since("INSERT VERSION") public class ExprParticleCount extends SimplePropertyExpression { static { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java index d425ec0f583..4d375848df4 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleDistribution.java @@ -1,6 +1,10 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import org.bukkit.event.Event; import org.bukkit.util.Vector; @@ -8,6 +12,28 @@ import org.joml.Vector3d; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +@Name("Particle Distribution") +@Description(""" + Determines the normal distribution that particles may be drawn within. + The distribution is defined by a vector of x, y, and z standard deviations. + + Particles will be randomly drawn based on these values, clustering towards the center. \ + 68% of particles will be within 1 standard deviation, 95% within 2, and 99.7% within three. + The area the particles will spawn in can be roughly estimated as being within 2 times the \ + standard deviation in each axis. + + For example, a distribution of 1, 2, and 1 would spawn particles within roughly 2 blocks on the x and z axes, \ + and within 4 blocks on the y axis. + + Please note that distributions only take effect if the particle count is greater than 0! + Particles with counts of 0 do not have distributions. + If the particle count is 0, the offset is treated differently depending on the particle. + + More detailed information on particle behavior can be found at \ + Paper's particle documentation. + """) +@Example("set the particle distribution of {_my-particle} to vector(1, 2, 1)") +@Since("INSERT VERSION") public class ExprParticleDistribution extends SimplePropertyExpression { static { @@ -16,7 +42,7 @@ public class ExprParticleDistribution extends SimplePropertyExpressionPaper's particle documentation. + """) +@Example("set the particle offset of {_my-particle} to vector(1, 2, 1)") +@Since("INSERT VERSION") public class ExprParticleOffset extends SimplePropertyExpression { static { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java index cd438051c73..3904cd07e2f 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java @@ -1,11 +1,28 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.classes.Changer; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.particleeffects.ScalableEffect; +@Name("Particle Scale") +@Description(""" + Determines the scale of a particle. + This only applies to explosion particles and sweep attack particles. + Setting the particle scale will set particle count to 0, and scale will not take effect if count is greater than 0. + + Particles with counts greater than 0 do not have scale. + + More detailed information on particle behavior can be found at \ + Paper's particle documentation. + """) +@Example("set the scale of {_my-explosion-particle} to 2.3") +@Since("INSERT VERSION") public class ExprParticleScale extends SimplePropertyExpression { static { diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java index 7ba463372b9..bcac505762c 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleSpeed.java @@ -1,15 +1,31 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.expressions.base.SimplePropertyExpression; import org.bukkit.event.Event; import org.jetbrains.annotations.Nullable; import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +@Name("Particle Speed/Extra Value") +@Description(""" + Determines the specific 'speed' or 'extra' value of a particle. + This value is used in different ways depending on the particle, but in general it: + * acts as the speed at which the particle moves if the particle count is greater than 0. + * acts as a multiplier to the particle's offset if the particle count is 0. + + More detailed information on particle behavior can be found at \ + Paper's particle documentation. + """) +@Example("set the speed of {_my-flame-particle} to 2") +@Since("INSERT VERSION") public class ExprParticleSpeed extends SimplePropertyExpression { static { - register(ExprParticleSpeed.class, Number.class, "speed [value]", "particles"); + register(ExprParticleSpeed.class, Number.class, "(speed|extra) [value]", "particles"); } @Override @@ -58,7 +74,7 @@ public Class getReturnType() { @Override protected String getPropertyName() { - return "speed"; + return "speed value"; } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java index 97b5d93c930..747bc545645 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java @@ -1,6 +1,10 @@ package org.skriptlang.skript.bukkit.particles.elements.expressions; import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; import ch.njol.skript.lang.Expression; import ch.njol.skript.lang.ExpressionType; import ch.njol.skript.lang.SkriptParser.ParseResult; @@ -15,6 +19,19 @@ import org.skriptlang.skript.bukkit.particles.registration.DataParticles; import org.skriptlang.skript.bukkit.particles.registration.EffectInfo; +import java.util.Arrays; + +@Name("Particles with Data") +@Description(""" + Creates particles that require some extra information, such as colors, locations, or block data. + Particles not present here do not require data and can be found in the Particle type. + Data requirements vary from version to version, so these docs are only accurate for the most recent Minecraft \ + version at time of release. + For example, between 1.21.8 and 1.21.9, the 'flash' particle became colourable and now requires a colour data. + """) +@Example("set {blood-effect} to a red dust particle of size 1") +@Example("draw 3 blue trail particles moving to player's target over 3 seconds at player") +@Since("INSERT VERSION") public class ExprParticleWithData extends SimpleExpression { private static final Patterns> PATTERNS; @@ -24,7 +41,7 @@ public class ExprParticleWithData extends SimpleExpression { Object[][] patterns = new Object[DataParticles.getParticleInfos().size()][2]; int i = 0; for (var particleInfo : DataParticles.getParticleInfos()) { - patterns[i][0] = particleInfo.pattern(); + patterns[i][0] = "[%-*number%|a[n]] " + particleInfo.pattern(); patterns[i][1] = particleInfo; i++; } @@ -36,11 +53,15 @@ public class ExprParticleWithData extends SimpleExpression { private ParseResult parseResult; private Expression[] expressions; private EffectInfo effectInfo; + private Expression count; @Override + @SuppressWarnings("unchecked") public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { this.parseResult = parseResult; - this.expressions = expressions; + // exclude count expr + this.expressions = Arrays.copyOfRange(expressions, 1, expressions.length); + this.count = (Expression) expressions[0]; effectInfo = PATTERNS.getInfo(matchedPattern); return effectInfo != null; } @@ -54,6 +75,12 @@ public boolean init(Expression[] expressions, int matchedPattern, Kleenean is } ParticleEffect effect = ParticleEffect.of(effectInfo.effect()); effect.data(data); + if (this.count != null) { + Number count = this.count.getSingle(event); + if (count != null) { + effect.count(Math.clamp(count.intValue(), 0, 16_384)); // drawing more than the maximum display count of 16,384 is likely unintended and can crash users. + } + } return new ParticleEffect[] {effect}; } @@ -69,7 +96,10 @@ public Class getReturnType() { @Override public String toString(@Nullable Event event, boolean debug) { - return effectInfo.toStringFunction().toString(expressions, parseResult, new SyntaxStringBuilder(event, debug)).toString(); + SyntaxStringBuilder ssb = new SyntaxStringBuilder(event, debug); + if (count != null) + ssb.append(count); + return effectInfo.toStringFunction().toString(expressions, parseResult, ssb).toString(); } } diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithOffset.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithOffset.java new file mode 100644 index 00000000000..15a4cd2682c --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithOffset.java @@ -0,0 +1,109 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.util.Patterns; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.Nullable; +import org.joml.Vector3d; +import org.skriptlang.skript.bukkit.particles.particleeffects.DirectionalEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +import java.lang.reflect.Array; +import java.util.function.BiFunction; + +@Name("Particle with Offset/Distribution/Velocity") +@Description(""" + Applies a specific offset to a particle. + Offsets are treated as distributions if particle count is greater than 0. + Offsets are treated as velocity or some other special behavior if particle count is 0. + Setting distribution/velocity with this method may change the particle count to 1/0 respectively. + + More detailed information on particle behavior can be found at \ + Paper's particle documentation. + """) +@Example("draw an electric spark particle with a velocity of vector(1,2,3) at player") +@Example("draw 12 red dust particles with a distribution of vector(1,2,1) at player's head location") +@Since("INSERT VERSION") +public class ExprParticleWithOffset extends PropertyExpression { + + enum Mode { + OFFSET(ParticleEffect::offset), + DISTRIBUTION(ParticleEffect::distribution), + VELOCITY((particle, offset) -> ((DirectionalEffect) particle).velocity(offset)); + + private final BiFunction apply; + + Mode(BiFunction apply) { + this.apply = apply; + } + + public ParticleEffect apply(ParticleEffect particle, Vector3d offset) { + return this.apply.apply(particle, offset); + } + } + + static Patterns patterns = new Patterns<>(new Object[][]{ + {"%particles% with [an] offset [of] %vector%", Mode.OFFSET}, + {"%particles% with [a] distribution [of] %vector%", Mode.DISTRIBUTION}, + {"%directionalparticles% with [a] velocity [of] %vector%", Mode.VELOCITY} + }); + + static { + Skript.registerExpression(ExprParticleWithOffset.class, ParticleEffect.class, ExpressionType.COMBINED, patterns.getPatterns()); + } + + private Mode mode; + private Expression offset; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + mode = patterns.getInfo(matchedPattern); + setExpr((Expression) expressions[0]); + offset = (Expression) expressions[1]; + return true; + } + + @Override + protected ParticleEffect[] get(Event event, ParticleEffect[] source) { + Vector offset = this.offset.getSingle(event); + if (offset == null) + return new ParticleEffect[0]; + Vector3d offsetJoml = offset.toVector3d(); + ParticleEffect[] results = (ParticleEffect[]) Array.newInstance(getExpr().getReturnType(), source.length); + for (int i = 0; i < source.length; i++) { + results[i] = mode.apply(source[i].copy(), offsetJoml); + } + return results; + } + + @Override + public Class getReturnType() { + return getExpr().getReturnType(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder ssb = new SyntaxStringBuilder(event, debug); + ssb.append(getExpr(), "with"); + switch (mode) { + case OFFSET -> ssb.append("an offset"); + case DISTRIBUTION -> ssb.append("a distribution"); + case VELOCITY -> ssb.append("a velocity"); + } + ssb.append("of", offset); + return ssb.toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithSpeed.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithSpeed.java new file mode 100644 index 00000000000..f4ba64a5ac2 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithSpeed.java @@ -0,0 +1,75 @@ +package org.skriptlang.skript.bukkit.particles.elements.expressions; + +import ch.njol.skript.Skript; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.PropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.ExpressionType; +import ch.njol.skript.lang.SkriptParser; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.util.Kleenean; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +import java.lang.reflect.Array; + +@Name("Particle with Speed/Extra Value") +@Description(""" + Applies a specific 'speed' or 'extra' value to a particle. + This value is used in different ways depending on the particle, but in general it: + * acts as the speed at which the particle moves if the particle count is greater than 0. + * acts as a multiplier to the particle's offset if the particle count is 0. + + More detailed information on particle behavior can be found at \ + Paper's particle documentation. + """) +@Example("draw an electric spark particle with a speed of 0 at player") +@Example("draw 12 red dust particles with an extra value of 0.4 at player's head location") +@Since("INSERT VERSION") +public class ExprParticleWithSpeed extends PropertyExpression { + + static { + Skript.registerExpression(ExprParticleWithSpeed.class, ParticleEffect.class, ExpressionType.COMBINED, + "%particles% with ([a] speed|[an] extra) [value] [of] %number%"); + } + + private Expression speed; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, SkriptParser.ParseResult parseResult) { + setExpr((Expression) expressions[0]); + speed = (Expression) expressions[1]; + return true; + } + + @Override + protected ParticleEffect[] get(Event event, ParticleEffect[] source) { + Number speed = this.speed.getSingle(event); + if (speed == null) + return new ParticleEffect[0]; + double speedValue = speed.doubleValue(); + ParticleEffect[] results = (ParticleEffect[]) Array.newInstance(getExpr().getReturnType(), source.length); + for (int i = 0; i < source.length; i++) { + results[i] = source[i].copy().extra(speedValue); + } + return results; + } + + @Override + public Class getReturnType() { + return getExpr().getReturnType(); + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + SyntaxStringBuilder ssb = new SyntaxStringBuilder(event, debug); + ssb.append(getExpr(), "with a speed value of", speed); + return ssb.toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java index 75eeda51375..448178f9766 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java @@ -22,6 +22,8 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * A wrapper around Paper's ParticleBuilder to provide additional functionality @@ -83,6 +85,8 @@ public class ParticleEffect extends ParticleBuilder implements Debuggable { */ private static final ParticleParser ENUM_PARSER = new ParticleParser(); + private static final Pattern LEADING_NUMBER_PATTERN = Pattern.compile("(\\d+) (.+)"); + /** * Parses a particle effect from a string input. Prints errors if the particle requires data. * @param input the input string @@ -90,6 +94,12 @@ public class ParticleEffect extends ParticleBuilder implements Debuggable { * @return the parsed ParticleEffect, or null if parsing failed */ public static @Nullable ParticleEffect parse(String input, ParseContext context) { + Matcher matcher = LEADING_NUMBER_PATTERN.matcher(input); + int count = 1; + if (matcher.matches()) { + count = Math.clamp(Integer.parseInt(matcher.group(1)), 0, 16_384); // drawing more than the maximum display count of 16,384 is likely unintended and can crash users. + input = matcher.group(2); + } Particle particle = ENUM_PARSER.parse(input.toLowerCase(Locale.ENGLISH), context); if (particle == null) return null; @@ -97,7 +107,7 @@ public class ParticleEffect extends ParticleBuilder implements Debuggable { Skript.error("The " + Classes.toString(particle) + " requires data and cannot be parsed directly. Use the Particle With Data expression instead."); return null; } - return ParticleEffect.of(particle); + return ParticleEffect.of(particle).count(count); } /** @@ -195,7 +205,7 @@ public boolean isUsingNormalDistribution() { * @return the distribution of this particle. The distribution is defined as 3 normal distributions in the x/y/z axes, * with the returned vector containing the standard deviations. The mean will always be 0. */ - public Vector3d getDistribution() { + public Vector3d distribution() { return offset(); } @@ -205,11 +215,11 @@ public Vector3d getDistribution() { * Sets the count to 1 if it was 0. * @param distribution The new standard deviations to use. */ - public void setDistribution(Vector3d distribution) { + public ParticleEffect distribution(Vector3d distribution) { if (!isUsingNormalDistribution()) { count(1); } - offset(distribution); + return offset(distribution); } @Override diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java index 7ae93010cf4..df46a582194 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java @@ -26,7 +26,7 @@ public class DataParticles { /** * Registers a new particle with a single data parameter, and a default value for when none is provided. * @param particle the particle - * @param pattern the pattern to use (must contain exactly one expression, which may be nullable) + * @param pattern the pattern to use (must contain exactly one expression, which may be nullable). Will be prefixed by "[%-*number%|a[n]]" * @param defaultData the default data to use if the expression is null or evaluates to null * @param dataFunction the function to convert the expression value to the data type (use input->input if they are the same) * @param toStringFunction the toString function for this pattern @@ -48,8 +48,8 @@ private static void registerParticle(Particle particle, String pattern, D /** * Registers a new particle with a custom data supplier. * @param particle the particle - * @param pattern the pattern to use - * @param dataSupplier the data supplier for this particle + * @param pattern the pattern to use. Will be prefixed by "[%-*number%|a[n]]" + * @param dataSupplier the data supplier for this particle. * @param toStringFunction the toString function for this pattern * @param the data type */ @@ -86,31 +86,31 @@ private static void registerAll() { return new Particle.Spell(color.asBukkitColor(), power.floatValue()); }; // - registerParticle(Particle.EFFECT, "[a[n]] %color% effect particle[s] (of|with) power %number%", + registerParticle(Particle.EFFECT, "%color% effect particle[s] (of|with) power %number%", spellData, (exprs, parseResult, builder) -> builder.append(exprs[0], "effect particle of power", exprs[1])); - registerParticle(Particle.INSTANT_EFFECT, "[a[n]] %color% instant effect particle[s] (of|with) power %number%", + registerParticle(Particle.INSTANT_EFFECT, "%color% instant effect particle[s] (of|with) power %number%", spellData, (exprs, parseResult, builder) -> builder.append(exprs[0], "instant effect particle of power", exprs[1])); - registerParticle(Particle.FLASH, "[a[n]] %color% flash particle[s]", org.bukkit.Color.WHITE, + registerParticle(Particle.FLASH, "%color% flash particle[s]", org.bukkit.Color.WHITE, color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), (exprs, parseResult, builder) -> builder.append(exprs[0], "flash particle")); } - registerParticle(Particle.ENTITY_EFFECT, "[a[n]] %color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, + registerParticle(Particle.ENTITY_EFFECT, "%color% (potion|entity) effect particle[s]", org.bukkit.Color.WHITE, color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), (exprs, parseResult, builder) -> builder.append(exprs[0], "potion effect particle")); if (Skript.isRunningMinecraft(1, 21, 5)) { - registerParticle(Particle.TINTED_LEAVES, "[a[n]] %color% tinted leaves particle[s]", org.bukkit.Color.WHITE, + registerParticle(Particle.TINTED_LEAVES, "%color% tinted leaves particle[s]", org.bukkit.Color.WHITE, color -> ((ch.njol.skript.util.Color) color).asBukkitColor(), (exprs, parseResult, builder) -> builder.append(exprs[0], "tinted leaves particle")); } - registerParticle(Particle.DUST, "[a[n]] %color% dust particle[s] [of size %number%]", + registerParticle(Particle.DUST, "%color% dust particle[s] [of size %number%]", // (event, expressions, parseResult) -> { org.bukkit.Color bukkitColor; @@ -131,7 +131,7 @@ private static void registerAll() { (exprs, parseResult, builder) -> builder.append(exprs[0], "dust particle of size", exprs[1])); // dust color transition particle - registerParticle(Particle.DUST_COLOR_TRANSITION, "[a[n]] %color% dust particle[s] [of size %number%] that transitions to %color%", + registerParticle(Particle.DUST_COLOR_TRANSITION, "%color% dust particle[s] [of size %number%] that transitions to %color%", // (event, expressions, parseResult) -> { org.bukkit.Color bukkitColor; @@ -160,31 +160,31 @@ private static void registerAll() { (exprs, parseResult, builder) -> builder.append(exprs[0], "dust particle of size", exprs[1], "that transitions to", exprs[2])); // blockdata - registerParticle(Particle.BLOCK, "[a[n]] %itemtype/blockdata% block particle[s]", + registerParticle(Particle.BLOCK, "%itemtype/blockdata% block particle[s]", DataSupplier::getBlockData, (exprs, parseResult, builder) -> builder.append(exprs[0], "block particle")); if (Skript.isRunningMinecraft(1, 21, 2)) { - registerParticle(Particle.BLOCK_CRUMBLE, "[a[n]] %itemtype/blockdata% [block] crumble particle[s]", + registerParticle(Particle.BLOCK_CRUMBLE, "%itemtype/blockdata% [block] crumble particle[s]", DataSupplier::getBlockData, (exprs, parseResult, builder) -> builder.append(exprs[0], "block crumble particle")); } - registerParticle(Particle.BLOCK_MARKER, "[a[n]] %itemtype/blockdata% [block] marker particle[s]", + registerParticle(Particle.BLOCK_MARKER, "%itemtype/blockdata% [block] marker particle[s]", DataSupplier::getBlockData, (exprs, parseResult, builder) -> builder.append(exprs[0], "block marker particle")); - registerParticle(Particle.DUST_PILLAR, "[a[n]] %itemtype/blockdata% dust pillar particle[s]", + registerParticle(Particle.DUST_PILLAR, "%itemtype/blockdata% dust pillar particle[s]", DataSupplier::getBlockData, (exprs, parseResult, builder) -> builder.append(exprs[0], "dust pillar particle")); - registerParticle(Particle.FALLING_DUST, "[a] falling %itemtype/blockdata% dust particle[s]", + registerParticle(Particle.FALLING_DUST, "falling %itemtype/blockdata% dust particle[s]", DataSupplier::getBlockData, (exprs, parseResult, builder) -> builder.append("falling", exprs[0], "dust particle")); // misc - registerParticle(Particle.ITEM, "[an] %itemtype% item particle[s]", + registerParticle(Particle.ITEM, "%itemtype% item particle[s]", // (event, expressions, parseResult) -> { ItemType itemType = (ItemType) expressions[0].getSingle(event); @@ -194,7 +194,7 @@ private static void registerAll() { }, // (exprs, parseResult, builder) -> builder.append(exprs[0], "item particle")); - registerParticle(Particle.SCULK_CHARGE, "[a] sculk charge particle[s] [with [a] roll angle [of] %-number%]", + registerParticle(Particle.SCULK_CHARGE, "sculk charge particle[s] [with [a] roll angle [of] %-number%]", // (event, expressions, parseResult) -> { if (expressions[0] == null) @@ -207,12 +207,12 @@ private static void registerAll() { (exprs, parseResult, builder) -> builder.append("sculk charge particle)") .appendIf(exprs[0] != null, "with a roll angle of", exprs[0])); - registerParticle(Particle.SHRIEK, "[a] shriek particle[s] [delayed by %-timespan%]", 0, + registerParticle(Particle.SHRIEK, "shriek particle[s] [delayed by %-timespan%]", 0, timespan -> ((Timespan) timespan).getAs(Timespan.TimePeriod.TICK), (exprs, parseResult, builder) -> builder.append("shriek particle") .appendIf(exprs[0] != null, "delayed by", exprs[0])); - registerParticle(Particle.VIBRATION, "[a] vibration particle moving to[wards] %entity/location% [over [a duration of] %-timespan%]", + registerParticle(Particle.VIBRATION, "vibration particle moving to[wards] %entity/location% [over [a duration of] %-timespan%]", // (event, expressions, parseResult) -> { Object target = expressions[0].getSingle(event); @@ -238,7 +238,7 @@ private static void registerAll() { .appendIf(exprs[1] != null, "over", exprs[1])); if (Skript.isRunningMinecraft(1, 21, 4)) { - registerParticle(Particle.TRAIL, "[a[n]] %color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", + registerParticle(Particle.TRAIL, "%color% trail particle moving to[wards] %location% [over [a duration of] %-timespan%]", // (event, expressions, parseResult) -> { org.bukkit.Color bukkitColor; @@ -308,7 +308,7 @@ private static void registerAll() { } if (Skript.isRunningMinecraft(1, 21, 9)) { - registerParticle(Particle.DRAGON_BREATH, "[a] dragon breath particle[s] [of power %-number%]", + registerParticle(Particle.DRAGON_BREATH, "dragon breath particle[s] [of power %-number%]", 0.5f, input -> input, (exprs, parseResult, builder) -> builder.append("dragon breath particle") .appendIf(exprs[0] != null, "of power", exprs[0])); diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index d51105339e9..6577f08f2db 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -1773,638 +1773,6 @@ game modes: adventure: adventure spectator: spectator -# -- Visual Effects -- -visual effects: - area_expression: [[with] [(1¦offset (of|by) %-number%, %-number%(,| and) %-number%)][(2¦[(and|at) ]speed %-number%)]] - effect: - ender_signal: - name: ender signal @an - pattern: ender [eye] signal[s] - mobspawner_flames: - name: mobspawner flames @x - pattern: [mo(b|nster)][ ]spawner flames - potion_break: - name: potion break @a - pattern: [%potioneffecttype%] potion break[ing] - smoke: - name: smoke @- - pattern: smoke [%direction%] - entityeffect: - hurt: - name: hurt @- - pattern: (hurt|damage) - sheep_eat: - name: sheep eating @a - pattern: sheep eat[ing] [a] [grass] [block] - wolf_hearts: - name: wolf hearts @x - pattern: wolf (hearts|tame|being tamed) - wolf_shake: - name: wolf shaking @a - pattern: wolf shak(e|ing) - wolf_smoke: - name: wolf smoke @- - pattern: wolf smok(e|ing) - iron_golem_rose: - name: iron golem offering rose @an - pattern: (iron golem [offering] rose|rose in hand) - witch_magic: - name: witch magic @- - pattern: witch magic - zombie_transform: - name: zombie turning to a villager - pattern: ([zombie] turning to a villager|zombie transform) - firework_explode: - name: firework explosion @a - pattern: firework explosion - arrow_particles: - name: arrow particles @x - pattern: [tipped] arrow particles - rabbit_jump: - name: jumping rabbit @a - pattern: (jumping rabbit|rabbit jump) - love_hearts: - name: love hearts @x - pattern: (love|breeding) hearts - squid_rotate: - name: squid rotation reset @a - pattern: [squid] rotation reset - entity_poof: - name: entity poof @- - pattern: entity poof - guardian_target: - name: guardian target @a - pattern: guardian target [with laser] - shield_block: - name: block with shield @- - pattern: (block [with a shield]|shield block) - shield_break: - name: shield break @a - pattern: shield break - armor_stand_hit: - name: armor stand hit @a - pattern: armo[u]r stand hit - thorns_hurt: - name: hurt by thorns @- - pattern: (hurt|damage) by thorns - iron_golem_sheath: - name: iron golem sheathing rose - pattern: iron golem (sheathing|putting away) rose - totem_resurrect: - name: resurrection by totem - pattern: resurrection [by totem] - hurt_drown: - name: hurt by drowning @- - pattern: (hurt|damage) by drowning - hurt_explosion: - name: hurt by explosion @- - pattern: (hurt|damage) by explosion - particle: - - angry_villager: # added in 1.20.5 - name: angry villager @a - pattern: (angry villager|[villager] thundercloud) - villager_angry: # for versions below 1.20.5 - name: angry villager @a - pattern: (angry villager|[villager] thundercloud) - - ash: # added in 1.16 - name: ash @- - pattern: ash - - block: # added in 1.20.5 - name: block @- - pattern: (%-blockdata/itemtype% break[ing]|[sprinting] dust of %-blockdata/itemtype%) - block_crack: # for versions below 1.20.5 - name: block break @- - pattern: %blockdata/itemtype% break[ing] - block_dust: # for versions below 1.20.5 - name: block dust @- - pattern: [sprinting] dust of %blockdata/itemtype% - - block_marker: # added in 1.18 - name: block marker @a - pattern: (barrierbm:barrier [particle]|lightbm:light [particle]|%-blockdata/itemtype% block marker) - barrier: - name: barrier @a - pattern: barrier [particle] - light: # added in 1.17 - name: light @- - pattern: light [particle] - - bubble: # added in 1.20.5 - name: bubble @- - pattern: [water] bubble - water_bubble: # for versions below 1.20.5 - name: water bubble @- - pattern: [water] bubble - - bubble_column_up: # added in 1.13 - name: bubble column up @- - pattern: (current|bubble column) up - - bubble_pop: # added in 1.13 - name: bubble pop @- - pattern: bubble pop - - campfire_cosy_smoke: # added in 1.14 - name: campfire cosy smoke @- - pattern: campfire co(s|z)y smoke - - campfire_signal_smoke: # added in 1.14 - name: campfire signal smoke @- - pattern: campfire signal smoke - - cherry_leaves: # added in 1.20 - name: cherry leaves @- - pattern: cherry leaves - - cloud: - name: cloud @a - pattern: cloud - - composter: # added in 1.14 - name: composter @- - pattern: composter [particle] - - crimson_spore: # added in 1.16 - name: crimson spore @- - pattern: crimson spore - - crit: - name: critical hit @a - pattern: (critical hit|crit) [spark] - - current_down: # added in 1.13 - name: current down @- - pattern: (current|bubble column) down - - damage_indicator: # added in 1.14 - name: damage indicator @- - pattern: damage indicator - - dolphin: # added in 1.13 - name: dolphin @- - pattern: dolphin [particle] - - dragon_breath: # added in 1.14 - name: dragon breath @- - pattern: dragon[[']s] breath - - dripping_dripstone_lava: # added in 1.17 - name: dripping dripstone lava @- - pattern: dripping dripstone lava - - dripping_dripstone_water: # added in 1.17 - name: dripping dripstone water @- - pattern: dripping dripstone water - - dripping_honey: # added in 1.15 - name: dripping honey @- - pattern: dripping honey - - dripping_lava: # added in 1.20.5 - name: dripping lava @a - pattern: (dripping lava|lava drip) - drip_lava: # for versions below 1.20.5 - name: lava drip @a - pattern: (dripping lava|lava drip) - - dripping_obsidian_tear: # added in 1.16 - name: dripping obsidian tear @- - pattern: dripping obsidian tear - - dripping_water: # added in 1.20.5 - name: dripping water - pattern: (dripping water|water drip) - drip_water: # for versions below 1.20.5 - name: water drip @a - pattern: (dripping water|water drip) - - dust: # added in 1.20.5 - name: dust @- - pattern: [%-color%] [colo[u]red] dust [with size %-float%] - redstone: # for versions below 1.20.5 - name: coloured dust @- - pattern: [%-color%] [colo[u]red] dust [with size %-float%] - - dust_color_transition: # added in 1.17 - name: dust color transition @a - pattern: dust colo[u]r transition [from %-color%] [to %-color%] [with size %-float%] - - dust_pillar: # added in 1.20.5 (for 1.21) - name: dust pillar @a - pattern: %blockdata/itemtype% dust pillar - - dust_plume: # added in 1.20.3 - name: dust plume @a - pattern: dust plume - - effect: # added in 1.20.5 - name: effect @an - pattern: (effect|spell|thrown potion) # lingering is part of entity_effect - spell: # for versions below 1.20.5 - name: spell @a - pattern: (effect|spell|thrown potion|lingering potion) - - egg_crack: # added in 1.20 - name: egg crack @an - pattern: [sniffer] egg crack - - elder_guardian: # added in 1.20.5 - name: elder guardian @- - pattern: (elder guardian particle|mob appearance|guardian ghost) - mob_appearance: # for versions below 1.20.5 - name: mob appearance @- - pattern: (elder guardian particle|mob appearance|guardian ghost) - - electric_spark: - name: electric spark @- - pattern: electric spark - - enchant: # added in 1.20.5 - name: enchant - pattern: (enchant|flying (glyph|letter)|enchantment table) - enchantment_table: # for versions below 1.20.5 - name: flying glyph @a - pattern: (enchant|flying (glyph|letter)|enchantment table) - - enchanted_hit: # added in 1.20.5 - name: enchanted hit @an - pattern: (enchanted hit|(magic[al]|blue) (critical hit|crit) [spark]) - crit_magic: # for versions below 1.20.5 - name: magical critical hit @a - pattern: (enchanted hit|(magic[al]|blue) (critical hit|crit) [spark]) - - end_rod: - name: end rod @- - pattern: end rod [particle] - - entity_effect: # added in 1.20.5 - name: entity effect @an - pattern: (lingering potion|[%-color%] [transparent] potion swirl) - # TODO ambient_entity_effect seems to exist, but is not supported by spigot particle enum? - spell_mob: # for versions below 1.20.5 - name: potion swirl @a - pattern: [%-color%] potion swirl - spell_mob_ambient: # for versions below 1.20.5 - name: transparent potion swirl @a - pattern: [%-color%] transparent potion swirl - - # TODO explosions are a mess (see explosion_normal, explosion_large, explosion_huge) - explosion: # added in 1.20.5 - name: large explosion @a - pattern: large explosion - explosion_large: # for versions below 1.20.5 - name: large explosion @a - pattern: large explosion - - explosion_emitter: # added in 1.20.5 - name: explosion emitter @an - pattern: (explosion emitter|huge explosion) - explosion_huge: # for versions below 1.20.5 - name: huge explosion @a - pattern: (explosion emitter|huge explosion) - - falling_dripstone_lava: # added in 1.17 - name: falling dripstone lava @- - pattern: falling dripstone lava - - falling_dripstone_water: # added in 1.17 - name: falling dripstone water @- - pattern: falling dripstone water - - falling_dust: - name: falling dust @- - pattern: falling dust of %blockdata/itemtype% - - falling_honey: # added in 1.15 - name: falling honey @- - pattern: falling honey - - falling_lava: # added in 1.14 - name: falling lava @- - pattern: falling lava - - falling_nectar: # added in 1.15 - name: falling nectar @- - pattern: falling nectar - - falling_obsidian_tear: # added in 1.16 - name: falling obsidian tear @- - pattern: falling obsidian tear - - falling_spore_blossom: # added in 1.17 - name: falling spore blossom @- - pattern: falling spore blossom - - falling_water: # added in 1.14 - name: falling water @- - pattern: falling water - - firework: # added in 1.20.5 - name: firework @- - pattern: (firework particle|firework['s] spark) - fireworks_spark: # for versions below 1.20.5 - name: firework's spark @- - pattern: (firework particle|firework['s] spark) - - fishing: # added in 1.20.5 - name: water wake @- - pattern: (fishing|water wake) - water_wake: # for versions below 1.20.5 - name: water wake @- - pattern: (fishing|water wake) - - flame: - name: flame @- - pattern: (flame|fire) - - flash: # added in 1.14 - name: flash @- - pattern: flash - - glow: # added in 1.17 - name: glow @- - pattern: glow - - glow_squid_ink: # added in 1.17 - name: glow squid ink @- - pattern: glow squid ink - - gust: # added in 1.20.5 (for 1.21) - name: gust @a - pattern: gust - - gust_emitter_large: # added in 1.20.5 (for 1.21) - name: large gust emitter @a - pattern: large gust emitter - - gust_emitter_small: # added in 1.20.5 (for 1.21) - name: small gust emitter @a - pattern: small gust emitter - - happy_villager: # added in 1.20.5 - name: happy villager @- - pattern: (happy villager|green spark) - villager_happy: - name: happy villager @- - pattern: (happy villager|green spark) - - heart: - name: heart @a - pattern: heart - - infested: # added in 1.20.5 (for 1.21) - name: infested @- - pattern: infested - - instant_effect: # added in 1.20.5 - name: instant effect @an - pattern: instant (effect|spell|thrown potion) - spell_instant: # for versions below 1.20.5 - name: spell @a - pattern: instant (effect|spell|thrown potion) - - item: # added in 1.20.5 - name: item @- - pattern: %itemtype% item (break|crack)[ing] - item_crack: # for versions below 1.20.5 - name: item crack @- - pattern: %itemtype% item (break|crack)[ing] - - item_cobweb: # added in 1.20.5 (for 1.21) - name: cobweb @- - pattern: cobweb [item|particle] - - item_slime: # added in 1.20.5 - name: slime @- - pattern: slime [item|particle] - slime: # for versions below 1.20.5 - name: slime @- - pattern: slime [item|particle] - - item_snowball: # added in 1.20.5 - name: snowball @- - pattern: (snowball [item|break|particle]|snow shovel|snow(man| golem) spawn) - snowball: # for versions below 1.20.5 - name: snowball break @- - pattern: snowball break - snow_shovel: # for versions below 1.20.5 - name: snow shovel @- - pattern: (snowball [item|particle]|snow shovel|snow(man| golem) spawn) - - landing_honey: # added in 1.15 - name: landing honey @- - pattern: landing honey - - landing_lava: # added in 1.14 - name: landing lava @- - pattern: landing lava - - landing_obsidian_tear: # added in 1.16 - name: landing obsidian tear @- - pattern: landing obsidian tear - - large_smoke: # added in 1.20.5 - name: large smoke @- - pattern: large smoke - smoke_large: # for versions below 1.20.5 - name: large smoke @- - pattern: large smoke - - lava: - name: lava pop @a - pattern: lava pop - - mycelium: # previously town_aura, changed in 1.20.5 - name: mycelium @- - pattern: (mycelium [particle]|small smoke|town aura) - town_aura: - name: small smoke @- - pattern: (mycelium|small smoke|town aura) - - nautilus: # added in 1.13 - name: nautilus @- - pattern: nautilus - - note: - name: note @a - pattern: note [(of|with) [colo[u]r] %number%] - - ominous_spawning: # added in 1.20.5 (for 1.21) - name: ominous spawning @- - pattern: ominous spawning - - poof: # added in 1.20.5 - name: poof @a - pattern: (poof|explosion) - explosion_normal: # for versions below 1.20.5 - name: explosion @an - pattern: (poof|explosion) - - portal: - name: portal @a - pattern: [nether] portal - - raid_omen: # added in 1.20.5 (1.21) - name: raid omen @a - pattern: raid omen - - rain: # added in 1.20.5 - name: rain @- - pattern: (rain|water drop) - water_drop: # for versions below 1.20.5 - name: water drop @a - pattern: (rain|water drop) - - reverse_portal: # added in 1.16 - name: reverse portal @- - pattern: reverse portal - - scrape: # added in 1.17 - name: scrape @- - pattern: scrape - - sculk_charge: # added in 1.19 - name: sculk charge @- - pattern: sculk charge [with ([a] roll|[an] angle) [of] %-float%] - - sculk_charge_pop: # added in 1.19 - name: sculk charge pop @- - pattern: sculk charge pop - - sculk_soul: # added in 1.19 - name: sculk soul @- - pattern: sculk soul - - shriek: # added in 1.19 - name: shriek @- - pattern: shriek [with [a] delay [of] %-timespan%] - - small_flame: # added in 1.17 - name: small flame @a - pattern: small flame - - small_gust: # added in 1.20.5 (for 1.21) - name: small gust @a - pattern: small gust - - smoke: # added in 1.20.5 - name: smoke @- - pattern: smoke [particle] - smoke_normal: # for versions below 1.20.5 - name: smoke @- - pattern: smoke [particle] - - sneeze: # added in 1.14 - name: sneeze @a - pattern: sneeze - - snowflake: # added in 1.17 - name: snowflake @a - pattern: snowflake - - sonic_boom: # added in 1.19 - name: sonic boom @a - pattern: sonic boom - - soul: # added in 1.16 - name: soul @- - pattern: soul - - soul_fire_flame: # added in 1.16 - name: soul fire flame @a - pattern: soul fire flame - - spit: # added in 1.11 - name: spit @- - pattern: spit - - splash: # added in 1.20.5 - name: splash @a - pattern: [water] splash - water_splash: # for versions below 1.20.5 - name: water splash @a - pattern: [water] splash - - spore_blossom_air: # added in 1.17 - name: spore blossom air @- - pattern: spore blossom air - - squid_ink: # added in 1.13 - name: squid ink @- - pattern: squid ink - - sweep_attack: # added in 1.14 - name: sweep attack @a - pattern: sweep attack - - totem_of_undying: # added in 1.20.5 - name: totem of undying @a - pattern: totem [of undying] [particle] - totem: # for versions below 1.20.5 - name: totem @a - pattern: totem [of undying] [particle] - - trial omen: # added in 1.20.5 (for 1.21) - name: trial omen @a - pattern: trial omen - - trial_spawner_detection: # added in 1.20.5 (for 1.21) - name: trial spawner detection @- - pattern: trial spawner detection - - trial_spawner_detection_ominous: # added in 1.20.5 (for 1.21) - name: ominous trial spawner detection @- - pattern: ominous trial spawner detection - - underwater: # added in 1.20.5 - name: underwater @- - pattern: (underwater|suspended|void fog) - suspended: # for versions below 1.20.5 - name: suspended @- - pattern: (underwater|suspended) - suspended_depth: # for versions below 1.20.5 - name: void fog @- - pattern: void fog - - vault_connection: # added in 1.20.5 (for 1.21) - name: vault connection @- - pattern: vault connection - - vibration: # added in 1.17 - name: vibration @a - # must be a literal, so you can't actually use this properly yet - pattern: vibration [to %-entity/location%] [over %-timespan%] - - warped_spore: # added in 1.16 - name: warped spore @a - pattern: warped spore - - wax_off: # added in 1.17 - name: wax off @- - pattern: wax off - - wax_on: # added in 1.17 - name: wax on @- - pattern: wax on - - white_ash: # added in 1.16 - name: white ash @- - pattern: white ash - - white_smoke: # added in 1.20.3 - name: white smoke @- - pattern: white smoke - - witch: # added in 1.20.5 - name: witch @a - pattern: (witch (magic|spell|particle)|purple spark) - spell_witch: # for versions below 1.20.5 - name: witch spell @a - pattern: (witch (magic|spell|particle)|purple spark) - # -- Inventory Actions -- inventory actions: nothing: nothing, do nothing @@ -2973,121 +2341,121 @@ entity effect: # -- Particle Effects -- # all patterns should include the word 'particle' particle: - angry_villager: angry villager particle - ash: ash particle - block: block particle - block_crumble: block crumble particle - block_marker: block marker particle - bubble: bubble particle - bubble_column_up: bubble column particle - bubble_pop: bubble pop particle - campfire_cosy_smoke: campfire cosy smoke particle - campfire_signal_smoke: campfire signal smoke particle - cherry_leaves: cherry leaves particle - cloud: cloud particle - composter: composter particle - copper_fire_flame: copper flame particle - crimson_spore: crimson spore particle - crit: crit particle - current_down: downward current particle - damage_indicator: damage indicator particle - dolphin: dolphin particle - dragon_breath: dragon breath particle - dripping_dripstone_lava: dripstone dripping lava particle - dripping_dripstone_water: dripstone dripping water particle - dripping_honey: dripping honey particle - dripping_lava: dripping lava particle - dripping_obsidian_tear: dripping obsidian tear particle - dripping_water: dripping water particle - dust: dust particle - dust_color_transition: dust color transition particle - dust_pillar: dust pillar particle - dust_plume: dust plume particle - effect: potion effect particle - egg_crack: egg crack particle - elder_guardian: elder guardian particle - electric_spark: electric spark particle - enchant: enchanting particle - enchanted_hit: enchanted hit particle - end_rod: end rod particle - entity_effect: entity effect particle - explosion: explosion particle - explosion_emitter: explosion emitter particle - falling_dripstone_lava: falling dripstone lava particle - falling_dripstone_water: falling dripstone water particle - falling_dust: falling dust particle - falling_honey: falling honey particle - falling_lava: falling lava particle - falling_nectar: falling nectar particle - falling_obsidian_tear: falling obsidian tear particle - falling_spore_blossom: falling spore blossom particle - falling_water: falling water particle - firefly: firefly particle - firework: firework particle - fishing: fishing particle - flame: flame particle - flash: flash particle - glow: glow particle - glow_squid_ink: glow squid ink particle - gust: gust particle - gust_emitter_large: large gust emitter particle - gust_emitter_small: small gust emitter particle - happy_villager: happy villager particle - heart: heart particle - infested: infested particle - instant_effect: instant effect particle - item: item particle - item_cobweb: cobweb item particle - item_slime: slime item particle - item_snowball: snowball item particle - landing_honey: landing honey particle - landing_lava: landing lava particle - landing_obsidian_tear: landing obsidian tear particle - large_smoke: large smoke particle - lava: lava particle - mycelium: mycelium particle - nautilus: nautilus particle - note: note particle - ominous_spawning: ominous spawning particle - pale_oak_leaves: pale oak leaves particle - poof: poof particle - portal: portal particle - raid_omen: raid omen particle - rain: rain particle - reverse_portal: reverse portal particle - scrape: scrape particle - sculk_charge: sculk charge particle - sculk_charge_pop: sculk charge pop particle - sculk_soul: sculk soul particle - shriek: shriek particle - small_flame: small flame particle - small_gust: small gust particle - smoke: smoke particle - sneeze: sneeze particle - snowflake: snowflake particle - sonic_boom: sonic boom particle - soul: soul particle - soul_fire_flame: soul fire flame particle - spit: spit particle - splash: splash particle - spore_blossom_air: spore blossom air particle - squid_ink: squid ink particle - sweep_attack: sweep attack particle - tinted_leaves: tinted leaves particle - totem_of_undying: totem of undying particle - trail: trail particle - trial_omen: trial omen particle - trial_spawner_detection: trial spawner detection particle - trial_spawner_detection_ominous: ominous trial spawner detection particle - underwater: underwater particle - vault_connection: vault connection particle - vibration: vibration particle - warped_spore: warped spore particle - wax_off: wax off particle - wax_on: wax on particle - white_ash: white ash particle - white_smoke: white smoke particle - witch: witch particle + angry_villager: angry villager particle¦s @an + ash: ash particle¦s @an + block: block particle¦s @a + block_crumble: block crumble particle¦s @a + block_marker: block marker particle¦s @a + bubble: bubble particle¦s @a + bubble_column_up: bubble column particle¦s @a + bubble_pop: bubble pop particle¦s @a + campfire_cosy_smoke: campfire cosy smoke particle¦s @a + campfire_signal_smoke: campfire signal smoke particle¦s @a + cherry_leaves: cherry leaves particle¦s @a + cloud: cloud particle¦s @a + composter: composter particle¦s @a + copper_fire_flame: copper flame particle¦s @a + crimson_spore: crimson spore particle¦s @a + crit: crit particle¦s @a + current_down: downward current particle¦s @a + damage_indicator: damage indicator particle¦s @a + dolphin: dolphin particle¦s @a + dragon_breath: dragon breath particle¦s @a + dripping_dripstone_lava: dripstone dripping lava particle¦s @a + dripping_dripstone_water: dripstone dripping water particle¦s @a + dripping_honey: dripping honey particle¦s @a + dripping_lava: dripping lava particle¦s @a + dripping_obsidian_tear: dripping obsidian tear particle¦s @a + dripping_water: dripping water particle¦s @a + dust: dust particle¦s @a + dust_color_transition: dust color transition particle¦s @a + dust_pillar: dust pillar particle¦s @a + dust_plume: dust plume particle¦s @a + effect: potion effect particle¦s @an + egg_crack: egg crack particle¦s @an + elder_guardian: elder guardian particle¦s @an + electric_spark: electric spark particle¦s @an + enchant: enchanting particle¦s @an + enchanted_hit: enchanted hit particle¦s @an + end_rod: end rod particle¦s @an + entity_effect: entity effect particle¦s @an + explosion: explosion particle¦s @an + explosion_emitter: explosion emitter particle¦s @an + falling_dripstone_lava: falling dripstone lava particle¦s @a + falling_dripstone_water: falling dripstone water particle¦s @a + falling_dust: falling dust particle¦s @a + falling_honey: falling honey particle¦s @a + falling_lava: falling lava particle¦s @a + falling_nectar: falling nectar particle¦s @a + falling_obsidian_tear: falling obsidian tear particle¦s @a + falling_spore_blossom: falling spore blossom particle¦s @a + falling_water: falling water particle¦s @a + firefly: firefly particle¦s @a + firework: firework particle¦s @a + fishing: fishing particle¦s @a + flame: flame particle¦s @a + flash: flash particle¦s @a + glow: glow particle¦s @a + glow_squid_ink: glow squid ink particle¦s @a + gust: gust particle¦s @a + gust_emitter_large: large gust emitter particle¦s @a + gust_emitter_small: small gust emitter particle¦s @a + happy_villager: happy villager particle¦s @a + heart: heart particle¦s @a + infested: infested particle¦s @an + instant_effect: instant effect particle¦s @an + item: item particle¦s @an + item_cobweb: cobweb item particle¦s @an + item_slime: slime item particle¦s @an + item_snowball: snowball item particle¦s @an + landing_honey: landing honey particle¦s @a + landing_lava: landing lava particle¦s @a + landing_obsidian_tear: landing obsidian tear particle¦s @a + large_smoke: large smoke particle¦s @a + lava: lava particle¦s @a + mycelium: mycelium particle¦s @a + nautilus: nautilus particle¦s @a + note: note particle¦s @a + ominous_spawning: ominous spawning particle¦s @an + pale_oak_leaves: pale oak leaves particle¦s @a + poof: poof particle¦s @a + portal: portal particle¦s @a + raid_omen: raid omen particle¦s @a + rain: rain particle¦s @a + reverse_portal: reverse portal particle¦s @a + scrape: scrape particle¦s @a + sculk_charge: sculk charge particle¦s @a + sculk_charge_pop: sculk charge pop particle¦s @a + sculk_soul: sculk soul particle¦s @a + shriek: shriek particle¦s @a + small_flame: small flame particle¦s @a + small_gust: small gust particle¦s @a + smoke: smoke particle¦s @a + sneeze: sneeze particle¦s @a + snowflake: snowflake particle¦s @a + sonic_boom: sonic boom particle¦s @a + soul: soul particle¦s @a + soul_fire_flame: soul fire flame particle¦s @a + spit: spit particle¦s @a + splash: splash particle¦s @a + spore_blossom_air: spore blossom air particle¦s @a + squid_ink: squid ink particle¦s @a + sweep_attack: sweep attack particle¦s @a + tinted_leaves: tinted leaves particle¦s @a + totem_of_undying: totem of undying particle¦s @a + trail: trail particle¦s @a + trial_omen: trial omen particle¦s @a + trial_spawner_detection: trial spawner detection particle¦s @a + trial_spawner_detection_ominous: ominous trial spawner detection particle¦s @a + underwater: underwater particle¦s @a + vault_connection: vault connection particle¦s @a + vibration: vibration particle¦s @a + warped_spore: warped spore particle¦s @a + wax_off: wax off particle¦s @a + wax_on: wax on particle¦s @a + white_ash: white ash particle¦s @a + white_smoke: white smoke particle¦s @a + witch: witch particle¦s @a # -- Teleport Flags -- @@ -3492,7 +2860,6 @@ types: experience: experience point¦s @an experience.pattern: (e?xp|experience( points?)?) classinfo: type¦s @a - visualeffect: visual effect¦s @a queue: queue¦s @a script: script¦s @a config: config¦s @a From 9e1cd2623467088d5c965535452b305c0421d9d2 Mon Sep 17 00:00:00 2001 From: sovdee <10354869+sovdeeth@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:48:17 -0800 Subject: [PATCH 16/16] remove 'show' pattern conflict --- .../bukkit/particles/elements/effects/EffPlayEffect.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java index 51cea64e248..1c8faf82e88 100644 --- a/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -40,12 +40,12 @@ @Example("draw 2 smoke particles at player") @Example("force draw 10 red dust particles of size 3 for player") @Example("play blue instant splash potion break effect with a view radius of 10") -@Example("show ravager attack animation on player's target") +@Example("play ravager attack animation on player's target") public class EffPlayEffect extends Effect { static { Skript.registerEffect(EffPlayEffect.class, "[:force] (play|show|draw) %gameeffects/particles% [%-directions% %locations%] [as %-player%]", - "[:force] (play|show|draw) %gameeffects/particles% [%-directions% %locations%] (for|to) %-players% [as %-player%]", + "[:force] (play|draw) %gameeffects/particles% [%-directions% %locations%] (for|to) %-players% [as %-player%]", // show is omitted to avoid conflicts with EffOpenInv "(play|show|draw) %gameeffects% [%-directions% %locations%] (in|with) [a] [view] (radius|range) [of] %number%", "(play|show|draw) %entityeffects% on %entities%"); }