From aa2d68f6df6d65e939c5a726dcace3d5c86e054c Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 8 Jun 2026 19:50:37 +0200 Subject: [PATCH 1/3] feat: SaveLocationSpell --- .../spells/targeted/SaveLocationSpell.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 core/src/main/java/com/nisovin/magicspells/spells/targeted/SaveLocationSpell.java diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SaveLocationSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SaveLocationSpell.java new file mode 100644 index 000000000..97a9f91bb --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SaveLocationSpell.java @@ -0,0 +1,68 @@ +package com.nisovin.magicspells.spells.targeted; + +import org.bukkit.Location; +import org.bukkit.entity.Player; + +import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.SpellData; +import com.nisovin.magicspells.util.CastResult; +import com.nisovin.magicspells.util.TargetInfo; +import com.nisovin.magicspells.util.MagicConfig; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spells.TargetedLocationSpell; +import com.nisovin.magicspells.util.managers.VariableManager; + +public class SaveLocationSpell extends TargetedSpell implements TargetedLocationSpell { + + private final ConfigData variableWorld; + + private final ConfigData variableX; + private final ConfigData variableY; + private final ConfigData variableZ; + + private final ConfigData variableYaw; + private final ConfigData variablePitch; + + public SaveLocationSpell(MagicConfig config, String spellName) { + super(config, spellName); + + variableWorld = getConfigDataString("variable-world", null); + + variableX = getConfigDataString("variable-x", null); + variableY = getConfigDataString("variable-y", null); + variableZ = getConfigDataString("variable-z", null); + + variableYaw = getConfigDataString("variable-yaw", null); + variablePitch = getConfigDataString("variable-pitch", null); + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedBlockLocation(data); + if (info.noTarget()) return noTarget(info); + + return castAtLocation(info.spellData()); + } + + @Override + public CastResult castAtLocation(SpellData data) { + if (!(data.caster() instanceof Player caster)) return new CastResult(PostCastAction.ALREADY_HANDLED, data); + + VariableManager manager = MagicSpells.getVariableManager(); + Location loc = data.location(); + + manager.set(variableWorld.get(data), caster, loc.getWorld().getName()); + + manager.set(variableX.get(data), caster, loc.x()); + manager.set(variableZ.get(data), caster, loc.z()); + manager.set(variableY.get(data), caster, loc.y()); + + manager.set(variableYaw.get(data), caster, loc.getYaw()); + manager.set(variablePitch.get(data), caster, loc.getPitch()); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + +} From ad79598e7d85a67bceec702716e8dbbc2e184c41 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 8 Jun 2026 19:50:57 +0200 Subject: [PATCH 2/3] feat: PathfindToSpell --- .../spells/targeted/PathfindToSpell.java | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 core/src/main/java/com/nisovin/magicspells/spells/targeted/PathfindToSpell.java diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/PathfindToSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/PathfindToSpell.java new file mode 100644 index 000000000..8aef14d6c --- /dev/null +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/PathfindToSpell.java @@ -0,0 +1,184 @@ +package com.nisovin.magicspells.spells.targeted; + +import java.util.Map; +import java.util.UUID; +import java.util.HashMap; + +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.entity.Mob; +import org.bukkit.util.Vector; +import org.bukkit.event.Listener; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.entity.LivingEntity; +import org.bukkit.util.NumberConversions; + +import com.nisovin.magicspells.Subspell; +import com.nisovin.magicspells.MagicSpells; +import com.nisovin.magicspells.util.SpellData; +import com.nisovin.magicspells.util.CastResult; +import com.nisovin.magicspells.util.TargetInfo; +import com.nisovin.magicspells.util.MagicConfig; +import com.nisovin.magicspells.spells.TargetedSpell; +import com.nisovin.magicspells.util.config.ConfigData; +import com.nisovin.magicspells.spells.TargetedEntitySpell; + +import com.destroystokyo.paper.entity.Pathfinder; +import com.destroystokyo.paper.event.entity.EntityPathfindEvent; + +public class PathfindToSpell extends TargetedSpell implements TargetedEntitySpell { + + private static Monitor monitor; + + private final ConfigData position; + + private final ConfigData speed; + private final ConfigData distanceAllowed; + + private final ConfigData allowInterrupt; + + private Subspell arriveSpell; + + public PathfindToSpell(MagicConfig config, String spellName) { + super(config, spellName); + + position = getConfigDataVector("position", null); + + speed = getConfigDataDouble("speed", 1); + distanceAllowed = getConfigDataDouble("distance-allowed", 1); + + allowInterrupt = getConfigDataBoolean("allow-interrupt", true); + } + + @Override + protected void initialize() { + super.initialize(); + + if (monitor == null) monitor = new Monitor(); + + arriveSpell = initSubspell( + getConfigString("spell-on-arrive", null), + "PathfindToSpell '" + internalName + "' has an invalid 'spell-on-arrive' defined.", + true + ); + } + + @Override + protected void turnOff() { + super.turnOff(); + + if (monitor == null) return; + monitor.stop(); + monitor = null; + } + + @Override + public CastResult cast(SpellData data) { + TargetInfo info = getTargetedEntity(data); + if (info.noTarget()) return noTarget(info); + + return setPath(info.spellData()); + } + + @Override + public CastResult castAtEntity(SpellData data) { + return setPath(data); + } + + private CastResult setPath(SpellData data) { + if (!(data.target() instanceof Mob mob)) return noTarget(data); + + Location destination = position.get(data).toLocation(mob.getWorld()); + + Pathfinder.PathResult path = mob.getPathfinder().findPath(destination); + if (path == null || !path.canReachFinalPoint()) return noTarget(data); + + mob.getPathfinder().moveTo(path, speed.get(data)); + + monitor.track(new MonitorData( + data, + mob.getUniqueId(), + destination, + NumberConversions.square(distanceAllowed.get(data)), + allowInterrupt.get(data), + arriveSpell + )); + + playSpellEffects(data); + return new CastResult(PostCastAction.HANDLE_NORMALLY, data); + } + + private record MonitorData( + SpellData spellData, + UUID target, + Location destination, + double distanceAllowedSquared, + boolean allowInterrupt, + Subspell arriveSpell + ) {} + + private static class Monitor implements Runnable, Listener { + + private final Map mobs = new HashMap<>(); + + private int taskId = -1; + + public void track(MonitorData data) { + mobs.put(data.target(), data); + start(); + } + + public void start() { + if (taskId != -1) return; + + MagicSpells.registerEvents(this); + taskId = MagicSpells.scheduleRepeatingTask(this, 0, 1); + } + + public void stop() { + if (taskId == -1) return; + + EntityPathfindEvent.getHandlerList().unregister(this); + MagicSpells.cancelTask(taskId); + taskId = -1; + } + + @Override + public void run() { + mobs.values().removeIf(Monitor::removeIf); + if (mobs.isEmpty()) stop(); + } + + private static boolean removeIf(MonitorData data) { + if (!(Bukkit.getEntity(data.target()) instanceof Mob mob) || !mob.isValid()) return true; + + if (mob.getLocation().distanceSquared(data.destination()) > data.distanceAllowedSquared()) { + Pathfinder.PathResult path = mob.getPathfinder().getCurrentPath(); + return path == null || !path.canReachFinalPoint(); + } + + mob.getPathfinder().stopPathfinding(); + if (data.arriveSpell() != null) data.arriveSpell().subcast(data.spellData()); + + return true; + } + + @EventHandler(priority = EventPriority.LOWEST) + private void onPath(EntityPathfindEvent event) { + if (!(event.getEntity() instanceof Mob mob)) return; + + MonitorData data = mobs.get(mob.getUniqueId()); + if (data == null) return; + + if (data.allowInterrupt()) { + mobs.remove(data.target()); + return; + } + + event.setCancelled(true); + } + + } + +} From 220a5fdfff5e75d03032edec75c847037e257ee7 Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 8 Jun 2026 19:52:09 +0200 Subject: [PATCH 3/3] fix: "path_to" goal distance comparison --- .../com/nisovin/magicspells/util/ai/goals/PathToGoal.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java b/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java index ceb2f7abc..552a2ae8d 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java +++ b/core/src/main/java/com/nisovin/magicspells/util/ai/goals/PathToGoal.java @@ -5,6 +5,7 @@ import org.bukkit.Location; import org.bukkit.entity.Mob; import org.bukkit.util.Vector; +import org.bukkit.util.NumberConversions; import org.bukkit.configuration.ConfigurationSection; import org.jetbrains.annotations.NotNull; @@ -36,7 +37,7 @@ public PathToGoal(Mob mob, SpellData data) { public boolean initialize(@Nullable ConfigurationSection config) { if (config == null) return false; speed = ConfigDataUtil.getDouble(config, "speed", 1); - position = ConfigDataUtil.getVector(config, "position", new Vector()); + position = ConfigDataUtil.getVector(config, "position", null); distanceAllowed = ConfigDataUtil.getDouble(config, "distance-allowed", 1); return true; } @@ -44,8 +45,7 @@ public boolean initialize(@Nullable ConfigurationSection config) { private void setLocation() { Vector position = PathToGoal.this.position.get(data); if (position == null) return; - - location = new Location(mob.getWorld(), position.getX(), position.getY(), position.getZ()); + location = position.toLocation(mob.getWorld()); } @Override @@ -54,7 +54,7 @@ public boolean shouldActivate() { return location != null && location.isChunkLoaded() && location.getWorld().equals(mob.getWorld()) && - mob.getLocation().distanceSquared(location) > distanceAllowed.get(data); + mob.getLocation().distanceSquared(location) > NumberConversions.square(distanceAllowed.get(data)); } @Override