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 diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index e38c523eb62..1e0cdaab360 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -101,6 +101,7 @@ import org.skriptlang.skript.bukkit.itemcomponents.ItemComponentModule; 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; @@ -444,7 +445,7 @@ public void onEnable() { if (!aliasesFolder.mkdirs()) throw new IOException("Could not create the directory " + aliasesFolder); } - + f = new ZipFile(getFile()); for (ZipEntry e : new EnumerationIterable(f.entries())) { if (e.isDirectory()) @@ -599,7 +600,8 @@ public void onEnable() { 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/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/ch/njol/skript/entity/SimpleEntityData.java b/src/main/java/ch/njol/skript/entity/SimpleEntityData.java index d723321dca0..dfeb9fa17d9 100644 --- a/src/main/java/ch/njol/skript/entity/SimpleEntityData.java +++ b/src/main/java/ch/njol/skript/entity/SimpleEntityData.java @@ -252,6 +252,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/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/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 f2b09d9267a..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)); } @@ -264,6 +264,48 @@ 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; + } + + /** + * 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/ch/njol/util/VectorMath.java b/src/main/java/ch/njol/util/VectorMath.java index 000be2af7bf..a7ba155c348 100644 --- a/src/main/java/ch/njol/util/VectorMath.java +++ b/src/main/java/ch/njol/util/VectorMath.java @@ -106,6 +106,9 @@ public static float wrapAngleDeg(float angle) { return ExprVectorFromYawAndPitch.wrapAngleDeg(angle); } + /** + * Copies vector components of {@code vector2} into {@code vector1}. + */ public static void copyVector(Vector vector1, Vector vector2) { vector1.copy(vector2); } 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/GameEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java new file mode 100644 index 00000000000..91b5a0af45f --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/GameEffect.java @@ -0,0 +1,142 @@ +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 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; +import java.util.Locale; + +/** + * A class to hold metadata about {@link org.bukkit.Effect}s before playing. + */ +public class GameEffect { + + public static final EnumParser ENUM_UTILS = new EnumParser<>(Effect.class, "game effect"); // exclude effects that require data + + /** + * The {@link Effect} that this object represents + */ + private final Effect effect; + + /** + * The optional extra data that some {@link Effect}s require. + */ + 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) + 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); + } + + /** + * The backing {@link Effect}. + * @return the effect + */ + public Effect getEffect() { + return effect; + } + + /** + * 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; + return true; + } else if (effect == Effect.ELECTRIC_SPARK && data == null) { + // ELECTRIC_SPARK effect can have null data + this.data = null; + return true; + } + 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) { + if (effect.getData() != null && data == null) + return; + 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) { + return ENUM_UTILS.toString(getEffect(), flags); + } + + @Override + 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 new file mode 100644 index 00000000000..3749cb6ca5e --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleModule.java @@ -0,0 +1,431 @@ +package org.skriptlang.skript.bukkit.particles; + +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; +import ch.njol.skript.lang.ParseContext; +import ch.njol.skript.registrations.Classes; +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; +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.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +/** + * Module for particle and game effect related classes and elements. + */ +public class ParticleModule implements AddonModule { + + @Override + public void load(SkriptAddon addon) { + registerClasses(); + registerDataSerializers(); + DataGameEffects.getGameEffectInfos(); + DataParticles.getParticleInfos(); + + // load 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") + .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()) + .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) { + 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) { + assert false; + } + + @Override + protected GameEffect deserialize(Fields fields) throws StreamCorruptedException { + 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 + 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(); + } + })); + + // 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 + + // 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") + .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); + } + })); + + 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 + 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.particle().name(); + } + })); + + Classes.registerClass(new ClassInfo<>(ConvergingEffect.class, "convergingparticle") + .user("converging ?particle( ?effect)?s?") + .since("INSERT VERSION") + .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))); + + 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))); + + 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))); + } + + /** + * 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 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 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 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); + } + }); + + if (Skript.isRunningMinecraft(1, 21, 9)) { + Variables.yggdrasil.registerClassResolver(new 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); + } + }); + } + + if (Skript.isRunningMinecraft(1, 21, 4)) { + Variables.yggdrasil.registerClassResolver(new 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); + } + }); + } 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 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); + } + // + } + } + + /** + * Serializer for ParticleEffect. + * Does not store receivers/locations, only the particle effect data. + */ + 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..90db38d6114 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/ParticleUtils.java @@ -0,0 +1,213 @@ +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; + +/** + * Utility class for working with Bukkit Particles and their behaviors. + * Currently used for categorizing particles based on shared characteristics. + */ +@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; + + /** + * 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); + 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 new file mode 100644 index 00000000000..328b5e50ece --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/effects/EffPlayEffect.java @@ -0,0 +1,155 @@ +package org.skriptlang.skript.bukkit.particles.elements.effects; + +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; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.registrations.Classes; +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.NotNull; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.GameEffect; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; + +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) %entityeffects% on %entities%"); + } + + private Expression toDraw; + private @Nullable Expression locations; + private @Nullable Expression toPlayers; + private @Nullable Expression asPlayer; + private @Nullable Expression radius; + private boolean force; + + // for entity effects + private @Nullable Expression entities; + + private Node node; + + @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]); + 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]); + this.radius = (Expression) expressions[3]; + } + case 3 -> this.entities = (Expression) expressions[1]; + } + this.node = getParser().getNode(); + 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); + drawEntityEffects(effects, entities); + 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; + Player asPlayer = this.asPlayer != null ? this.asPlayer.getSingle(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) { + 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); + } + } + } + + /** + * 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) { + //noinspection DataFlowIssue + return new SyntaxStringBuilder(event, debug) + .append("play", toDraw) + .appendIf(locations != null, locations) + .appendIf(toPlayers != null, "for", toPlayers) + .toString(); + } + + @Override + public Node getNode() { + return node; + } +} 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 new file mode 100644 index 00000000000..9e287a40742 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprGameEffectWithData.java @@ -0,0 +1,82 @@ +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.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; +import org.bukkit.Effect; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.GameEffect; +import org.skriptlang.skript.bukkit.particles.registration.DataGameEffects; +import org.skriptlang.skript.bukkit.particles.registration.EffectInfo; + +public class ExprGameEffectWithData extends SimpleExpression { + + private static final Patterns> PATTERNS; + + static { + // create Patterns object + Object[][] patterns = new Object[DataGameEffects.getGameEffectInfos().size()][2]; + int i = 0; + for (var gameEffectInfo : DataGameEffects.getGameEffectInfos()) { + patterns[i][0] = gameEffectInfo.pattern(); + patterns[i][1] = gameEffectInfo; + i++; + } + PATTERNS = new Patterns<>(patterns); + + Skript.registerExpression(ExprGameEffectWithData.class, GameEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); + } + + private EffectInfo gameEffectInfo; + private Expression[] expressions; + private ParseResult parseResult; + + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + gameEffectInfo = PATTERNS.getInfo(matchedPattern); + this.expressions = expressions; + this.parseResult = parseResult; + return true; + } + + @Override + protected GameEffect @Nullable [] get(Event event) { + GameEffect gameEffect = new GameEffect(gameEffectInfo.effect()); + Object data = gameEffectInfo.dataSupplier().getData(event, expressions, parseResult); + + 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) { + error("Could not obtain required data for " + gameEffect); + return new GameEffect[0]; + } + return new GameEffect[]{gameEffect}; + } + + @Override + public boolean isSingle() { + return true; + } + + @Override + public Class getReturnType() { + return GameEffect.class; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return gameEffectInfo.toStringFunction() + .toString(expressions, parseResult, new SyntaxStringBuilder(event, debug)).toString(); + } + +} 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/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..cd438051c73 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleScale.java @@ -0,0 +1,75 @@ +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) { + if (from.hasScale()) + return from.scale(); + return null; + } + + @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) { + if (!effect.hasScale()) // don't set scale if it doesn't have one + continue; + effect.scale(effect.scale() + scaleDelta); + } + break; + case SET: + for (ScalableEffect effect : scalableEffect) + effect.scale(scaleDelta); + break; + case RESET: + for (ScalableEffect effect : scalableEffect) { + if (!effect.hasScale()) // don't reset scale if it doesn't have one + continue; + effect.scale(1.0); + } + 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 new file mode 100644 index 00000000000..97b5d93c930 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/elements/expressions/ExprParticleWithData.java @@ -0,0 +1,75 @@ +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.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; +import org.bukkit.Particle; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.particles.particleeffects.ParticleEffect; +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; + + static { + // create Patterns object + Object[][] patterns = new Object[DataParticles.getParticleInfos().size()][2]; + int i = 0; + for (var particleInfo : DataParticles.getParticleInfos()) { + patterns[i][0] = particleInfo.pattern(); + patterns[i][1] = particleInfo; + i++; + } + PATTERNS = new Patterns<>(patterns); + + Skript.registerExpression(ExprParticleWithData.class, ParticleEffect.class, ExpressionType.COMBINED, PATTERNS.getPatterns()); + } + + private ParseResult parseResult; + private Expression[] expressions; + private EffectInfo effectInfo; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + this.parseResult = parseResult; + this.expressions = expressions; + effectInfo = PATTERNS.getInfo(matchedPattern); + return effectInfo != null; + } + + @Override + protected ParticleEffect @Nullable [] get(Event event) { + Object data = effectInfo.dataSupplier().getData(event, expressions, parseResult); + if (data == null) { + error("Could not obtain required data for " + ParticleEffect.toString(effectInfo.effect(), 0)); + return null; + } + ParticleEffect effect = ParticleEffect.of(effectInfo.effect()); + effect.data(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 effectInfo.toStringFunction().toString(expressions, parseResult, new SyntaxStringBuilder(event, debug)).toString(); + } + +} 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..b63fa9b0b35 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ConvergingEffect.java @@ -0,0 +1,32 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + +import org.bukkit.Particle; +import org.jetbrains.annotations.ApiStatus; +import org.skriptlang.skript.bukkit.particles.ParticleUtils; + +/** + * A particle effect where particles converge towards a point. + * Currently used only for `all converging particles` + * @see ParticleUtils#isConverging(Particle) + */ +public class ConvergingEffect extends ParticleEffect { + + /** + * Internal constructor. + * Use {@link ParticleEffect#of(Particle)} instead. + * @param particle The particle type + */ + @ApiStatus.Internal + public ConvergingEffect(Particle particle) { + super(particle); + } + + /** + * @return a copy of this converging effect + */ + @Override + public ConvergingEffect copy() { + return (ConvergingEffect) super.copy(); + } + +} 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..2587456cc08 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/DirectionalEffect.java @@ -0,0 +1,63 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + + +import org.bukkit.Particle; +import org.jetbrains.annotations.ApiStatus; +import org.joml.Vector3d; + +/** + * A particle effect where particles have a specific direction of travel/velocity. + * Allows getting and setting the velocity of the particles. Setting the velocity will set the count to 0. + * Velocity is only applied if the count is 0. + */ +public class DirectionalEffect extends ParticleEffect { + + /** + * Internal constructor. + * Use {@link ParticleEffect#of(Particle)} instead. + * @param particle The particle type + */ + @ApiStatus.Internal + public DirectionalEffect(Particle particle) { + super(particle); + } + + /** + * Checks if the effect will use the offset as velocity. + * Velocity is only applied if the count is 0. + * @return true if the effect will use the offset as velocity, false otherwise + */ + public boolean hasVelocity() { + return count() == 0; + } + + /** + * Alias for {@link #offset()} when the effect is directional. + * Prefer using this method when dealing with directional effects that have count = 0. + * @return the velocity vector + */ + public Vector3d velocity() { + return offset(); + } + + /** + * Sets the velocity of the particles by setting the offset and count. + * This will set the count to 0 to ensure the velocity is applied. + * @param velocity the velocity vector + * @return this effect for chaining + */ + public DirectionalEffect velocity(Vector3d velocity) { + count(0); + offset(velocity); + return this; + } + + /** + * @return a copy of this directional effect + */ + @Override + public DirectionalEffect copy() { + return (DirectionalEffect) super.copy(); + } + +} 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..75eeda51375 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ParticleEffect.java @@ -0,0 +1,366 @@ +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.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 { + + /** + * Creates the appropriate ParticleEffect subclass based on the particle type. + * @param particle The particle type + * @return The appropriate ParticleEffect instance + */ + @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); + } + + /** + * 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(); + + /** + * 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; + 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); + } + + /** + * 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); + } + + /** + * 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 + + /** + * Internal constructor. + * Use {@link ParticleEffect#of(Particle)} instead. + * @param particle The particle type + */ + 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(); + } + + /** + * 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()); + } + + /** + * 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()); + } + + /** + * 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()); + } + + /** + * 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 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); + } + 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); + } + + /** + * 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(); + } + + @Override + public String toString() { + return toString(null, false); + } + + @Override + 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() { + 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/ScalableEffect.java b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java new file mode 100644 index 00000000000..1ac2d0e2b57 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/particleeffects/ScalableEffect.java @@ -0,0 +1,102 @@ +package org.skriptlang.skript.bukkit.particles.particleeffects; + +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; + + /** + * Enum representing different scaling functions for scalable particles. + */ + 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); + } + } + + /** + * 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; + case EXPLOSION -> ScalingFunction.EXPLOSION; + default -> throw new IllegalArgumentException("Particle " + particle.name() + " is not a scalable effect."); + }; + } + + /** + * Internal constructor. + * Use {@link ParticleEffect#of(Particle)} instead. + * @param particle The particle type + */ + @ApiStatus.Internal + public ScalableEffect(Particle particle) { + super(particle); + this.scale = 1.0f; + 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); + offset(scalingFunction.apply(scale), 0, 0); + 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 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 new file mode 100644 index 00000000000..364dedd074d --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataGameEffects.java @@ -0,0 +1,194 @@ +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 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; +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(); + } + 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) { + return info.toStringFunction().toString(exprs, parseResult, new SyntaxStringBuilder(event, debug)).toString(); + } + } + 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%", + // + (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, + (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::getCartesianBlockFaceData, + (exprs, parseResult, builder) -> builder.append("white smoke effect in direction", exprs[0])); + + 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, + (exprs, parseResult, builder) -> builder.append(exprs[0], "splash potion break effect")); + + registerEffect(Effect.INSTANT_POTION_BREAK, "%color% instant [splash] potion break effect", + DataSupplier::getColorData, + (exprs, parseResult, builder) -> builder.append(exprs[0], "instant splash potion break 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, + (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::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::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]", + (event, expressions, parseResult) -> (parseResult.mark == 0 ? null : Axis.values()[parseResult.mark - 1]), + (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 %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, + (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::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::getNumberDefault1, + (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] [mob] effect", + DataSupplier::isOminous, + (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", + DataSupplier::isOminous, + (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::getNumberDefault10, + (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, + (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, + (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"), + (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, + (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::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::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 new file mode 100644 index 00000000000..7ae93010cf4 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataParticles.java @@ -0,0 +1,317 @@ +package org.skriptlang.skript.bukkit.particles.registration; + +import ch.njol.skript.Skript; +import ch.njol.skript.aliases.ItemType; +import ch.njol.skript.util.ColorRGB; +import ch.njol.skript.util.Timespan; +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; +import java.util.ArrayList; +import java.util.Collections; +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) + 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); + } + + /** + * 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)); + } + + /** + * @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 + + 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(), + (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, + 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%]", + // + (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()); + }, // + (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%", + // + (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()); + }, // + (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, + (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]", + 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, + (exprs, parseResult, builder) -> builder.append(exprs[0], "block marker particle")); + + registerParticle(Particle.DUST_PILLAR, "[a[n]] %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]", + DataSupplier::getBlockData, + (exprs, parseResult, builder) -> builder.append("falling", exprs[0], "dust particle")); + + // 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(); + }, // + (exprs, parseResult, builder) -> builder.append(exprs[0], "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()); + }, // + (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, + 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%]", + // + (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); + }, // + (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 new file mode 100644 index 00000000000..450381db9f8 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/DataSupplier.java @@ -0,0 +1,153 @@ +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; + +/** + * 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 { + /** + * 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 + // + + /** + * 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)) + return null; + 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)) + return null; + 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)) + return null; + 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) + return itemType.getMaterial().createBlockData(); + if (input instanceof BlockData blockData) + return blockData; + 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)) + return null; + 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"); + } + + /** + * 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); + if (!(input instanceof Number number)) + return 10; + return number.intValue(); + } + + /** + * 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); + 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..ecaa4c605de --- /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..e5de2f6a573 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/particles/registration/ToString.java @@ -0,0 +1,24 @@ +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.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 { + /** + * Converts the particle and provided data to a 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 + */ + 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 e9853b2c01a..d51105339e9 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] @@ -2756,6 +2759,337 @@ transform reasons: unknown: unknown infection: infection, villager infection +# -- Game Effects -- +# 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 + 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. + 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 + + # 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 + +# -- 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 animation + reset_spawner_minecart_delay: reset spawner minecart delay effect + + # death was superseded by entity_death 1.12.2 + death: death animation + entity_death: death 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 + ravager_roared: ravager roared effect + warden_attack: warden attack animation + zoglin_attack: zoglin attack animation + entity_attack: entity attack animation + + # 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 + 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 sound effect + firework_explode: firework explode animation + love_hearts: love hearts effect + squid_rotate: squid rotation reset + entity_poof: entity poof 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 + 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 + 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 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 + 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 (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' +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 -- teleport flags: retain_open_inventory: opened inventory, open inventory, inventory @@ -3133,6 +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/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..9f879abf65c --- /dev/null +++ b/src/test/skript/tests/syntaxes/effects/EffPlayEffect.sk @@ -0,0 +1,103 @@ +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) + +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 + + + 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 {_}