From 09666dac2cd76c2aa1cd9fc56e7e4abc827e649a Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Mon, 27 Jul 2026 03:30:48 +0200 Subject: [PATCH 1/3] feat: Support any bounding box Also added support to disable entity centering with "center-target". --- .../spells/targeted/EntombSpell.java | 64 +++++++++++++------ 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntombSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntombSpell.java index fe08d7bc6..daf118308 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntombSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/EntombSpell.java @@ -8,6 +8,7 @@ import org.bukkit.Material; import org.bukkit.Location; import org.bukkit.block.Block; +import org.bukkit.util.BoundingBox; import org.bukkit.event.EventHandler; import org.bukkit.entity.LivingEntity; import org.bukkit.block.data.BlockData; @@ -29,6 +30,8 @@ public class EntombSpell extends TargetedSpell implements TargetedEntitySpell { private final ConfigData duration; private final boolean allowBreaking; + + private final ConfigData centerTarget; private final ConfigData closeTopAndBottom; private final ConfigData powerAffectsDuration; @@ -42,6 +45,8 @@ public EntombSpell(MagicConfig config, String spellName) { duration = getConfigDataInt("duration", 20); allowBreaking = getConfigBoolean("allow-breaking", true); + + centerTarget = getConfigDataBoolean("center-target", true); closeTopAndBottom = getConfigDataBoolean("close-top-and-bottom", true); powerAffectsDuration = getConfigDataBoolean("power-affects-duration", true); @@ -73,27 +78,44 @@ public CastResult castAtEntity(SpellData data) { List tombBlocks = new ArrayList<>(); LivingEntity target = data.target(); - Block feet = target.getLocation().getBlock(); - float pitch = target.getLocation().getPitch(); - float yaw = target.getLocation().getYaw(); - - Location tpLoc = feet.getLocation().add(0.5, 0, 0.5); - tpLoc.setYaw(yaw); - tpLoc.setPitch(pitch); - target.teleportAsync(tpLoc); - - tempBlocks.add(feet.getRelative(1, 0, 0)); - tempBlocks.add(feet.getRelative(1, 1, 0)); - tempBlocks.add(feet.getRelative(-1, 0, 0)); - tempBlocks.add(feet.getRelative(-1, 1, 0)); - tempBlocks.add(feet.getRelative(0, 0, 1)); - tempBlocks.add(feet.getRelative(0, 1, 1)); - tempBlocks.add(feet.getRelative(0, 0, -1)); - tempBlocks.add(feet.getRelative(0, 1, -1)); - - if (closeTopAndBottom.get(data)) { - tempBlocks.add(feet.getRelative(0, -1, 0)); - tempBlocks.add(feet.getRelative(0, 2, 0)); + + if (centerTarget.get(data)) { + Location location = target.getLocation(); + location.setX(location.getBlockX() + 0.5); + location.setZ(location.getBlockZ() + 0.5); + target.teleport(location); + } + + BoundingBox box = target.getBoundingBox(); + BoundingBox exp = box.clone().expand(1 - 1e-7); + + int minX = (int) Math.floor(exp.getMinX()); + int minY = (int) Math.floor(exp.getMinY()); + int minZ = (int) Math.floor(exp.getMinZ()); + + int maxX = (int) Math.ceil(exp.getMaxX()); + int maxY = (int) Math.ceil(exp.getMaxY()); + int maxZ = (int) Math.ceil(exp.getMaxZ()); + + boolean closeTopAndBottom = this.closeTopAndBottom.get(data); + for (int x = minX; x < maxX; x++) { + for (int y = minY; y < maxY; y++) { + for (int z = minZ; z < maxZ; z++) { + Block block = target.getWorld().getBlockAt(x, y, z); + if (box.overlaps(BoundingBox.of(block))) continue; + + int boundary = 0; + if (x == minX || x == maxX - 1) boundary++; + if (y == minY || y == maxY - 1) { + if (!closeTopAndBottom) continue; + boundary++; + } + if (z == minZ || z == maxZ - 1) boundary++; + if (boundary > 1) continue; + + tempBlocks.add(block); + } + } } BlockData blockType = this.blockType.get(data); From e274f29b08518fcadbc1ddf383e8c78d525c71bc Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 28 Jul 2026 04:25:05 +0200 Subject: [PATCH 2/3] feat: Improve collision and ground checks --- .../magicspells/spells/buff/DodgeSpell.java | 3 +- .../magicspells/spells/instant/GateSpell.java | 5 +- .../spells/targeted/BlinkSpell.java | 37 +++++++++----- .../spells/targeted/ShadowstepSpell.java | 14 +++-- .../spells/targeted/SummonSpell.java | 14 ++--- .../spells/targeted/TeleportSpell.java | 9 +++- .../nisovin/magicspells/util/BlockUtils.java | 51 +++++++++++++++++-- 7 files changed, 100 insertions(+), 33 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/buff/DodgeSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/buff/DodgeSpell.java index a343da70d..559e2812a 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/buff/DodgeSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/buff/DodgeSpell.java @@ -126,13 +126,14 @@ private void dodge(LivingEntity caster, ParticleProjectileTracker tracker, Spell Vector v = RandomUtils.getRandomCircleVector().multiply(distance); targetLoc.add(v); targetLoc.setDirection(caster.getLocation().getDirection()); + targetLoc = BlockUtils.adjustToSafeLocation(caster, targetLoc); if (spellBeforeDodge != null) { SpellData castData = subData.builder().caster(caster).target(null).location(casterLoc).recipient(null).build(); spellBeforeDodge.subcast(castData); } - if (!targetLoc.getBlock().isPassable() || !targetLoc.getBlock().getRelative(BlockFace.UP).isPassable()) return; + if (targetLoc == null) return; caster.teleportAsync(targetLoc); addUseAndChargeCost(caster); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/GateSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/GateSpell.java index 52729c442..a9b7ee715 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/instant/GateSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/GateSpell.java @@ -3,7 +3,6 @@ import org.bukkit.World; import org.bukkit.Bukkit; import org.bukkit.Location; -import org.bukkit.block.Block; import org.bukkit.entity.Vehicle; import com.nisovin.magicspells.util.*; @@ -84,8 +83,8 @@ public CastResult cast(SpellData data) { } MagicSpells.debug(3, "Gate location: " + location); - Block b = location.getBlock(); - if (!b.isPassable() || !b.getRelative(0, 1, 0).isPassable()) { + location = BlockUtils.adjustToSafeLocation(data.caster(), location); + if (location == null) { MagicSpells.error("GateSpell '" + internalName + "' has landing spot blocked!"); sendMessage(strGateFailed, data.caster(), data); return new CastResult(PostCastAction.ALREADY_HANDLED, data); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/BlinkSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/BlinkSpell.java index 9bc886720..6f0d05b2b 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/BlinkSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/BlinkSpell.java @@ -2,6 +2,7 @@ import org.bukkit.Location; import org.bukkit.block.Block; +import org.bukkit.block.BlockFace; import org.bukkit.util.RayTraceResult; import com.nisovin.magicspells.util.*; @@ -12,15 +13,21 @@ public class BlinkSpell extends TargetedSpell implements TargetedLocationSpell { - private final ConfigData passThroughCeiling; - private final String strCantBlink; + private final ConfigData requireGroundDistance; + + private final ConfigData snapToGround; + private final ConfigData passThroughCeiling; + public BlinkSpell(MagicConfig config, String spellName) { super(config, spellName); strCantBlink = getConfigString("str-cant-blink", "You can't blink there."); + requireGroundDistance = getConfigDataInt("require-ground-distance", 0); + + snapToGround = getConfigDataBoolean("snap-to-ground", false); passThroughCeiling = getConfigDataBoolean("pass-through-ceiling", false); } @@ -30,21 +37,25 @@ public CastResult cast(SpellData data) { if (result == null) return noTarget(strCantBlink, data); Block found = result.getHitBlock(); - Block prev = found.getRelative(result.getHitBlockFace()); + BlockFace face = result.getHitBlockFace(); + Block prev = found.getRelative(face); Location loc = null; - if (!passThroughCeiling.get(data) && found.getRelative(0, -1, 0).equals(prev) && prev.isPassable()) { - Block under = prev.getRelative(0, -1, 0); - if (under.isPassable()) loc = under.getLocation().add(0.5, 0, 0.5); - } else if (found.getRelative(0, 1, 0).isPassable() && found.getRelative(0, 2, 0).isPassable()) { - loc = found.getLocation().add(0.5, 1, 0.5); - } else if (prev.isPassable() && prev.getRelative(0, 1, 0).isPassable()) { - loc = prev.getLocation().add(0.5, 0, 0.5); + + boolean snapToGround = this.snapToGround.get(data); + int requireGroundDistance = this.requireGroundDistance.get(data); + + // Under + if (face == BlockFace.DOWN && !passThroughCeiling.get(data)) { + Location target = prev.getLocation().add(0.5, -1, 0.5); + loc = BlockUtils.adjustToSafeLocation(data.caster(), target, requireGroundDistance, snapToGround); } - if (loc == null) return noTarget(strCantBlink, data); + // Above + if (loc == null) loc = BlockUtils.adjustToSafeLocation(data.caster(), found.getLocation().add(0.5, 0, 0.5)); + // Side + if (loc == null) loc = BlockUtils.adjustToSafeLocation(data.caster(), prev.getLocation().add(0.5, 0, 0.5), requireGroundDistance, snapToGround); - loc.setPitch(data.caster().getPitch()); - loc.setYaw(data.caster().getYaw()); + if (loc == null) return noTarget(strCantBlink, data); SpellTargetLocationEvent targetEvent = new SpellTargetLocationEvent(this, data, loc); if (!targetEvent.callEvent()) return noTarget(strCantBlink, targetEvent); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/ShadowstepSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ShadowstepSpell.java index 008c4d2f1..e1b2adc92 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/ShadowstepSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/ShadowstepSpell.java @@ -1,9 +1,7 @@ package com.nisovin.magicspells.spells.targeted; import org.bukkit.Location; -import org.bukkit.block.Block; import org.bukkit.util.Vector; -import org.bukkit.block.BlockFace; import org.bukkit.entity.LivingEntity; import com.nisovin.magicspells.util.*; @@ -18,6 +16,10 @@ public class ShadowstepSpell extends TargetedSpell implements TargetedEntitySpel private final ConfigData distance; + private final ConfigData snapToGround; + + private final ConfigData requireGroundDistance; + private final ConfigData relativeOffset; private final String strNoLandingSpot; @@ -30,6 +32,10 @@ public ShadowstepSpell(MagicConfig config, String spellName) { distance = getConfigDataDouble("distance", -1); + snapToGround = getConfigDataBoolean("snap-to-ground", false); + + requireGroundDistance = getConfigDataInt("require-ground-distance", 0); + relativeOffset = getConfigDataVector("relative-offset", new Vector(-1, 0, 0)); strNoLandingSpot = getConfigString("str-no-landing-spot", "Cannot shadowstep there."); @@ -60,8 +66,8 @@ public CastResult castAtEntity(SpellData data) { targetLoc.setPitch(pitch.get(data)); targetLoc.setYaw(targetLoc.getYaw() + yaw.get(data)); - Block b = targetLoc.getBlock(); - if (!b.isPassable() || !b.getRelative(BlockFace.UP).isPassable()) return noTarget(strNoLandingSpot, data); + targetLoc = BlockUtils.adjustToSafeLocation(data.caster(), targetLoc, requireGroundDistance.get(data), snapToGround.get(data)); + if (targetLoc == null) return noTarget(strNoLandingSpot, data); playSpellEffects(data.caster(), targetLoc, data); data.caster().teleportAsync(targetLoc); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SummonSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SummonSpell.java index 27b0e9154..6803f5742 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/SummonSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/SummonSpell.java @@ -37,6 +37,7 @@ public class SummonSpell extends TargetedSpell implements TargetedEntitySpell, T private final Map pending; private final ConfigData maxAcceptDelay; + private final ConfigData requireGroundDistance; private final ConfigData requireExactName; private final ConfigData requireAcceptance; @@ -51,6 +52,7 @@ public SummonSpell(MagicConfig config, String spellName) { super(config, spellName); maxAcceptDelay = getConfigDataInt("max-accept-delay", 90); + requireGroundDistance = getConfigDataInt("require-ground-distance", 2); requireExactName = getConfigDataBoolean("require-exact-name", false); requireAcceptance = getConfigDataBoolean("require-acceptance", true); @@ -91,12 +93,6 @@ public CastResult cast(SpellData data) { return new CastResult(PostCastAction.ALREADY_HANDLED, data); } - // Check location - if (!BlockUtils.isSafeToStand(landLoc.clone())) { - sendMessage(strUsage, caster, data); - return new CastResult(PostCastAction.ALREADY_HANDLED, data); - } - // Get player LivingEntity target = requireExactName.get(data) ? Bukkit.getPlayerExact(targetName) : Bukkit.getPlayer(targetName); if (target == null || !validTargetList.canTarget(caster, target)) return noTarget(data); @@ -107,6 +103,12 @@ public CastResult cast(SpellData data) { data = targetEvent.getSpellData(); target = data.target(); + landLoc = BlockUtils.adjustToSafeLocation(target, landLoc, requireGroundDistance.get(data), false); + if (landLoc == null) { + sendMessage(strUsage, caster, data); + return new CastResult(PostCastAction.ALREADY_HANDLED, data); + } + // Teleport player if (requireAcceptance.get(data)) { pending.put(target.getUniqueId(), new SummonData(landLoc, System.currentTimeMillis(), maxAcceptDelay.get(data), data)); diff --git a/core/src/main/java/com/nisovin/magicspells/spells/targeted/TeleportSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/targeted/TeleportSpell.java index 52c9525e8..bf06365a5 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/targeted/TeleportSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/targeted/TeleportSpell.java @@ -17,6 +17,8 @@ public class TeleportSpell extends TargetedSpell implements TargetedEntitySpell private final ConfigData relativeOffset; + private final ConfigData requireGroundDistance; + private final String strCantTeleport; public TeleportSpell(MagicConfig config, String spellName) { @@ -25,7 +27,9 @@ public TeleportSpell(MagicConfig config, String spellName) { yaw = getConfigDataFloat("yaw", 0); pitch = getConfigDataFloat("pitch", 0); - relativeOffset = getConfigDataVector("relative-offset", new Vector(0, 0.1, 0)); + requireGroundDistance = getConfigDataInt("require-ground-distance", 0); + + relativeOffset = getConfigDataVector("relative-offset", new Vector()); strCantTeleport = getConfigString("str-cant-teleport", ""); } @@ -52,7 +56,8 @@ public CastResult castAtEntity(SpellData data) { targetLoc.setPitch(startLoc.getPitch() - pitch.get(data)); targetLoc.setYaw(startLoc.getYaw() + yaw.get(data)); - if (!targetLoc.getBlock().isPassable()) return noTarget(strCantTeleport, data); + targetLoc = BlockUtils.adjustToSafeLocation(data.caster(), targetLoc, requireGroundDistance.get(data), false); + if (targetLoc == null) return noTarget(strCantTeleport, data); playSpellEffects(EffectPosition.CASTER, data.caster(), data); playSpellEffects(EffectPosition.TARGET, data.target(), data); diff --git a/core/src/main/java/com/nisovin/magicspells/util/BlockUtils.java b/core/src/main/java/com/nisovin/magicspells/util/BlockUtils.java index 1c7497b76..fac24ab19 100644 --- a/core/src/main/java/com/nisovin/magicspells/util/BlockUtils.java +++ b/core/src/main/java/com/nisovin/magicspells/util/BlockUtils.java @@ -6,12 +6,16 @@ import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.block.Block; +import org.bukkit.entity.Entity; +import org.bukkit.util.BoundingBox; import org.bukkit.entity.FallingBlock; import org.bukkit.entity.LivingEntity; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Powerable; import org.bukkit.block.data.AnaloguePowerable; +import org.jetbrains.annotations.NotNull; + import com.nisovin.magicspells.Spell; import com.nisovin.magicspells.MagicSpells; import com.nisovin.magicspells.handlers.DebugHandler; @@ -74,10 +78,49 @@ public static boolean isPathable(Material mat) { }; } - public static boolean isSafeToStand(Location location) { - if (!location.getBlock().isPassable()) return false; - if (!location.add(0, 1, 0).getBlock().isPassable()) return false; - return !location.subtract(0, 2, 0).getBlock().isPassable() || !location.subtract(0, 1, 0).getBlock().isPassable(); + public static Location adjustToSafeLocation(@NotNull Entity entity, @NotNull Location location) { + return adjustToSafeLocation(entity, location, 0, false); + } + + public static Location adjustToSafeLocation(@NotNull Entity entity, @NotNull Location location, int requireGroundDistance, boolean snapToGround) { + Location adjusted = location.clone(); + + // Check if the entity collides because the block's collision box. + if (entity.collidesAt(adjusted)) { + if (adjusted.getBlock().isPassable()) { + adjusted.subtract(0, 1, 0); + if (adjusted.getBlock().isPassable()) return null; + } + + adjusted = getYMaxCollision(adjusted); + + if (entity.collidesAt(adjusted)) return null; + } + + if (requireGroundDistance < 1 || !adjusted.getBlock().isPassable()) return adjusted; + + Location ground = adjusted.clone(); + for (int i = 1; i <= requireGroundDistance; i++) { + ground.subtract(0, 1, 0); + if (ground.getBlock().isPassable()) continue; + + return snapToGround ? getYMaxCollision(ground) : adjusted; + } + + return null; + } + + private static Location getYMaxCollision(@NotNull Location location) { + // Note: getBoundingBoxes[i] is [0.0, x] + double maxCollisionY = location.getBlock() + .getCollisionShape() + .getBoundingBoxes() + .stream() + .mapToDouble(BoundingBox::getMaxY) + .max() + .orElse(0); + + return location.clone().add(0, maxCollisionY, 0); } public static void activatePowerable(Block block) { From 010474670f29124518b9bb5d7d47be742ccceaed Mon Sep 17 00:00:00 2001 From: JasperLorelai Date: Tue, 28 Jul 2026 05:05:18 +0200 Subject: [PATCH 3/3] feat: Improve phasing mechanics --- .../spells/instant/PhaseSpell.java | 122 ++++++++++-------- 1 file changed, 68 insertions(+), 54 deletions(-) diff --git a/core/src/main/java/com/nisovin/magicspells/spells/instant/PhaseSpell.java b/core/src/main/java/com/nisovin/magicspells/spells/instant/PhaseSpell.java index 2e13fa0b7..69e0f276b 100644 --- a/core/src/main/java/com/nisovin/magicspells/spells/instant/PhaseSpell.java +++ b/core/src/main/java/com/nisovin/magicspells/spells/instant/PhaseSpell.java @@ -2,10 +2,13 @@ import java.util.List; import java.util.ArrayList; +import java.util.function.Predicate; import org.bukkit.Material; import org.bukkit.Location; import org.bukkit.block.Block; +import org.bukkit.util.BoundingBox; +import org.bukkit.FluidCollisionMode; import org.bukkit.util.BlockIterator; import com.nisovin.magicspells.util.*; @@ -15,34 +18,45 @@ public class PhaseSpell extends InstantSpell { - private final List phasableBlocks; - private final List nonPhasableBlocks; - private final ConfigData maxDistance; + private final ConfigData requireGroundDistance; + + private final ConfigData airOnlyExit; + private final ConfigData snapToGround; + private final ConfigData ignoreTransparentBlocks; private final ConfigData powerAffectsMaxDistance; - private String strCantPhase; + + private final String strCantPhase; + + private final List enterBlocks = new ArrayList<>(); + private final List nonExitBlocks = new ArrayList<>(); + private final List phasableBlocks = new ArrayList<>(); + private final List nonPhasableBlocks = new ArrayList<>(); public PhaseSpell(MagicConfig config, String spellName) { super(config, spellName); maxDistance = getConfigDataInt("max-distance", 15); - strCantPhase = getConfigString("str-cant-phase", "Unable to find place to phase to."); + requireGroundDistance = getConfigDataInt("require-ground-distance", 0); + + airOnlyExit = getConfigDataBoolean("air-only-exit", true); + snapToGround = getConfigDataBoolean("snap-to-ground", false); + ignoreTransparentBlocks = getConfigDataBoolean("ignore-transparent-blocks", false); powerAffectsMaxDistance = getConfigDataBoolean("power-affects-max-distance", true); - phasableBlocks = new ArrayList<>(); - nonPhasableBlocks = new ArrayList<>(); + strCantPhase = getConfigString("str-cant-phase", "Unable to find place to phase to."); + processMaterials(enterBlocks, "enter-blocks"); + processMaterials(nonExitBlocks, "non-exit-blocks"); processMaterials(phasableBlocks, "phasable-blocks"); processMaterials(nonPhasableBlocks, "non-phasable-blocks"); } private void processMaterials(List materials, String path) { - List matList = getConfigStringList(path, null); - if (matList == null || matList.isEmpty()) return; - for (String mat : matList) { + for (String mat : getConfigStringList(path, List.of())) { Material material = Util.getMaterial(mat); if (material == null) { - MagicSpells.error("PhaseSpell has an invalid material specified on '" + path + "': " + mat); + MagicSpells.error("PhaseSpell '" + internalName + "' has an invalid material specified on '" + path + "': " + mat); continue; } materials.add(material); @@ -51,51 +65,68 @@ private void processMaterials(List materials, String path) { @Override public CastResult cast(SpellData data) { - int r = getRange(data); + int range = getRange(data); + int rangeSquared = range * range; + Location casterLoc = data.caster().getLocation(); int distance = maxDistance.get(data); if (powerAffectsMaxDistance.get(data)) distance = Math.round(distance * data.power()); + int distanceSquared = distance * distance; BlockIterator iter; try { - iter = new BlockIterator(data.caster(), distance << 1); + iter = new BlockIterator(data.caster(), distance); } catch (IllegalStateException e) { - sendMessage(strCantPhase, data.caster(), data); + sendMessage(strCantPhase, data); return new CastResult(PostCastAction.ALREADY_HANDLED, data); } - int i = 0; - Block start = null; - Location location = null, casterLoc = data.caster().getLocation(); + Predicate transparent = ignoreTransparentBlocks.get(data) ? isTransparent(data) : l -> l.getBlock().isEmpty(); - while (i++ < r << 1 && iter.hasNext()) { + while (iter.hasNext()) { Block b = iter.next(); - if (b.getType().isAir()) continue; - if (casterLoc.distanceSquared(b.getLocation()) >= r * r) continue; - start = b; - break; + Location loc = b.getLocation(); + + if (enterBlocks.contains(b.getType())) break; + if (transparent.test(loc)) continue; + + if (casterLoc.distanceSquared(loc) < rangeSquared && canPassThrough(b)) break; + + sendMessage(strCantPhase, data); + return new CastResult(PostCastAction.ALREADY_HANDLED, data); } - if (start != null) { - if (canPassThrough(start)) { - while (i++ < distance << 1 && iter.hasNext()) { - Block block = iter.next(); - if (block.getType().isAir() && block.getRelative(0, 1, 0).getType().isAir() && casterLoc.distanceSquared(block.getLocation()) < distance * distance) { - location = block.getLocation(); - break; - } - if (!canPassThrough(block)) break; - } + Location location = null; + boolean airOnlyExit = this.airOnlyExit.get(data); + boolean snapToGround = this.snapToGround.get(data); + int requireGroundDistance = this.requireGroundDistance.get(data); + + while (iter.hasNext()) { + Block block = iter.next(); + Location loc = block.getLocation().add(0.5, 0, 0.5); + + if (casterLoc.distanceSquared(loc) >= distanceSquared) break; + + Location adjusted = BlockUtils.adjustToSafeLocation(data.caster(), loc, requireGroundDistance, snapToGround); + if (adjusted == null) continue; + + BoundingBox box = data.caster().getBoundingBox(); + box.shift(adjusted.clone().subtract(casterLoc)); + if (!Util.hasCollisionsIn(data.caster().getWorld(), box, false, FluidCollisionMode.NEVER, + b -> (airOnlyExit && !b.isEmpty()) || nonExitBlocks.contains(b.getType()) + )) { + location = adjusted; + break; } + + if (!canPassThrough(adjusted.getBlock())) break; } if (location == null) { - sendMessage(strCantPhase, data.caster(), data); + sendMessage(strCantPhase, data); return new CastResult(PostCastAction.ALREADY_HANDLED, data); } - location.setX(location.getX() + 0.5); - location.setZ(location.getZ() + 0.5); location.setPitch(casterLoc.getPitch()); location.setYaw(casterLoc.getYaw()); data = data.location(location); @@ -107,25 +138,8 @@ public CastResult cast(SpellData data) { } private boolean canPassThrough(Block block) { - // Check only blacklist. - if (phasableBlocks.isEmpty()) return !nonPhasableBlocks.contains(block.getType()); - return phasableBlocks.contains(block.getType()) && !nonPhasableBlocks.contains(block.getType()); - } - - public List getPhasableBlocks() { - return phasableBlocks; - } - - public List getNonPhasableBlocks() { - return nonPhasableBlocks; - } - - public String getStrCantPhase() { - return strCantPhase; - } - - public void setStrCantPhase(String strCantPhase) { - this.strCantPhase = strCantPhase; + Material type = block.getType(); + return !nonPhasableBlocks.contains(type) && (phasableBlocks.isEmpty() || phasableBlocks.contains(type)); } }