diff --git a/src/main/java/ch/njol/skript/Skript.java b/src/main/java/ch/njol/skript/Skript.java index e38c523eb62..597ae114a87 100644 --- a/src/main/java/ch/njol/skript/Skript.java +++ b/src/main/java/ch/njol/skript/Skript.java @@ -98,6 +98,7 @@ import org.skriptlang.skript.bukkit.fishing.FishingModule; import org.skriptlang.skript.bukkit.furnace.FurnaceModule; import org.skriptlang.skript.bukkit.input.InputModule; +import org.skriptlang.skript.bukkit.interactions.InteractionModule; import org.skriptlang.skript.bukkit.itemcomponents.ItemComponentModule; import org.skriptlang.skript.bukkit.log.runtime.BukkitRuntimeErrorConsumer; import org.skriptlang.skript.bukkit.loottables.LootTableModule; @@ -599,7 +600,8 @@ public void onEnable() { new DamageSourceModule(), new ItemComponentModule(), new BrewingModule(), - new CommonModule() + new CommonModule(), + new InteractionModule() ); } catch (final Exception e) { exception(e, "Could not load required .class files: " + e.getLocalizedMessage()); diff --git a/src/main/java/org/skriptlang/skript/bukkit/interactions/InteractionModule.java b/src/main/java/org/skriptlang/skript/bukkit/interactions/InteractionModule.java new file mode 100644 index 00000000000..0ab248f4338 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/interactions/InteractionModule.java @@ -0,0 +1,51 @@ +package org.skriptlang.skript.bukkit.interactions; + +import org.bukkit.entity.Interaction; +import org.bukkit.entity.Interaction.PreviousInteraction; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.addon.AddonModule; +import org.skriptlang.skript.addon.SkriptAddon; +import org.skriptlang.skript.bukkit.interactions.elements.conditions.CondIsResponsive; +import org.skriptlang.skript.bukkit.interactions.elements.effects.EffMakeResponsive; +import org.skriptlang.skript.bukkit.interactions.elements.expressions.ExprInteractionDimensions; +import org.skriptlang.skript.bukkit.interactions.elements.expressions.ExprLastInteractionDate; +import org.skriptlang.skript.bukkit.interactions.elements.expressions.ExprLastInteractionPlayer; +import org.skriptlang.skript.registration.SyntaxRegistry; + +public class InteractionModule implements AddonModule { + + @Override + public void load(SkriptAddon addon) { + SyntaxRegistry registry = addon.syntaxRegistry(); + CondIsResponsive.register(registry); + EffMakeResponsive.register(registry); + ExprInteractionDimensions.register(registry); + ExprLastInteractionDate.register(registry); + ExprLastInteractionPlayer.register(registry); + } + + public enum InteractionType { + ATTACK, + INTERACT, + BOTH + } + + /** + * Useful helper to get the latest {@link PreviousInteraction} of an {@link Interaction}. + * @param interaction The interaction entity to check. + * @return The most recent {@link PreviousInteraction}, or null if no interactions have occurred. + */ + public static @Nullable PreviousInteraction getLatestInteraction(Interaction interaction) { + PreviousInteraction attack = interaction.getLastAttack(); + PreviousInteraction interact = interaction.getLastInteraction(); + if (attack == null) // no attacks, return last interact/null + return interact; + if (interact == null) // attack but no interact + return attack; + // both not null, compare + if (attack.getTimestamp() > interact.getTimestamp()) + return attack; + return interact; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/conditions/CondIsResponsive.java b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/conditions/CondIsResponsive.java new file mode 100644 index 00000000000..3ad23f95a64 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/conditions/CondIsResponsive.java @@ -0,0 +1,51 @@ +package org.skriptlang.skript.bukkit.interactions.elements.conditions; + +import ch.njol.skript.conditions.base.PropertyCondition; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Interaction; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Is Responsive") +@Description(""" + Checks whether an interaction is responsive or not. Responsiveness determines whether clicking the entity will cause \ + the clicker's arm to swing. + """) +@Example("if last spawned interaction is responsive:") +@Example("if last spawned interaction is unresponsive:") +@Since("INSERT VERSION") +public class CondIsResponsive extends PropertyCondition { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.CONDITION, + infoBuilder(CondIsResponsive.class, PropertyType.BE, "(responsive|:unresponsive)", "entities") + .supplier(CondIsResponsive::new) + .build()); + } + + private boolean responsive; + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + responsive = !parseResult.hasTag("unresponsive"); + return super.init(exprs, matchedPattern, isDelayed, parseResult); + } + @Override + public boolean check(Entity entity) { + if (entity instanceof Interaction interaction) { + return interaction.isResponsive() == responsive; + } + return false; + } + @Override + protected String getPropertyName() { + return responsive ? "responsive" : "unresponsive"; + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/effects/EffMakeResponsive.java b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/effects/EffMakeResponsive.java new file mode 100644 index 00000000000..c52ea895081 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/effects/EffMakeResponsive.java @@ -0,0 +1,70 @@ +package org.skriptlang.skript.bukkit.interactions.elements.effects; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.lang.Effect; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.util.Kleenean; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Interaction; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Make Interaction Responsive") +@Description(""" + Makes an interaction either responsive or unresponsive. This determines whether clicking the entity will cause \ + the clicker's arm to swing. + Interactions default to unresponsive. + """) +@Example("make last spawned interaction responsive") +@Since("INSERT VERSION") +public class EffMakeResponsive extends Effect { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EFFECT, + SyntaxInfo.builder(EffMakeResponsive.class) + .addPatterns( + "make %entities% responsive", + "make %entities% (not |un)responsive" + ) + .supplier(EffMakeResponsive::new) + .build() + ); + } + + private Expression interactions; + private boolean negated; + + @Override + @SuppressWarnings("unchecked") + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + interactions = (Expression) expressions[0]; + negated = matchedPattern == 1; + return true; + } + + @Override + protected void execute(Event event) { + for (Entity entity : interactions.getArray(event)) { + if (entity instanceof Interaction interaction) + interaction.setResponsive(!negated); + } + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("make", interactions) + .appendIf(negated, "not") + .append("responsive") + .toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprInteractionDimensions.java b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprInteractionDimensions.java new file mode 100644 index 00000000000..d86cb56686a --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprInteractionDimensions.java @@ -0,0 +1,103 @@ +package org.skriptlang.skript.bukkit.interactions.elements.expressions; + +import ch.njol.skript.classes.Changer.ChangeMode; +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.util.Kleenean; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Interaction; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Interaction Height/Width") +@Description(""" + Returns the height or width of an interaction entity's hitbox. Both default to 1. + The width of the hitbox determines the x/z widths + """) +@Example("set interaction height of last spawned interaction to 5.3") +@Example("set interaction width of last spawned interaction to 2") +@Since("INSERT VERSION") +public class ExprInteractionDimensions extends SimplePropertyExpression { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, + infoBuilder(ExprInteractionDimensions.class, Number.class, + "interaction (height|:width)[s]", "entities", + true) + .supplier(ExprInteractionDimensions::new) + .build()); + } + + private boolean width; + + @Override + public boolean init(Expression[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + width = parseResult.hasTag("width"); + return super.init(expressions, matchedPattern, isDelayed, parseResult); + } + + @Override + public @Nullable Number convert(Entity entity) { + if (entity instanceof Interaction interaction) + return width ? interaction.getInteractionWidth() : interaction.getInteractionHeight(); + return null; + } + + @Override + public Class @Nullable [] acceptChange(ChangeMode mode) { + return switch (mode) { + case ADD, REMOVE, SET, RESET -> new Class[]{ Number.class }; + default -> null; + }; + } + + @Override + public void change(Event event, Object @Nullable [] delta, ChangeMode mode) { + Entity[] entities = getExpr().getArray(event); + if (entities.length == 0) + return; + float deltaValue = delta == null ? 1.0f : ((Number) delta[0]).floatValue(); + for (Entity entity : entities) { + if (!(entity instanceof Interaction interaction)) + continue; + switch (mode) { + case REMOVE: + deltaValue = -deltaValue; + // fallthrough + case ADD: + if (width) { + interaction.setInteractionWidth(Math.max(interaction.getInteractionWidth() + deltaValue, 0)); + } else { + interaction.setInteractionHeight(Math.max(interaction.getInteractionHeight() + deltaValue, 0)); + } + break; + case SET, RESET: + deltaValue = Math.max(deltaValue, 0); + if (width) { + interaction.setInteractionWidth(deltaValue); + } else { + interaction.setInteractionHeight(deltaValue); + } + break; + } + } + } + + @Override + public Class getReturnType() { + return Number.class; + } + + @Override + protected String getPropertyName() { + return "interaction " + (width ? "width" : "height"); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprLastInteractionDate.java b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprLastInteractionDate.java new file mode 100644 index 00000000000..1eae1a37745 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprLastInteractionDate.java @@ -0,0 +1,90 @@ +package org.skriptlang.skript.bukkit.interactions.elements.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Examples; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.skript.util.Date; +import ch.njol.util.Kleenean; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Interaction; +import org.bukkit.entity.Interaction.PreviousInteraction; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.interactions.InteractionModule; +import org.skriptlang.skript.bukkit.interactions.InteractionModule.InteractionType; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Last Interaction Date") +@Description(""" + Returns the date of the last attack (left click), or interaction (right click) on an interaction entity + Using 'clicked on' will return the latest attack or interaction, whichever was more recent. + """) +@Examples("if the last time {_interaction} was clicked < 5 seconds ago") +@Since("INSERT VERSION") +public class ExprLastInteractionDate extends SimplePropertyExpression { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.builder(ExprLastInteractionDate.class, Date.class) + .addPatterns( + "[the] last (date|time)[s] [that|when] %entities% (were|was) (attacked|1:interacted with|2:clicked [on])" + ) + .supplier(ExprLastInteractionDate::new) + .build()); + } + + private InteractionType interactionType; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + interactionType = InteractionType.values()[parseResult.mark]; + return super.init(exprs, matchedPattern, isDelayed, parseResult); + } + + @Override + public @Nullable Date convert(Entity entity) { + if (entity instanceof Interaction interaction) { + PreviousInteraction lastInteraction = switch (interactionType) { + case ATTACK -> interaction.getLastAttack(); + case INTERACT -> interaction.getLastInteraction(); + case BOTH -> InteractionModule.getLatestInteraction(interaction); + }; + if (lastInteraction == null) + return null; + return new Date(lastInteraction.getTimestamp()); + } + return null; + } + + @Override + public Class getReturnType() { + return Date.class; + } + + @Override + protected String getPropertyName() { + return "UNUSED"; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("the last date that") + .append(getExpr()) + .append(getExpr().isSingle() ? "was" : "were") + .append(switch (interactionType) { + case ATTACK -> "attacked"; + case INTERACT -> "interacted with"; + case BOTH -> "clicked on"; + }) + .toString(); + } + +} diff --git a/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprLastInteractionPlayer.java b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprLastInteractionPlayer.java new file mode 100644 index 00000000000..ada43456120 --- /dev/null +++ b/src/main/java/org/skriptlang/skript/bukkit/interactions/elements/expressions/ExprLastInteractionPlayer.java @@ -0,0 +1,92 @@ +package org.skriptlang.skript.bukkit.interactions.elements.expressions; + +import ch.njol.skript.doc.Description; +import ch.njol.skript.doc.Example; +import ch.njol.skript.doc.Name; +import ch.njol.skript.doc.Since; +import ch.njol.skript.expressions.base.SimplePropertyExpression; +import ch.njol.skript.lang.Expression; +import ch.njol.skript.lang.SkriptParser.ParseResult; +import ch.njol.skript.lang.SyntaxStringBuilder; +import ch.njol.util.Kleenean; +import org.bukkit.OfflinePlayer; +import org.bukkit.entity.Entity; +import org.bukkit.entity.Interaction; +import org.bukkit.entity.Interaction.PreviousInteraction; +import org.bukkit.event.Event; +import org.jetbrains.annotations.Nullable; +import org.skriptlang.skript.bukkit.interactions.InteractionModule; +import org.skriptlang.skript.bukkit.interactions.InteractionModule.InteractionType; +import org.skriptlang.skript.registration.SyntaxInfo; +import org.skriptlang.skript.registration.SyntaxRegistry; + +@Name("Last Interaction Player") +@Description(""" + Returns the last player to attack (left click), or interact (right click) with an interaction entity. + If 'click on' or 'clicked on' are used, this will return the last player to either attack or interact with the entity \ + whichever was most recent. + """) +@Example("kill the last player that attacked the last spawned interaction") +@Example("feed the last player who interacted with {_i}") +@Since("INSERT VERSION") +public class ExprLastInteractionPlayer extends SimplePropertyExpression { + + public static void register(SyntaxRegistry registry) { + registry.register( + SyntaxRegistry.EXPRESSION, + SyntaxInfo.Expression.builder(ExprLastInteractionPlayer.class, OfflinePlayer.class) + .addPatterns( + "[the] last player[s] to (attack|1:interact with|2:click [on]) %entities%", + "[the] last player[s] (who|that) (attacked|1:interacted with|2:clicked [on]) %entities%" + ) + .supplier(ExprLastInteractionPlayer::new) + .build()); + } + + private InteractionType interactionType; + + @Override + public boolean init(Expression[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) { + interactionType = InteractionType.values()[parseResult.mark]; + return super.init(exprs, matchedPattern, isDelayed, parseResult); + } + + @Override + public @Nullable OfflinePlayer convert(Entity entity) { + if (entity instanceof Interaction interaction) { + PreviousInteraction lastInteraction = switch (interactionType) { + case ATTACK -> interaction.getLastAttack(); + case INTERACT -> interaction.getLastInteraction(); + case BOTH -> InteractionModule.getLatestInteraction(interaction); + }; + if (lastInteraction == null) + return null; + return lastInteraction.getPlayer(); + } + return null; + } + + @Override + public Class getReturnType() { + return OfflinePlayer.class; + } + + @Override + protected String getPropertyName() { + return "UNUSED"; + } + + @Override + public String toString(@Nullable Event event, boolean debug) { + return new SyntaxStringBuilder(event, debug) + .append("the last player to") + .append(switch (interactionType) { + case ATTACK -> "attack"; + case INTERACT -> "interact with"; + case BOTH -> "click on"; + }) + .append(getExpr()) + .toString(); + } + +} diff --git a/src/main/resources/lang/default.lang b/src/main/resources/lang/default.lang index e9853b2c01a..f32c3d74f5b 100644 --- a/src/main/resources/lang/default.lang +++ b/src/main/resources/lang/default.lang @@ -1557,7 +1557,7 @@ entities: pattern: sniffer[plural:s] interaction: name: interaction¦s - pattern: interaction[plural:s] + pattern: interaction([plural:s]| entit(plural:ies|y)) # 1.20.3 Entities breeze: diff --git a/src/test/skript/tests/general/InteractionEntities.sk b/src/test/skript/tests/general/InteractionEntities.sk new file mode 100644 index 00000000000..1904cfccb1b --- /dev/null +++ b/src/test/skript/tests/general/InteractionEntities.sk @@ -0,0 +1,63 @@ +test "interaction entities": + spawn an interaction at test-location: + assert interaction width of entity is 1 with "wrong default width" + assert interaction height of entity is 1 with "wrong default height" + + set interaction width of entity to 10 + assert interaction width of entity is 10 with "wrong width after set" + add 10 to interaction width of entity + assert interaction width of entity is 20 with "wrong width after add" + add -25 to interaction width of entity + assert interaction width of entity is 0 with "wrong width after -add" + remove -25 from interaction width of entity + assert interaction width of entity is 25 with "wrong width after -remove" + remove 10 from interaction width of entity + assert interaction width of entity is 15 with "wrong width after remove" + reset interaction width of entity + assert interaction width of entity is 1 with "wrong width after reset" + + set interaction height of entity to 10 + assert interaction height of entity is 10 with "wrong height after set" + add 10 to interaction height of entity + assert interaction height of entity is 20 with "wrong height after add" + add -25 to interaction height of entity + assert interaction height of entity is 0 with "wrong height after -add" + remove -25 from interaction height of entity + assert interaction height of entity is 25 with "wrong height after -remove" + remove 10 from interaction height of entity + assert interaction height of entity is 15 with "wrong height after remove" + reset interaction height of entity + assert interaction height of entity is 1 with "wrong height after reset" + + # test limits + + set interaction height of entity to positive infinity + assert interaction height of entity is positive infinity with "wrong max height" + set interaction width of entity to positive infinity + assert interaction width of entity is positive infinity with "wrong max width" + + # assert interactions are null + + assert last player to interact with entity is not set with "last interaction player set" + assert last player to attack entity is not set with "last attack player set" + assert last player to click entity is not set with "last click player set" + + assert last time that entity was interacted with is not set with "last interaction date set" + assert last time when entity was attacked is not set with "last attack date set" + assert last date that entity was clicked on is not set with "last click date set" + + # responsiveness + + assert entity is not responsive with "wrong default responsiveness" + make entity responsive + assert entity is responsive with "wrong responsiveness" + assert entity is not unresponsive with "wrong responsiveness" + make entity unresponsive + assert entity is not responsive with "wrong responsiveness" + assert entity is unresponsive with "wrong responsiveness" + make entity responsive + assert entity is responsive with "wrong responsiveness" + make entity not responsive + assert entity is not responsive with "wrong responsiveness" + + delete entity